mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
refactor(svc): tidy the rust services and build tooling (#2735)
This commit is contained in:
@@ -11,7 +11,7 @@ chrono = { version = "0.4", default-features = false }
|
||||
fluxer_common = { path = "../fluxer_common" }
|
||||
fluxer-svc = { path = "../fluxer_svc" }
|
||||
futures = "0.3.32"
|
||||
moka = { version = "0.12.15", features = ["future", "sync"] }
|
||||
moka = { version = "0.12.15", features = ["future"] }
|
||||
rmp-serde = "1.3"
|
||||
scylla = { version = "1.6.0", features = ["chrono-04"], optional = true }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
|
||||
@@ -26,10 +26,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
);
|
||||
|
||||
match config.mode {
|
||||
Mode::Router => {
|
||||
let router = UsersRouter::new(config.cache_max_entries, config.cache_ttl);
|
||||
fluxer_svc::router::run_router(&config, router, transport).await
|
||||
}
|
||||
Mode::Router => fluxer_svc::router::run_router(&config, UsersRouter, transport).await,
|
||||
Mode::Shard => {
|
||||
let shard = match config.database_backend {
|
||||
DatabaseBackend::Postgres => {
|
||||
@@ -37,12 +34,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
fluxer_svc::postgres::PostgresConfig::from_service_config(&config);
|
||||
let pool = fluxer_svc::postgres::connect(&postgres_config).await?;
|
||||
let kv = fluxer_svc::postgres::KvClient::new(pool, &postgres_config)?;
|
||||
UsersShard::new_postgres(
|
||||
kv,
|
||||
transport.clone(),
|
||||
config.cache_max_entries,
|
||||
config.cache_ttl,
|
||||
)?
|
||||
UsersShard::new_postgres(kv, config.cache_max_entries, config.cache_ttl)
|
||||
}
|
||||
DatabaseBackend::Cassandra => {
|
||||
#[cfg(feature = "scylla")]
|
||||
@@ -50,13 +42,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
let scylla_config =
|
||||
fluxer_svc::scylla::ScyllaConfig::from_service_config(&config);
|
||||
let db = fluxer_svc::scylla::connect(&scylla_config).await?;
|
||||
UsersShard::new_scylla(
|
||||
db,
|
||||
transport.clone(),
|
||||
config.cache_max_entries,
|
||||
config.cache_ttl,
|
||||
)
|
||||
.await?
|
||||
UsersShard::new_scylla(db, config.cache_max_entries, config.cache_ttl)
|
||||
.await?
|
||||
}
|
||||
#[cfg(not(feature = "scylla"))]
|
||||
{
|
||||
|
||||
+36
-187
@@ -2,36 +2,24 @@
|
||||
|
||||
use crate::types::{UserRequest, UserResponse};
|
||||
use fluxer_svc::router::RouterService;
|
||||
use moka::sync::Cache;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct UsersRouter {
|
||||
l1: Cache<String, UserResponse>,
|
||||
}
|
||||
|
||||
impl UsersRouter {
|
||||
pub fn new(max_entries: u64, ttl: Duration) -> Self {
|
||||
Self {
|
||||
l1: Cache::builder()
|
||||
.max_capacity(max_entries)
|
||||
.time_to_live(ttl)
|
||||
.build(),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct UsersRouter;
|
||||
|
||||
impl RouterService for UsersRouter {
|
||||
type Request = UserRequest;
|
||||
type Response = UserResponse;
|
||||
|
||||
const CACHES_RESPONSES: bool = false;
|
||||
|
||||
fn service_name(&self) -> &str {
|
||||
"users"
|
||||
}
|
||||
|
||||
fn route_key(req: &UserRequest) -> String {
|
||||
match req {
|
||||
UserRequest::GetById { user_id } => user_id.to_string(),
|
||||
UserRequest::GetPartialById { user_id } => user_id.to_string(),
|
||||
UserRequest::GetById { user_id }
|
||||
| UserRequest::GetPartialById { user_id }
|
||||
| UserRequest::Invalidate { user_id } => user_id.to_string(),
|
||||
UserRequest::GetPartialsByIds { user_ids } => user_ids
|
||||
.iter()
|
||||
.min()
|
||||
@@ -43,7 +31,6 @@ impl RouterService for UsersRouter {
|
||||
.min()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "0".to_owned()),
|
||||
UserRequest::Invalidate { user_id } => user_id.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,117 +61,12 @@ impl RouterService for UsersRouter {
|
||||
}
|
||||
}
|
||||
|
||||
fn l1_lookup(&self, req: &UserRequest) -> Option<UserResponse> {
|
||||
match req {
|
||||
UserRequest::GetById { user_id } => self.l1.get(&user_id.to_string()),
|
||||
UserRequest::GetPartialById { user_id } => {
|
||||
let cached = self.l1.get(&user_id.to_string())?;
|
||||
match cached {
|
||||
UserResponse::Found(ref user) => {
|
||||
Some(UserResponse::FoundPartial(user.to_partial()))
|
||||
}
|
||||
UserResponse::FoundPartial(_) => Some(cached),
|
||||
UserResponse::NotFound => Some(UserResponse::NotFound),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
UserRequest::GetPartialsByIds { user_ids } => {
|
||||
let mut partials = Vec::with_capacity(user_ids.len());
|
||||
for user_id in user_ids {
|
||||
let cached = self.l1.get(&user_id.to_string())?;
|
||||
match cached {
|
||||
UserResponse::Found(ref user) => partials.push(user.to_partial()),
|
||||
UserResponse::FoundPartial(partial) => partials.push(partial),
|
||||
UserResponse::NotFound => {}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
Some(UserResponse::FoundPartials(partials))
|
||||
}
|
||||
UserRequest::GetApiPartialById { user_id } => {
|
||||
let cached = self.l1.get(user_id)?;
|
||||
match cached {
|
||||
UserResponse::Found(ref user) => {
|
||||
Some(UserResponse::FoundApiPartial(user.to_api_partial()))
|
||||
}
|
||||
UserResponse::FoundPartial(ref partial) => {
|
||||
Some(UserResponse::FoundApiPartial(partial.to_api_partial()))
|
||||
}
|
||||
UserResponse::FoundApiPartial(partial) => {
|
||||
Some(UserResponse::FoundApiPartial(partial))
|
||||
}
|
||||
UserResponse::NotFound => Some(UserResponse::NotFound),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
UserRequest::GetApiPartialsByIds { user_ids } => {
|
||||
let mut partials = Vec::with_capacity(user_ids.len());
|
||||
for user_id in user_ids {
|
||||
let cached = self.l1.get(user_id)?;
|
||||
match cached {
|
||||
UserResponse::Found(ref user) => partials.push(user.to_api_partial()),
|
||||
UserResponse::FoundPartial(ref partial) => {
|
||||
partials.push(partial.to_api_partial())
|
||||
}
|
||||
UserResponse::FoundApiPartial(partial) => partials.push(partial),
|
||||
UserResponse::NotFound => {}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
Some(UserResponse::FoundApiPartials(partials))
|
||||
}
|
||||
UserRequest::Invalidate { .. } => None,
|
||||
}
|
||||
fn is_broadcast_request(req: &UserRequest) -> bool {
|
||||
matches!(req, UserRequest::Invalidate { .. })
|
||||
}
|
||||
|
||||
fn l1_insert(&self, req: &UserRequest, resp: &UserResponse) {
|
||||
match req {
|
||||
UserRequest::GetById { user_id } => {
|
||||
self.l1.insert(user_id.to_string(), resp.clone());
|
||||
}
|
||||
UserRequest::GetPartialById { user_id } => {
|
||||
if !matches!(
|
||||
self.l1.get(&user_id.to_string()),
|
||||
Some(UserResponse::Found(_))
|
||||
) {
|
||||
self.l1.insert(user_id.to_string(), resp.clone());
|
||||
}
|
||||
}
|
||||
UserRequest::GetPartialsByIds { .. } => {
|
||||
if let UserResponse::FoundPartials(partials) = resp {
|
||||
for partial in partials {
|
||||
let key = partial.user_id.to_string();
|
||||
if !matches!(self.l1.get(&key), Some(UserResponse::Found(_))) {
|
||||
self.l1
|
||||
.insert(key, UserResponse::FoundPartial(partial.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
UserRequest::GetApiPartialById { .. } => {
|
||||
if let UserResponse::FoundApiPartial(partial) = resp {
|
||||
self.l1.insert(
|
||||
partial.id.clone(),
|
||||
UserResponse::FoundApiPartial(partial.clone()),
|
||||
);
|
||||
}
|
||||
}
|
||||
UserRequest::GetApiPartialsByIds { .. } => {
|
||||
if let UserResponse::FoundApiPartials(partials) = resp {
|
||||
for partial in partials {
|
||||
self.l1.insert(
|
||||
partial.id.clone(),
|
||||
UserResponse::FoundApiPartial(partial.clone()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
UserRequest::Invalidate { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn l1_invalidate(&self, key: &str) {
|
||||
self.l1.invalidate(key);
|
||||
fn is_broadcast_acknowledgement(response: &UserResponse) -> bool {
|
||||
matches!(response, UserResponse::Invalidated)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,72 +74,39 @@ impl RouterService for UsersRouter {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn api_partial(id: &str) -> crate::types::ApiUserPartial {
|
||||
crate::types::ApiUserPartial {
|
||||
id: id.to_owned(),
|
||||
username: "Ada".to_owned(),
|
||||
discriminator: "0007".to_owned(),
|
||||
global_name: Some("Ada Lovelace".to_owned()),
|
||||
avatar: Some("avatar_hash".to_owned()),
|
||||
avatar_color: Some(0x336699),
|
||||
bot: None,
|
||||
system: None,
|
||||
flags: 1,
|
||||
mention_flags: None,
|
||||
}
|
||||
#[test]
|
||||
fn coalesce_key_ignores_batch_id_order() {
|
||||
let forward = UserRequest::GetPartialsByIds {
|
||||
user_ids: vec![7, 42, 7],
|
||||
};
|
||||
let reversed = UserRequest::GetPartialsByIds {
|
||||
user_ids: vec![42, 7],
|
||||
};
|
||||
assert_eq!(
|
||||
UsersRouter::coalesce_key(&forward),
|
||||
UsersRouter::coalesce_key(&reversed)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn l1_caches_api_partial_batch_responses() {
|
||||
let router = UsersRouter::new(100, Duration::from_secs(30));
|
||||
let partial = api_partial("9223372036854775807");
|
||||
router.l1_insert(
|
||||
&UserRequest::GetApiPartialsByIds {
|
||||
user_ids: vec![partial.id.clone()],
|
||||
},
|
||||
&UserResponse::FoundApiPartials(vec![partial.clone()]),
|
||||
fn coalesce_key_separates_batches_with_different_ids() {
|
||||
let left = UserRequest::GetApiPartialsByIds {
|
||||
user_ids: vec!["7".to_owned(), "42".to_owned()],
|
||||
};
|
||||
let right = UserRequest::GetApiPartialsByIds {
|
||||
user_ids: vec!["7".to_owned()],
|
||||
};
|
||||
assert_ne!(
|
||||
UsersRouter::coalesce_key(&left),
|
||||
UsersRouter::coalesce_key(&right)
|
||||
);
|
||||
|
||||
let cached = router.l1_lookup(&UserRequest::GetApiPartialsByIds {
|
||||
user_ids: vec![partial.id.clone()],
|
||||
});
|
||||
|
||||
match cached {
|
||||
Some(UserResponse::FoundApiPartials(partials)) => {
|
||||
assert_eq!(partials.len(), 1);
|
||||
assert_eq!(partials[0].id, partial.id);
|
||||
assert_eq!(partials[0].username, partial.username);
|
||||
}
|
||||
other => panic!("unexpected cached response: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn l1_invalidates_cached_api_partials_by_user_id() {
|
||||
let router = UsersRouter::new(100, Duration::from_secs(30));
|
||||
let partial = api_partial("42");
|
||||
router.l1_insert(
|
||||
&UserRequest::GetApiPartialById {
|
||||
user_id: partial.id.clone(),
|
||||
},
|
||||
&UserResponse::FoundApiPartial(partial.clone()),
|
||||
);
|
||||
|
||||
assert!(
|
||||
router
|
||||
.l1_lookup(&UserRequest::GetApiPartialById {
|
||||
user_id: partial.id.clone(),
|
||||
})
|
||||
.is_some()
|
||||
);
|
||||
router.l1_invalidate(&partial.id);
|
||||
|
||||
assert!(
|
||||
router
|
||||
.l1_lookup(&UserRequest::GetApiPartialById {
|
||||
user_id: partial.id,
|
||||
})
|
||||
.is_none()
|
||||
fn invalidations_are_never_coalesced() {
|
||||
assert_eq!(
|
||||
UsersRouter::coalesce_key(&UserRequest::Invalidate { user_id: 42 }),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+167
-131
@@ -4,7 +4,6 @@ use crate::types::{ApiUserPartial, User, UserPartial, UserRequest, UserResponse}
|
||||
#[cfg(feature = "scylla")]
|
||||
use chrono::{DateTime, NaiveDate, Utc};
|
||||
use fluxer_svc::shard::ShardService;
|
||||
use fluxer_svc::transport::NatsTransport;
|
||||
use fluxer_svc::{postgres, postgres::KeyPart};
|
||||
use futures::stream::{self, StreamExt};
|
||||
use moka::future::Cache;
|
||||
@@ -17,8 +16,12 @@ use scylla::statement::prepared::PreparedStatement;
|
||||
#[cfg(feature = "scylla")]
|
||||
use scylla::value::MaybeEmpty;
|
||||
use serde::Deserialize;
|
||||
use std::collections::{HashMap, hash_map::DefaultHasher};
|
||||
use std::fmt::Write;
|
||||
use std::future::Future;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(feature = "scylla")]
|
||||
@@ -60,6 +63,8 @@ const PARTIAL_USER_COLUMNS: &str = "\
|
||||
mention_flags";
|
||||
const USER_BATCH_SIZE: usize = 128;
|
||||
const USER_BATCH_CONCURRENCY: usize = 8;
|
||||
const USER_CACHE_MIN_GENERATION_STRIPES: usize = 4096;
|
||||
const USER_CACHE_MAX_GENERATION_STRIPES: usize = 1 << 20;
|
||||
const FLUXER_SYSTEM_USER_ID: i64 = 0;
|
||||
const FLUXER_SYSTEM_USERNAME: &str = "Fluxer";
|
||||
const FLUXER_SYSTEM_DISCRIMINATOR: i32 = 0;
|
||||
@@ -68,12 +73,19 @@ const USER_FLAG_STAFF: i64 = 1;
|
||||
pub struct UsersShard {
|
||||
storage: UsersStorage,
|
||||
caches: UserCaches,
|
||||
transport: NatsTransport,
|
||||
}
|
||||
|
||||
struct UserCaches {
|
||||
full: Cache<i64, Option<User>>,
|
||||
partial: Cache<i64, Option<UserPartial>>,
|
||||
full: Cache<UserCacheKey, Option<User>>,
|
||||
partial: Cache<UserCacheKey, Option<UserPartial>>,
|
||||
generations: Box<[AtomicU64]>,
|
||||
generation_bumps: AtomicU64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
|
||||
struct UserCacheKey {
|
||||
user_id: i64,
|
||||
generation: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -159,26 +171,9 @@ struct FullUserDbRow {
|
||||
timezone_privacy_flags: Option<i32>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "scylla")]
|
||||
#[derive(Debug, DeserializeRow)]
|
||||
struct PartialUserDbRow {
|
||||
user_id: i64,
|
||||
username: String,
|
||||
discriminator: i32,
|
||||
global_name: Option<String>,
|
||||
avatar_hash: Option<String>,
|
||||
bot: Option<bool>,
|
||||
system: Option<bool>,
|
||||
flags: Option<i64>,
|
||||
banner_hash: Option<String>,
|
||||
banner_color: Option<i32>,
|
||||
accent_color: Option<i32>,
|
||||
avatar_color: Option<i32>,
|
||||
mention_flags: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PartialUserKvRow {
|
||||
#[cfg_attr(feature = "scylla", derive(DeserializeRow))]
|
||||
struct PartialUserDbRow {
|
||||
user_id: i64,
|
||||
username: String,
|
||||
discriminator: i32,
|
||||
@@ -256,6 +251,16 @@ struct FullUserKvRow {
|
||||
timezone_privacy_flags: Option<i32>,
|
||||
}
|
||||
|
||||
fn generation_stripes(max_entries: u64) -> usize {
|
||||
usize::try_from(max_entries)
|
||||
.unwrap_or(USER_CACHE_MAX_GENERATION_STRIPES)
|
||||
.clamp(
|
||||
USER_CACHE_MIN_GENERATION_STRIPES,
|
||||
USER_CACHE_MAX_GENERATION_STRIPES,
|
||||
)
|
||||
.next_power_of_two()
|
||||
}
|
||||
|
||||
impl UserCaches {
|
||||
fn new(max_entries: u64, ttl: Duration) -> Self {
|
||||
Self {
|
||||
@@ -267,6 +272,23 @@ impl UserCaches {
|
||||
.max_capacity(max_entries)
|
||||
.time_to_live(ttl)
|
||||
.build(),
|
||||
generations: (0..generation_stripes(max_entries))
|
||||
.map(|_| AtomicU64::new(0))
|
||||
.collect(),
|
||||
generation_bumps: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn generation(&self, user_id: i64) -> &AtomicU64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
user_id.hash(&mut hasher);
|
||||
&self.generations[hasher.finish() as usize % self.generations.len()]
|
||||
}
|
||||
|
||||
fn key(&self, user_id: i64) -> UserCacheKey {
|
||||
UserCacheKey {
|
||||
user_id,
|
||||
generation: self.generation(user_id).load(Ordering::SeqCst),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,13 +296,14 @@ impl UserCaches {
|
||||
where
|
||||
F: Future<Output = anyhow::Result<Option<User>>>,
|
||||
{
|
||||
let key = self.key(user_id);
|
||||
let user = self
|
||||
.full
|
||||
.try_get_with(user_id, fetch)
|
||||
.try_get_with(key, fetch)
|
||||
.await
|
||||
.map_err(|e: Arc<anyhow::Error>| anyhow::anyhow!("{e}"))?;
|
||||
self.partial
|
||||
.insert(user_id, user.as_ref().map(User::to_partial))
|
||||
.insert(key, user.as_ref().map(User::to_partial))
|
||||
.await;
|
||||
Ok(user)
|
||||
}
|
||||
@@ -294,43 +317,47 @@ impl UserCaches {
|
||||
F: Future<Output = anyhow::Result<Option<UserPartial>>>,
|
||||
{
|
||||
self.partial
|
||||
.try_get_with(user_id, fetch)
|
||||
.try_get_with(self.key(user_id), fetch)
|
||||
.await
|
||||
.map_err(|e: Arc<anyhow::Error>| anyhow::anyhow!("{e}"))
|
||||
}
|
||||
|
||||
async fn get_partial(&self, user_id: i64) -> Option<Option<UserPartial>> {
|
||||
self.partial.get(&user_id).await
|
||||
self.partial.get(&self.key(user_id)).await
|
||||
}
|
||||
|
||||
async fn insert_partial(&self, user_id: i64, partial: Option<UserPartial>) {
|
||||
self.partial.insert(user_id, partial).await;
|
||||
async fn insert_partial(&self, key: UserCacheKey, partial: Option<UserPartial>) {
|
||||
self.partial.insert(key, partial).await;
|
||||
}
|
||||
|
||||
async fn invalidate(&self, user_id: i64) {
|
||||
self.full.invalidate(&user_id).await;
|
||||
self.partial.invalidate(&user_id).await;
|
||||
let generation = self
|
||||
.generation(user_id)
|
||||
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| {
|
||||
current.checked_add(1)
|
||||
})
|
||||
.expect("user cache generation exhausted");
|
||||
self.generation_bumps.fetch_add(1, Ordering::Relaxed);
|
||||
let key = UserCacheKey {
|
||||
user_id,
|
||||
generation,
|
||||
};
|
||||
self.full.invalidate(&key).await;
|
||||
self.partial.invalidate(&key).await;
|
||||
}
|
||||
}
|
||||
|
||||
impl UsersShard {
|
||||
pub fn new_postgres(
|
||||
kv: postgres::KvClient,
|
||||
transport: NatsTransport,
|
||||
max_entries: u64,
|
||||
ttl: Duration,
|
||||
) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
pub fn new_postgres(kv: postgres::KvClient, max_entries: u64, ttl: Duration) -> Self {
|
||||
Self {
|
||||
storage: UsersStorage::Postgres(PostgresUsersStorage { kv }),
|
||||
caches: UserCaches::new(max_entries, ttl),
|
||||
transport,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "scylla")]
|
||||
pub async fn new_scylla(
|
||||
db: Arc<Session>,
|
||||
transport: NatsTransport,
|
||||
max_entries: u64,
|
||||
ttl: Duration,
|
||||
) -> anyhow::Result<Self> {
|
||||
@@ -358,7 +385,6 @@ impl UsersShard {
|
||||
stmt_partial_batch,
|
||||
})),
|
||||
caches: UserCaches::new(max_entries, ttl),
|
||||
transport,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -366,12 +392,8 @@ impl UsersShard {
|
||||
if user_id == FLUXER_SYSTEM_USER_ID {
|
||||
return Ok(Some(fluxer_system_user()));
|
||||
}
|
||||
let storage = self.storage.clone();
|
||||
self.caches
|
||||
.get_or_fetch_full(
|
||||
user_id,
|
||||
async move { storage.fetch_full_user(user_id).await },
|
||||
)
|
||||
.get_or_fetch_full(user_id, self.storage.fetch_full_user(user_id))
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -379,17 +401,12 @@ impl UsersShard {
|
||||
if user_id == FLUXER_SYSTEM_USER_ID {
|
||||
return Ok(Some(fluxer_system_user().to_partial()));
|
||||
}
|
||||
let storage = self.storage.clone();
|
||||
self.caches
|
||||
.get_or_fetch_partial(
|
||||
user_id,
|
||||
async move { storage.fetch_partial_user(user_id).await },
|
||||
)
|
||||
.get_or_fetch_partial(user_id, self.storage.fetch_partial_user(user_id))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_partial_users(&self, user_ids: Vec<i64>) -> anyhow::Result<Vec<UserPartial>> {
|
||||
let mut user_ids = user_ids;
|
||||
async fn get_partial_users(&self, mut user_ids: Vec<i64>) -> anyhow::Result<Vec<UserPartial>> {
|
||||
user_ids.sort_unstable();
|
||||
user_ids.dedup();
|
||||
let mut partials = Vec::new();
|
||||
@@ -451,43 +468,40 @@ impl UsersShard {
|
||||
}
|
||||
|
||||
async fn fetch_partial_batch(&self, user_ids: Vec<i64>) -> anyhow::Result<Vec<UserPartial>> {
|
||||
let mut partials = Vec::new();
|
||||
let user_ids = user_ids
|
||||
.into_iter()
|
||||
.filter(|user_id| {
|
||||
if *user_id == FLUXER_SYSTEM_USER_ID {
|
||||
partials.push(fluxer_system_user().to_partial());
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if user_ids.is_empty() {
|
||||
return Ok(partials);
|
||||
}
|
||||
let mut cache_keys = user_ids
|
||||
.iter()
|
||||
.map(|&user_id| (user_id, self.caches.key(user_id)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let fetched_partials = match self.storage.fetch_partial_batch(user_ids.clone()).await {
|
||||
Ok(partials) => partials,
|
||||
Err(_) => {
|
||||
partials.extend(self.fetch_partial_batch_individually(user_ids).await?);
|
||||
return Ok(partials);
|
||||
Ok(fetched) => fetched,
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
user_count = user_ids.len(),
|
||||
"user partial batch read failed, retrying per user"
|
||||
);
|
||||
return self.fetch_partial_batch_individually(user_ids).await;
|
||||
}
|
||||
};
|
||||
let found_ids = fetched_partials
|
||||
.iter()
|
||||
.map(|partial| partial.user_id)
|
||||
.collect::<std::collections::HashSet<_>>();
|
||||
for partial in &fetched_partials {
|
||||
self.caches
|
||||
.insert_partial(partial.user_id, Some(partial.clone()))
|
||||
.await;
|
||||
let mut unexpected = 0usize;
|
||||
let mut partials = Vec::with_capacity(fetched_partials.len());
|
||||
for partial in fetched_partials {
|
||||
let Some(key) = cache_keys.remove(&partial.user_id) else {
|
||||
unexpected += 1;
|
||||
continue;
|
||||
};
|
||||
self.caches.insert_partial(key, Some(partial.clone())).await;
|
||||
partials.push(partial);
|
||||
}
|
||||
for user_id in user_ids {
|
||||
if !found_ids.contains(&user_id) {
|
||||
self.caches.insert_partial(user_id, None).await;
|
||||
}
|
||||
if unexpected > 0 {
|
||||
tracing::warn!(
|
||||
unexpected,
|
||||
"user batch returned duplicate or unrequested users"
|
||||
);
|
||||
}
|
||||
for key in cache_keys.into_values() {
|
||||
self.caches.insert_partial(key, None).await;
|
||||
}
|
||||
partials.extend(fetched_partials);
|
||||
Ok(partials)
|
||||
}
|
||||
|
||||
@@ -603,7 +617,7 @@ fn decode_postgres_user(row: serde_json::Value) -> anyhow::Result<User> {
|
||||
|
||||
fn decode_postgres_user_partial(row: serde_json::Value) -> anyhow::Result<UserPartial> {
|
||||
let row = postgres::decode_row_dates_as_millis(row)?;
|
||||
let row: PartialUserKvRow = serde_json::from_value(row)?;
|
||||
let row: PartialUserDbRow = serde_json::from_value(row)?;
|
||||
Ok(row.into())
|
||||
}
|
||||
|
||||
@@ -678,34 +692,49 @@ impl ShardService for UsersShard {
|
||||
"users"
|
||||
}
|
||||
|
||||
fn render_prometheus_metrics(&self, output: &mut String) {
|
||||
let _ = writeln!(
|
||||
output,
|
||||
"# TYPE fluxer_users_shard_cache_generation_bumps_total counter"
|
||||
);
|
||||
let _ = writeln!(
|
||||
output,
|
||||
"fluxer_users_shard_cache_generation_bumps_total {}",
|
||||
self.caches.generation_bumps.load(Ordering::Relaxed)
|
||||
);
|
||||
let _ = writeln!(
|
||||
output,
|
||||
"# TYPE fluxer_users_shard_cache_generation_stripes gauge"
|
||||
);
|
||||
let _ = writeln!(
|
||||
output,
|
||||
"fluxer_users_shard_cache_generation_stripes {}",
|
||||
self.caches.generations.len()
|
||||
);
|
||||
}
|
||||
|
||||
async fn handle(&self, request: UserRequest) -> anyhow::Result<UserResponse> {
|
||||
match request {
|
||||
UserRequest::GetById { user_id } => match self.get_full_user(user_id).await? {
|
||||
Some(user) => Ok(UserResponse::Found(user)),
|
||||
None => Ok(UserResponse::NotFound),
|
||||
},
|
||||
UserRequest::GetPartialById { user_id } => {
|
||||
match self.get_partial_user(user_id).await? {
|
||||
Some(partial) => Ok(UserResponse::FoundPartial(partial)),
|
||||
None => Ok(UserResponse::NotFound),
|
||||
}
|
||||
}
|
||||
UserRequest::GetById { user_id } => Ok(self
|
||||
.get_full_user(user_id)
|
||||
.await?
|
||||
.map_or(UserResponse::NotFound, UserResponse::Found)),
|
||||
UserRequest::GetPartialById { user_id } => Ok(self
|
||||
.get_partial_user(user_id)
|
||||
.await?
|
||||
.map_or(UserResponse::NotFound, UserResponse::FoundPartial)),
|
||||
UserRequest::GetPartialsByIds { user_ids } => Ok(UserResponse::FoundPartials(
|
||||
self.get_partial_users(user_ids).await?,
|
||||
)),
|
||||
UserRequest::GetApiPartialById { user_id } => {
|
||||
match self.get_api_partial_user(user_id).await? {
|
||||
Some(partial) => Ok(UserResponse::FoundApiPartial(partial)),
|
||||
None => Ok(UserResponse::NotFound),
|
||||
}
|
||||
}
|
||||
UserRequest::GetApiPartialById { user_id } => Ok(self
|
||||
.get_api_partial_user(user_id)
|
||||
.await?
|
||||
.map_or(UserResponse::NotFound, UserResponse::FoundApiPartial)),
|
||||
UserRequest::GetApiPartialsByIds { user_ids } => Ok(UserResponse::FoundApiPartials(
|
||||
self.get_api_partial_users(user_ids).await?,
|
||||
)),
|
||||
UserRequest::Invalidate { user_id } => {
|
||||
self.caches.invalidate(user_id).await;
|
||||
let subject = format!("svc.users.invalidate.{user_id}");
|
||||
self.transport.publish(&subject, &[]).await?;
|
||||
Ok(UserResponse::Invalidated)
|
||||
}
|
||||
}
|
||||
@@ -734,7 +763,6 @@ fn optional_date_string(value: OptionalDate) -> Option<String> {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "scylla")]
|
||||
impl From<PartialUserDbRow> for UserPartial {
|
||||
fn from(row: PartialUserDbRow) -> Self {
|
||||
Self {
|
||||
@@ -755,26 +783,6 @@ impl From<PartialUserDbRow> for UserPartial {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PartialUserKvRow> for UserPartial {
|
||||
fn from(row: PartialUserKvRow) -> Self {
|
||||
Self {
|
||||
user_id: row.user_id,
|
||||
username: row.username,
|
||||
discriminator: row.discriminator,
|
||||
global_name: row.global_name,
|
||||
avatar_hash: row.avatar_hash,
|
||||
bot: row.bot,
|
||||
system: row.system,
|
||||
flags: row.flags,
|
||||
banner_hash: row.banner_hash,
|
||||
banner_color: row.banner_color,
|
||||
accent_color: row.accent_color,
|
||||
avatar_color: row.avatar_color,
|
||||
mention_flags: row.mention_flags,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "scylla")]
|
||||
impl From<FullUserDbRow> for User {
|
||||
fn from(row: FullUserDbRow) -> Self {
|
||||
@@ -967,7 +975,7 @@ mod tests {
|
||||
caches.get_partial(42).await.unwrap().unwrap().username,
|
||||
"Ada"
|
||||
);
|
||||
assert!(caches.full.get(&42).await.is_none());
|
||||
assert!(caches.full.get(&caches.key(42)).await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1001,7 +1009,14 @@ mod tests {
|
||||
|
||||
assert_eq!(fetched.email.as_deref(), Some("[email protected]"));
|
||||
assert_eq!(
|
||||
caches.full.get(&42).await.unwrap().unwrap().bio.as_deref(),
|
||||
caches
|
||||
.full
|
||||
.get(&caches.key(42))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.bio
|
||||
.as_deref(),
|
||||
Some("analytical engine enjoyer")
|
||||
);
|
||||
let cached_partial = caches.get_partial(42).await.unwrap().unwrap();
|
||||
@@ -1019,7 +1034,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert!(fetched.is_none());
|
||||
assert!(matches!(caches.full.get(&42).await, Some(None)));
|
||||
assert!(matches!(caches.full.get(&caches.key(42)).await, Some(None)));
|
||||
assert!(matches!(caches.get_partial(42).await, Some(None)));
|
||||
}
|
||||
|
||||
@@ -1030,12 +1045,12 @@ mod tests {
|
||||
.get_or_fetch_full(42, async { Ok(Some(test_user(42))) })
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(caches.full.get(&42).await.is_some());
|
||||
assert!(caches.full.get(&caches.key(42)).await.is_some());
|
||||
assert!(caches.get_partial(42).await.is_some());
|
||||
|
||||
caches.invalidate(42).await;
|
||||
|
||||
assert!(caches.full.get(&42).await.is_none());
|
||||
assert!(caches.full.get(&caches.key(42)).await.is_none());
|
||||
assert!(caches.get_partial(42).await.is_none());
|
||||
}
|
||||
|
||||
@@ -1052,6 +1067,27 @@ mod tests {
|
||||
assert!(caches.get_partial(42).await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalidation_counts_generation_bumps() {
|
||||
let caches = caches();
|
||||
assert_eq!(caches.generation_bumps.load(Ordering::Relaxed), 0);
|
||||
|
||||
caches.invalidate(42).await;
|
||||
caches.invalidate(43).await;
|
||||
|
||||
assert_eq!(caches.generation_bumps.load(Ordering::Relaxed), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generation_stripes_follow_the_configured_capacity() {
|
||||
assert_eq!(generation_stripes(16), USER_CACHE_MIN_GENERATION_STRIPES);
|
||||
assert_eq!(generation_stripes(100_000), 131_072);
|
||||
assert_eq!(
|
||||
generation_stripes(u64::MAX),
|
||||
USER_CACHE_MAX_GENERATION_STRIPES
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "scylla")]
|
||||
#[test]
|
||||
fn partial_columns_match_the_user_partial_fields_exactly() {
|
||||
|
||||
@@ -159,24 +159,6 @@ impl User {
|
||||
mention_flags: self.mention_flags,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_api_partial(&self) -> ApiUserPartial {
|
||||
if self.user_id == FLUXER_SYSTEM_USER_ID {
|
||||
return fluxer_system_user();
|
||||
}
|
||||
ApiUserPartial {
|
||||
id: self.user_id.to_string(),
|
||||
username: self.username.clone(),
|
||||
discriminator: format!("{:04}", self.discriminator),
|
||||
global_name: self.global_name.clone(),
|
||||
avatar: self.avatar_hash.clone(),
|
||||
avatar_color: self.avatar_color,
|
||||
bot: self.bot.filter(|bot| *bot),
|
||||
system: self.system.filter(|system| *system),
|
||||
flags: visible_user_flags(self.flags.unwrap_or_default()),
|
||||
mention_flags: self.mention_flags.filter(|flags| *flags != 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UserPartial {
|
||||
@@ -320,30 +302,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_api_partial_matches_the_two_step_conversion() {
|
||||
for user_id in [123, FLUXER_SYSTEM_USER_ID] {
|
||||
for flags in [
|
||||
0,
|
||||
USER_FLAG_STAFF,
|
||||
USER_FLAG_STAFF | USER_FLAG_STAFF_HIDDEN | USER_FLAG_PARTNER,
|
||||
USER_FLAG_DELETED,
|
||||
] {
|
||||
let user = user_with_flags(user_id, flags);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(user.to_api_partial()).unwrap(),
|
||||
serde_json::to_value(user.to_partial().to_api_partial()).unwrap(),
|
||||
"user_id {user_id} flags {flags}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_api_partial_ignores_fields_outside_the_partial() {
|
||||
fn api_partial_ignores_fields_outside_the_partial() {
|
||||
let mut user = user_with_flags(123, USER_FLAG_STAFF);
|
||||
user.mention_flags = Some(0);
|
||||
let api_partial = user.to_api_partial();
|
||||
let api_partial = user.to_partial().to_api_partial();
|
||||
|
||||
assert_eq!(api_partial.id, "123");
|
||||
assert_eq!(api_partial.username, "Ada");
|
||||
|
||||
Reference in New Issue
Block a user