perf: round 12 — auth write-path narrowing, fused quota gate, moka blob-cache index, media single-read, sized listing JSON
Benchmark-gated round (benches/ROUND12.md; every change ships with a BEFORE/AFTER harness + equivalence gates, one candidate rejected by its own bench): DB / query shapes (bench_round12_queries): - NC sharee search: username-only projection instead of the 21-column row (incl. the <=512 KiB avatar) per match, + gin_trgm_ops indexes on auth.users for the leading-wildcard ILIKE (4.98x; 54.7x with index). - Password login: delete the redundant full-row update_user — create_session already stamps last_login_at in its own txn (4.45x per login). - Email-verified stamp: narrow conditional UPDATE (8.9x); OIDC repeat login now compares profile state in memory and issues ZERO queries when nothing changed (was: full 17-column rewrite per login). - Refresh rotation: revoke+insert+stamp fused into one transaction via new rotate_session port method (1.18x). - WOPI CheckFileInfo / authorize_wopi_access: require(Read) + get_file + check(Update) overlapped with tokio::join!, original result precedence (cold 1.34x). - Upload quota gate: user-envelope + drive-cap checks fused into ONE round-trip (check_upload_quotas) — the NC chunked PUT pays this per chunk (1.81x, 2 -> 1 queries/chunk); shared verdict evaluators keep error shapes byte-identical. CPU / allocs (bench_round12_micro): - sized_json: pre-sized listing serialization replacing axum Json's 128 B seed + doubling-realloc chain on files/folder-resources/photos/search responses (1.40x, 13 -> 2 allocs per 500-row page; byte-identical). - Security headers: 4 SetResponseHeaderLayer folded into the CSP middleware pass (5 layers -> 1; 1.43x per request, -26 allocs; header set gated byte-identical incl. 304s). - Media capture-metadata: single-read extraction — nom-exif now parses the buffer kamadak already read (zero-copy Bytes) and videos open once with a kind() dispatch; per-image opens 2-3 -> 1 (1.44x warm geomean, 1.6-3.2x cold cache; extraction outputs gated identical incl. the MIME-mislabel track fallback). - Chunked-upload session ops: owner gate folded into the operation's own DashMap lookup + stack-encoded uuid compare (5 -> 3 lookups, -2 allocs, 1.28x per chunk). Blob cache (bench_blob_cache_index + round-3 regression guard): - CachedBlobBackend index: tokio::sync::Mutex<LruCache> -> moka::sync::Cache with byte weigher. The mutex serialized every cached chunk read and scaled NEGATIVELY (2.08 -> 1.07 Mops/s from 1 -> 2 readers); moka probes are lock-free (2.17x at K=2). Byte budget now enforced by moka (manual current_size + collect_evictions machinery deleted); eviction listener unlinks size-evicted files only (Replaced entries keep their file — gated). Single-flight miss gate unchanged (16 concurrent misses -> 1 fetch re-verified via the round-3 harness). - put_blob now populates the cache BEFORE the inner backend consumes the source file (the old order failed 100% of the time — local renames, S3/Azure delete the source — so the first read after a whole-file put re-downloaded from the remote); inner-put failure invalidates the entry. Frontend (vitest gates): - List-view thumbnails request the 150px icon rendition instead of 400px preview into a 40px slot (~7.1x fewer pixels, ~4-5x fewer bytes per thumbnail across list views); grid keeps preview. Rejected by its own bench (kept as evidence in bench_round12_micro §2): - Single-pass compression predicate: the monomorphized And-chain already costs ~4.6 ns / 0 allocs total; the fused node measured within noise. New migration: 20260719000000_users_search_trgm.sql (trgm indexes). Deferred with prepared design: grouped file/grid view virtualization (single-VirtualRows flatten, the photos pattern) — next round's headline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BfidAJD5AHw23jtvBUNamB
This commit is contained in:
@@ -327,6 +327,77 @@ impl SessionStoragePort for SessionPgRepository {
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
/// Revoke + insert + last-login stamp in ONE transaction — the refresh
|
||||
/// rotation used to pay two full BEGIN/COMMIT round-trip pairs
|
||||
/// (`revoke_session` then `create_session`) per token refresh.
|
||||
async fn rotate_session(
|
||||
&self,
|
||||
old_session_id: Uuid,
|
||||
new_session: Session,
|
||||
) -> Result<Session, DomainError> {
|
||||
let session_clone = new_session.clone();
|
||||
with_transaction(&self.pool, "rotate_session", |tx| {
|
||||
Box::pin(async move {
|
||||
sqlx::query("UPDATE auth.sessions SET revoked = true WHERE id = $1")
|
||||
.bind(old_session_id)
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO auth.sessions (
|
||||
id, user_id, refresh_token, expires_at,
|
||||
ip_address, user_agent, created_at, revoked, family_id
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(session_clone.id())
|
||||
.bind(session_clone.user_id())
|
||||
.bind(session_clone.refresh_token())
|
||||
.bind(session_clone.expires_at())
|
||||
.bind(session_clone.ip_address())
|
||||
.bind(session_clone.user_agent())
|
||||
.bind(session_clone.created_at())
|
||||
.bind(session_clone.is_revoked())
|
||||
.bind(session_clone.family_id())
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE auth.users
|
||||
SET last_login_at = NOW(), updated_at = NOW()
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(session_clone.user_id())
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::warn!(
|
||||
"Could not update last_login_at for user {}: {}",
|
||||
session_clone.user_id(),
|
||||
e
|
||||
);
|
||||
SessionRepositoryError::DatabaseError(format!(
|
||||
"Session rotated but could not update last_login_at: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(session_clone)
|
||||
}) as BoxFuture<'_, SessionRepositoryResult<Session>>
|
||||
})
|
||||
.await
|
||||
.map_err(DomainError::from)?;
|
||||
|
||||
Ok(new_session)
|
||||
}
|
||||
|
||||
async fn get_session_by_refresh_token(
|
||||
&self,
|
||||
refresh_token: &str,
|
||||
|
||||
@@ -1067,6 +1067,81 @@ impl UserStoragePort for UserPgRepository {
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn search_usernames(
|
||||
&self,
|
||||
query: &str,
|
||||
limit: i64,
|
||||
include_external: bool,
|
||||
) -> Result<Vec<Option<String>>, DomainError> {
|
||||
// Same predicate / order / limit as `search_users`, username-only
|
||||
// projection — the sharee autocomplete path reads nothing else, and
|
||||
// the wide row drags the avatar `image` per matched user.
|
||||
let pattern = format!("%{}%", query);
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT username
|
||||
FROM auth.users
|
||||
WHERE (username ILIKE $1 OR email ILIKE $1)
|
||||
AND ($3 OR is_external = FALSE)
|
||||
ORDER BY username
|
||||
LIMIT $2
|
||||
"#,
|
||||
)
|
||||
.bind(&pattern)
|
||||
.bind(limit)
|
||||
.bind(include_external)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)
|
||||
.map_err(DomainError::from)?;
|
||||
Ok(rows.into_iter().map(|row| row.get("username")).collect())
|
||||
}
|
||||
|
||||
async fn mark_email_verified(&self, user_id: Uuid) -> Result<(), DomainError> {
|
||||
// SQL twin of `User::mark_email_verified` — stamps once, keeps the
|
||||
// first timestamp, and touches only the two columns involved.
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE auth.users
|
||||
SET email_verified_at = NOW(), updated_at = NOW()
|
||||
WHERE id = $1 AND email_verified_at IS NULL
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)
|
||||
.map_err(DomainError::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn sync_oidc_login_profile(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
image: Option<&str>,
|
||||
) -> Result<(), DomainError> {
|
||||
// `IS DISTINCT FROM` guard (the `update_storage_usage` pattern): the
|
||||
// common repeat-login case — same IdP avatar, already verified —
|
||||
// writes nothing at all (no dead tuple, no WAL).
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE auth.users
|
||||
SET image = $2,
|
||||
email_verified_at = COALESCE(email_verified_at, NOW()),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
AND (image IS DISTINCT FROM $2 OR email_verified_at IS NULL)
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(image)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)
|
||||
.map_err(DomainError::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_users_by_role(&self, role: &str) -> Result<Vec<User>, DomainError> {
|
||||
UserRepository::list_users_by_role(self, role)
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user