feat(avatar): update avatar URL caching strategy and TTL for improved performance
ZhenYi committed
This change is very large, so some diffs are not shown. Clone the repository to see the full change.
Modified lib/service/space/src/handlers/get_space_image_file.rs lib/service/space/src/handlers/get_space_image_file.rs +1 −1 View lib/service/space/src/handlers/get_space_image_file.rs
@@ -21,7 +21,7 @@
2121 get,
2222 path = "/spaces/{space_name}/images/{kind}/file",
2323 tag = "space",
24 description = "重定向到 S3 预签名空间图片链接(7 天有效, Redis 缓存)。默认返回压缩后的预览图,`?variant=original` 返回上传原图。私有社区对非成员返回 404。",
24 description = "重定向到 S3 预签名空间图片链接(1 小时有效, Redis 缓存 48 分钟)。默认返回压缩后的预览图,`?variant=original` 返回上传原图。私有社区对非成员返回 404。",
2525 params(SpaceImageKindPath, SpaceImageVariantQuery),
2626 responses(
2727 (status = 302, description = "redirect to presigned s3 space image url"),
Modified lib/service/space/src/services/images.rs lib/service/space/src/services/images.rs +45 −18 View lib/service/space/src/services/images.rs
@@ -34,8 +34,11 @@
3434
3535pub const MAX_SPACE_IMAGE_BYTES: usize = 5 * 1024 * 1024;
3636pub const SPACE_IMAGE_PREFIX: &str = "space-images";
37pub const SPACE_IMAGE_URL_TTL_SECS: u64 = 7 * 24 * 60 * 60;
38pub const SPACE_IMAGE_URL_CACHE_PREFIX: &str = "space:image:url";
37pub const SPACE_IMAGE_URL_TTL_SECS: u64 = 60 * 60;
38/// Strictly shorter than the signature lifetime, so a cache hit can never hand
39/// out a URL that is about to expire.
40pub const SPACE_IMAGE_URL_CACHE_TTL_SECS: u64 = SPACE_IMAGE_URL_TTL_SECS / 5 * 4;
41pub const SPACE_IMAGE_URL_CACHE_PREFIX: &str = "space:image:url:v2";
3942
4043#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
4144#[serde(rename_all = "lowercase")]
@@ -108,9 +111,19 @@
108111 }
109112}
110113
111fn space_image_url_cache_key(space_id: Uuid, kind: SpaceImageKind) -> String {
114/// Keyed by variant too: the preview and the original are different objects, so
115/// sharing one key would pin whichever representation was requested first.
116fn space_image_url_cache_key(
117 space_id: Uuid,
118 kind: SpaceImageKind,
119 variant: ImageVariant,
120) -> String {
121 let variant = match variant {
122 ImageVariant::Preview => "preview",
123 ImageVariant::Original => "original",
124 };
112125 format!(
113 "{SPACE_IMAGE_URL_CACHE_PREFIX}:{space_id}:{}",
126 "{SPACE_IMAGE_URL_CACHE_PREFIX}:{space_id}:{}:{variant}",
114127 kind.as_str()
115128 )
116129}
@@ -387,9 +400,10 @@
387400 &self,
388401 space_id: Uuid,
389402 kind: SpaceImageKind,
403 variant: ImageVariant,
390404 stored_url: &str,
391405 ) -> Result<String, SpaceError> {
392 let cache_key = space_image_url_cache_key(space_id, kind);
406 let cache_key = space_image_url_cache_key(space_id, kind, variant);
393407 match cache::client::get_string(&self.redis, &cache_key).await {
394408 Ok(Some(url)) => return Ok(url),
395409 Ok(None) => {}
@@ -402,14 +416,34 @@
402416 .storage
403417 .as_ref()
404418 .presign_get(&key, SPACE_IMAGE_URL_TTL_SECS as u32)?;
405 if let Err(error) =
406 cache::client::set_string(&self.redis, &cache_key, &url, SPACE_IMAGE_URL_TTL_SECS).await
419 if let Err(error) = cache::client::set_string(
420 &self.redis,
421 &cache_key,
422 &url,
423 SPACE_IMAGE_URL_CACHE_TTL_SECS,
424 )
425 .await
407426 {
408427 tracing::warn!(%error, "failed to cache space image presigned url");
409428 }
410429 Ok(url)
411430 }
412431
432 /// Drop the cached URL of both representations; keys are variant-scoped, so
433 /// one invalidation target is no longer enough.
434 async fn invalidate_space_image_url_cache(&self, space_id: Uuid, kind: SpaceImageKind) {
435 for variant in [ImageVariant::Preview, ImageVariant::Original] {
436 if let Err(error) = cache::client::delete(
437 &self.redis,
438 &space_image_url_cache_key(space_id, kind, variant),
439 )
440 .await
441 {
442 tracing::warn!(%error, "failed to invalidate space image url cache");
443 }
444 }
445 }
446
413447 /// Member-visible space row for an image read, mirroring `get_space`:
414448 /// private spaces are a 404 for strangers rather than a 403, so the image
415449 /// URLs leak nothing about which spaces exist.
@@ -501,11 +535,7 @@
501535 )
502536 .await?;
503537
504 if let Err(error) =
505 cache::client::delete(&self.redis, &space_image_url_cache_key(space_id, kind)).await
506 {
507 tracing::warn!(%error, "failed to invalidate space image url cache");
508 }
538 self.invalidate_space_image_url_cache(space_id, kind).await;
509539
510540 // The derivative is built here so the very next page load already gets
511541 // the small object; the same extension may have been in use before, so
@@ -581,11 +611,7 @@
581611
582612 write_space_image(&self.access, space_id, kind, None, None, None).await?;
583613
584 if let Err(error) =
585 cache::client::delete(&self.redis, &space_image_url_cache_key(space_id, kind)).await
586 {
587 tracing::warn!(%error, "failed to invalidate space image url cache");
588 }
614 self.invalidate_space_image_url_cache(space_id, kind).await;
589615
590616 if let Some(key) = old_url.as_deref().and_then(space_image_object_key) {
591617 if let Err(error) = self.storage.as_ref().delete(&key).await {
@@ -686,6 +712,7 @@
686712 .await
687713 }
688714 };
689 self.presign_space_image_url(space.id, kind, &locator).await
715 self.presign_space_image_url(space.id, kind, variant, &locator)
716 .await
690717 }
691718}
Modified lib/service/user/src/handlers/get_user_avatar.rs lib/service/user/src/handlers/get_user_avatar.rs +1 −1 View lib/service/user/src/handlers/get_user_avatar.rs
@@ -21,7 +21,7 @@
2121 get,
2222 path = "/users/{user_id}/avatar",
2323 tag = "users",
24 description = "重定向到 S3 预签名头像链接(7 天有效, Redis 缓存)。默认返回压缩后的预览图,`?variant=original` 返回上传原图。头像不受资料可见性限制:聊天与成员列表依赖它辨认发言人。",
24 description = "重定向到 S3 预签名头像链接(24 小时有效, Redis 缓存)。默认返回压缩后的预览图,`?variant=original` 返回上传原图。头像不受资料可见性限制:聊天与成员列表依赖它辨认发言人。",
2525 params(("user_id" = i64, Path, description = "user id"), AvatarVariantQuery),
2626 responses(
2727 (status = 302, description = "redirect to presigned s3 avatar url"),
Modified lib/service/user/src/services/avatar.rs lib/service/user/src/services/avatar.rs +7 −3 View lib/service/user/src/services/avatar.rs
@@ -14,8 +14,11 @@
1414
1515pub const MAX_AVATAR_BYTES: usize = 5 * 1024 * 1024;
1616pub const AVATAR_PREFIX: &str = "avatars";
17pub const AVATAR_URL_TTL_SECS: u64 = 7 * 24 * 60 * 60;
18pub const AVATAR_URL_CACHE_PREFIX: &str = "user:avatar:url";
17pub const AVATAR_URL_TTL_SECS: u64 = 24 * 60 * 60;
18/// Strictly shorter than the signature lifetime, so a cache hit can never hand
19/// out a URL that is about to expire.
20pub const AVATAR_URL_CACHE_TTL_SECS: u64 = AVATAR_URL_TTL_SECS / 5 * 4;
21pub const AVATAR_URL_CACHE_PREFIX: &str = "user:avatar:url:v2";
1922
2023fn avatar_url_cache_key(uid: i64) -> String {
2124 format!("{AVATAR_URL_CACHE_PREFIX}:{uid}")
@@ -150,7 +153,8 @@
150153 .presign_get(&key, AVATAR_URL_TTL_SECS as u32)
151154 .map_err(UserError::from)?;
152155 if let Err(error) =
153 cache::client::set_string(&self.redis, &cache_key, &url, AVATAR_URL_TTL_SECS).await
156 cache::client::set_string(&self.redis, &cache_key, &url, AVATAR_URL_CACHE_TTL_SECS)
157 .await
154158 {
155159 tracing::warn!(%error, "failed to cache avatar presigned url");
156160 }
Modified openapi.json openapi.json View openapi.json
Diff too large to display.