1101 lines
40 KiB
Rust
1101 lines
40 KiB
Rust
//! Round-12 query-shape pack (needs the dev Postgres up; reads DATABASE_URL
|
|
//! from `.env`).
|
|
//!
|
|
//! Six sections, each BEFORE (verbatim replica of the shipped query shape)
|
|
//! vs AFTER (proposed shape), with equivalence/safety gates:
|
|
//!
|
|
//! [1] NC sharee search — the wide 21-column `search_users` row (incl. the
|
|
//! up-to-512 KiB avatar `image`) fetched per match when the handler
|
|
//! only reads `username`, vs a narrow username-only SELECT; plus a
|
|
//! `gin_trgm_ops` index arm for the leading-wildcard ILIKE.
|
|
//! [2] Password login — the redundant full-row `update_user` (17 columns
|
|
//! incl. `image`) that `create_session`'s own `last_login_at` UPDATE
|
|
//! immediately overwrites, vs create_session alone.
|
|
//! [3] Email-verified stamp (magic-link / OIDC JIT) — full-row
|
|
//! `update_user` to set one timestamp vs a narrow conditional UPDATE.
|
|
//! [4] Refresh-token rotation — revoke txn + create txn (2 transactions,
|
|
//! 6 statements) vs one fused rotation transaction.
|
|
//! [5] WOPI CheckFileInfo — require(Read) → get_file → check(Update)
|
|
//! serial vs `tokio::join!` (real `PgAclEngine` + real file read repo;
|
|
//! cold and warm arms).
|
|
//! [6] Upload quota pair — user-envelope + drive-cap checks as two serial
|
|
//! point reads vs one fused SELECT (verdict precedence preserved).
|
|
//!
|
|
//! Run:
|
|
//! cargo run --release --features bench --example bench_round12_queries
|
|
//! Tunables (env): BENCH_PASSES (200), BENCH_SHR_USERS (3000),
|
|
//! BENCH_WOPI_FILES (100), BENCH_WARM_ITERS (2000)
|
|
|
|
use std::env;
|
|
use std::sync::Arc;
|
|
use std::time::Instant;
|
|
|
|
use chrono::Utc;
|
|
use oxicloud::application::ports::authorization_ports::AuthorizationEngine;
|
|
use oxicloud::application::ports::storage_ports::FileReadPort;
|
|
use oxicloud::domain::services::authorization::{Permission, Resource, Subject};
|
|
use oxicloud::infrastructure::repositories::pg::{
|
|
FileBlobReadRepository, FolderDbRepository, SubjectGroupPgRepository,
|
|
};
|
|
use oxicloud::infrastructure::services::dedup_service::DedupService;
|
|
use oxicloud::infrastructure::services::local_blob_backend::LocalBlobBackend;
|
|
use oxicloud::infrastructure::services::pg_acl_engine::PgAclEngine;
|
|
use sqlx::postgres::PgPoolOptions;
|
|
use sqlx::{PgPool, Row};
|
|
use uuid::Uuid;
|
|
|
|
fn env_or<T: std::str::FromStr>(key: &str, default: T) -> T {
|
|
env::var(key)
|
|
.ok()
|
|
.and_then(|v| v.parse().ok())
|
|
.unwrap_or(default)
|
|
}
|
|
|
|
fn stats(mut samples: Vec<f64>) -> (f64, f64, f64) {
|
|
samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
|
let n = samples.len();
|
|
let mean = samples.iter().sum::<f64>() / n as f64;
|
|
let p50 = samples[n / 2];
|
|
let p95 = samples[((n as f64 * 0.95) as usize).min(n - 1)];
|
|
(mean, p50, p95)
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// [1] NC sharee search — wide row vs narrow username-only (+ trgm arm)
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// BEFORE, verbatim `UserPgRepository::search_users` SELECT list.
|
|
async fn sharee_before(pool: &PgPool, pattern: &str, limit: i64) -> Vec<Option<String>> {
|
|
let rows = sqlx::query(
|
|
r#"
|
|
SELECT
|
|
id, username, email, password_hash, role::text as role_text,
|
|
storage_quota_bytes, storage_used_bytes,
|
|
created_at, updated_at, last_login_at, active,
|
|
oidc_provider, oidc_subject, image, is_external,
|
|
given_name, family_name, email_verified_at, preferred_locale, notify_on_share,
|
|
ui_preferences
|
|
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(false)
|
|
.fetch_all(pool)
|
|
.await
|
|
.expect("sharee wide");
|
|
rows.into_iter()
|
|
.map(|r| {
|
|
// The handler materializes the whole row (incl. `image`) into a
|
|
// `User`/`UserDto` and then keeps only the username. Touch the
|
|
// wide columns like the entity build does.
|
|
let _image: Option<String> = r.get("image");
|
|
let _email: Option<String> = r.get("email");
|
|
r.get("username")
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// AFTER: same WHERE / ORDER / LIMIT, username-only projection.
|
|
async fn sharee_after(pool: &PgPool, pattern: &str, limit: i64) -> Vec<Option<String>> {
|
|
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(false)
|
|
.fetch_all(pool)
|
|
.await
|
|
.expect("sharee narrow");
|
|
rows.into_iter().map(|r| r.get("username")).collect()
|
|
}
|
|
|
|
async fn section_sharee(pool: &PgPool) {
|
|
let n_users: i64 = env_or("BENCH_SHR_USERS", 3000);
|
|
let passes: usize = env_or("BENCH_PASSES", 200);
|
|
let avatared = 600.min(n_users);
|
|
|
|
// Seed server-side (no avatar bytes on the wire): first `avatared` users
|
|
// carry a ~256 KiB data-URI image, the rest none.
|
|
sqlx::query(
|
|
r#"
|
|
INSERT INTO auth.users (username, email, role, image)
|
|
SELECT
|
|
'shr_user_' || lpad(i::text, 5, '0'),
|
|
'shr' || i || '@bench.invalid',
|
|
'user',
|
|
CASE WHEN i < $2 THEN 'data:image/png;base64,' || repeat('QUJDRA==', 32768) END
|
|
FROM generate_series(0, $1 - 1) AS g(i)
|
|
"#,
|
|
)
|
|
.bind(n_users)
|
|
.bind(avatared)
|
|
.execute(pool)
|
|
.await
|
|
.expect("seed sharee users");
|
|
|
|
// Typing "shr_user_0" — 26-row NC sharee page, all matches avatar-carrying.
|
|
let pattern = "%shr_user_0%";
|
|
let limit = 26i64;
|
|
|
|
// Equivalence gate: identical username lists.
|
|
let b = sharee_before(pool, pattern, limit).await;
|
|
let a = sharee_after(pool, pattern, limit).await;
|
|
assert_eq!(b, a, "sharee result lists differ");
|
|
assert_eq!(b.len(), limit as usize, "expected a full page");
|
|
println!("# [1] gate: wide and narrow username lists identical — OK");
|
|
|
|
let mut wide = Vec::with_capacity(passes);
|
|
for _ in 0..passes {
|
|
let t = Instant::now();
|
|
std::hint::black_box(sharee_before(pool, pattern, limit).await);
|
|
wide.push(t.elapsed().as_secs_f64() * 1e3);
|
|
}
|
|
let mut narrow = Vec::with_capacity(passes);
|
|
for _ in 0..passes {
|
|
let t = Instant::now();
|
|
std::hint::black_box(sharee_after(pool, pattern, limit).await);
|
|
narrow.push(t.elapsed().as_secs_f64() * 1e3);
|
|
}
|
|
|
|
// trgm arm: the production migration candidate.
|
|
sqlx::query(
|
|
"CREATE INDEX IF NOT EXISTS bench_users_username_trgm
|
|
ON auth.users USING gin (username gin_trgm_ops)",
|
|
)
|
|
.execute(pool)
|
|
.await
|
|
.expect("trgm username");
|
|
sqlx::query(
|
|
"CREATE INDEX IF NOT EXISTS bench_users_email_trgm
|
|
ON auth.users USING gin (email gin_trgm_ops)",
|
|
)
|
|
.execute(pool)
|
|
.await
|
|
.expect("trgm email");
|
|
sqlx::query("ANALYZE auth.users")
|
|
.execute(pool)
|
|
.await
|
|
.expect("analyze");
|
|
let a_idx = sharee_after(pool, pattern, limit).await;
|
|
assert_eq!(b, a_idx, "trgm-indexed narrow list differs");
|
|
let mut narrow_idx = Vec::with_capacity(passes);
|
|
for _ in 0..passes {
|
|
let t = Instant::now();
|
|
std::hint::black_box(sharee_after(pool, pattern, limit).await);
|
|
narrow_idx.push(t.elapsed().as_secs_f64() * 1e3);
|
|
}
|
|
|
|
let (wm, wp50, wp95) = stats(wide);
|
|
let (nm, np50, np95) = stats(narrow);
|
|
let (im, ip50, ip95) = stats(narrow_idx);
|
|
println!("\n## [1] NC sharee search ({n_users} users, 26-row page, all matches avatared)");
|
|
println!("| arm | mean ms | p50 ms | p95 ms |");
|
|
println!("| BEFORE wide row (incl. image) | {wm:>8.3} | {wp50:>7.3} | {wp95:>7.3} |");
|
|
println!("| AFTER narrow username | {nm:>8.3} | {np50:>7.3} | {np95:>7.3} |");
|
|
println!("| AFTER narrow + trgm index | {im:>8.3} | {ip50:>7.3} | {ip95:>7.3} |");
|
|
println!("# narrow speedup {:.2}x; +trgm {:.2}x", wm / nm, wm / im);
|
|
|
|
// Cleanup (drop bench indexes; production ones ship via migration only
|
|
// if the arm wins).
|
|
sqlx::query("DROP INDEX IF EXISTS auth.bench_users_username_trgm")
|
|
.execute(pool)
|
|
.await
|
|
.ok();
|
|
sqlx::query("DROP INDEX IF EXISTS auth.bench_users_email_trgm")
|
|
.execute(pool)
|
|
.await
|
|
.ok();
|
|
sqlx::query("DELETE FROM auth.users WHERE username LIKE 'shr\\_user\\_%'")
|
|
.execute(pool)
|
|
.await
|
|
.expect("cleanup sharee users");
|
|
|
|
if nm >= wm {
|
|
eprintln!("GATE FAIL [1]: narrow arm not faster — rollback");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// [2] Login stamp — redundant full-row update_user + create_session
|
|
// vs create_session alone. [3] email-verified narrow stamp.
|
|
// [4] rotation fused txn. Shared user fixture with a 256 KiB avatar.
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
struct AuthFixture {
|
|
user_id: Uuid,
|
|
image: String,
|
|
}
|
|
|
|
async fn seed_auth_user(pool: &PgPool, tag: &str) -> AuthFixture {
|
|
let image = format!("data:image/png;base64,{}", "QUJDRA==".repeat(32 * 1024));
|
|
let user_id: Uuid = sqlx::query_scalar(
|
|
"INSERT INTO auth.users (username, email, role, image, storage_quota_bytes)
|
|
VALUES ($1, $2, 'user', $3, 10737418240) RETURNING id",
|
|
)
|
|
.bind(format!("bench12_{tag}"))
|
|
.bind(format!("bench12_{tag}@bench.invalid"))
|
|
.bind(&image)
|
|
.fetch_one(pool)
|
|
.await
|
|
.expect("seed auth user");
|
|
AuthFixture { user_id, image }
|
|
}
|
|
|
|
/// Verbatim replica of `UserPgRepository::update_user`'s statement, executed
|
|
/// inside a transaction like `with_transaction` does.
|
|
async fn full_row_update_user(pool: &PgPool, f: &AuthFixture, last_login: bool) {
|
|
let now = Utc::now();
|
|
let mut tx = pool.begin().await.expect("begin");
|
|
sqlx::query(
|
|
r#"
|
|
UPDATE auth.users
|
|
SET
|
|
username = $2,
|
|
email = $3,
|
|
password_hash = $4,
|
|
role = $5::auth.userrole,
|
|
storage_quota_bytes = $6,
|
|
storage_used_bytes = $7,
|
|
updated_at = $8,
|
|
last_login_at = $9,
|
|
active = $10,
|
|
image = $11,
|
|
given_name = $12,
|
|
family_name = $13,
|
|
email_verified_at = $14,
|
|
preferred_locale = $15,
|
|
notify_on_share = $16,
|
|
is_external = $17
|
|
WHERE id = $1
|
|
"#,
|
|
)
|
|
.bind(f.user_id)
|
|
.bind("bench12_login")
|
|
.bind("bench12_login@bench.invalid")
|
|
.bind(Option::<String>::None)
|
|
.bind("user")
|
|
.bind(10737418240i64)
|
|
.bind(0i64)
|
|
.bind(now)
|
|
.bind(if last_login { Some(now) } else { None })
|
|
.bind(true)
|
|
.bind(&f.image)
|
|
.bind(Option::<String>::None)
|
|
.bind(Option::<String>::None)
|
|
.bind(if last_login { None } else { Some(now) })
|
|
.bind(Option::<String>::None)
|
|
.bind(true)
|
|
.bind(false)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.expect("full-row update");
|
|
tx.commit().await.expect("commit");
|
|
}
|
|
|
|
/// Verbatim replica of `SessionPgRepository::create_session` (insert + the
|
|
/// last_login stamp, one transaction).
|
|
async fn create_session_txn(pool: &PgPool, user_id: Uuid) -> Uuid {
|
|
let sid = Uuid::new_v4();
|
|
let mut tx = pool.begin().await.expect("begin");
|
|
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(sid)
|
|
.bind(user_id)
|
|
.bind(format!("rt-{sid}"))
|
|
.bind(Utc::now() + chrono::Duration::days(30))
|
|
.bind(Option::<String>::None)
|
|
.bind(Option::<String>::None)
|
|
.bind(Utc::now())
|
|
.bind(false)
|
|
.bind(Uuid::new_v4())
|
|
.execute(&mut *tx)
|
|
.await
|
|
.expect("insert session");
|
|
sqlx::query("UPDATE auth.users SET last_login_at = NOW(), updated_at = NOW() WHERE id = $1")
|
|
.bind(user_id)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.expect("stamp last_login");
|
|
tx.commit().await.expect("commit");
|
|
sid
|
|
}
|
|
|
|
async fn section_login_stamp(pool: &PgPool) {
|
|
let passes: usize = env_or("BENCH_PASSES", 200);
|
|
let f = seed_auth_user(pool, "login").await;
|
|
|
|
// Safety gate: the AFTER flow must leave the same observable row state
|
|
// (last_login_at set, avatar intact, everything else untouched).
|
|
full_row_update_user(pool, &f, true).await;
|
|
create_session_txn(pool, f.user_id).await;
|
|
let before_row: (Option<chrono::DateTime<Utc>>, Option<String>, bool) =
|
|
sqlx::query_as("SELECT last_login_at, image, active FROM auth.users WHERE id = $1")
|
|
.bind(f.user_id)
|
|
.fetch_one(pool)
|
|
.await
|
|
.expect("row");
|
|
sqlx::query("UPDATE auth.users SET last_login_at = NULL WHERE id = $1")
|
|
.bind(f.user_id)
|
|
.execute(pool)
|
|
.await
|
|
.unwrap();
|
|
create_session_txn(pool, f.user_id).await;
|
|
let after_row: (Option<chrono::DateTime<Utc>>, Option<String>, bool) =
|
|
sqlx::query_as("SELECT last_login_at, image, active FROM auth.users WHERE id = $1")
|
|
.bind(f.user_id)
|
|
.fetch_one(pool)
|
|
.await
|
|
.expect("row");
|
|
assert!(before_row.0.is_some() && after_row.0.is_some());
|
|
assert_eq!(before_row.1, after_row.1, "avatar must be untouched");
|
|
assert_eq!(before_row.2, after_row.2);
|
|
println!("# [2] gate: create_session alone leaves identical observable state — OK");
|
|
|
|
let mut before = Vec::with_capacity(passes);
|
|
for _ in 0..passes {
|
|
let t = Instant::now();
|
|
full_row_update_user(pool, &f, true).await;
|
|
create_session_txn(pool, f.user_id).await;
|
|
before.push(t.elapsed().as_secs_f64() * 1e3);
|
|
}
|
|
let mut after = Vec::with_capacity(passes);
|
|
for _ in 0..passes {
|
|
let t = Instant::now();
|
|
create_session_txn(pool, f.user_id).await;
|
|
after.push(t.elapsed().as_secs_f64() * 1e3);
|
|
}
|
|
let (bm, bp50, bp95) = stats(before);
|
|
let (am, ap50, ap95) = stats(after);
|
|
println!("\n## [2] Password-login stamp (user with 256 KiB avatar)");
|
|
println!("| arm | mean ms | p50 ms | p95 ms |");
|
|
println!("| BEFORE update_user + create_session | {bm:>7.3} | {bp50:>7.3} | {bp95:>7.3} |");
|
|
println!("| AFTER create_session only | {am:>7.3} | {ap50:>7.3} | {ap95:>7.3} |");
|
|
println!(
|
|
"# {:.2}x faster per login; 1 txn + full-row write (incl. avatar) removed",
|
|
bm / am
|
|
);
|
|
|
|
sqlx::query("DELETE FROM auth.sessions WHERE user_id = $1")
|
|
.bind(f.user_id)
|
|
.execute(pool)
|
|
.await
|
|
.ok();
|
|
sqlx::query("DELETE FROM auth.users WHERE id = $1")
|
|
.bind(f.user_id)
|
|
.execute(pool)
|
|
.await
|
|
.ok();
|
|
if am >= bm {
|
|
eprintln!("GATE FAIL [2]: AFTER not faster — rollback");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
async fn section_email_stamp(pool: &PgPool) {
|
|
let passes: usize = env_or("BENCH_PASSES", 200);
|
|
let f = seed_auth_user(pool, "email").await;
|
|
|
|
// AFTER: the narrow conditional stamp (idempotent in SQL, mirroring the
|
|
// entity guard `if email_verified_at.is_none()`).
|
|
async fn narrow_stamp(pool: &PgPool, id: Uuid) -> u64 {
|
|
sqlx::query(
|
|
"UPDATE auth.users
|
|
SET email_verified_at = NOW(), updated_at = NOW()
|
|
WHERE id = $1 AND email_verified_at IS NULL",
|
|
)
|
|
.bind(id)
|
|
.execute(pool)
|
|
.await
|
|
.expect("narrow stamp")
|
|
.rows_affected()
|
|
}
|
|
|
|
// Gates: first call stamps; second call is a no-op (idempotent); value
|
|
// survives; avatar untouched.
|
|
assert_eq!(narrow_stamp(pool, f.user_id).await, 1);
|
|
let first: Option<chrono::DateTime<Utc>> =
|
|
sqlx::query_scalar("SELECT email_verified_at FROM auth.users WHERE id = $1")
|
|
.bind(f.user_id)
|
|
.fetch_one(pool)
|
|
.await
|
|
.unwrap();
|
|
assert!(first.is_some());
|
|
assert_eq!(
|
|
narrow_stamp(pool, f.user_id).await,
|
|
0,
|
|
"second stamp must be a no-op"
|
|
);
|
|
let second: Option<chrono::DateTime<Utc>> =
|
|
sqlx::query_scalar("SELECT email_verified_at FROM auth.users WHERE id = $1")
|
|
.bind(f.user_id)
|
|
.fetch_one(pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(first, second, "timestamp must not move on re-stamp");
|
|
println!("# [3] gate: narrow stamp idempotent, value stable — OK");
|
|
|
|
let mut before = Vec::with_capacity(passes);
|
|
for _ in 0..passes {
|
|
let t = Instant::now();
|
|
full_row_update_user(pool, &f, false).await;
|
|
before.push(t.elapsed().as_secs_f64() * 1e3);
|
|
}
|
|
// Reset so the narrow arm measures the write path (not the no-op path).
|
|
let mut after = Vec::with_capacity(passes);
|
|
for _ in 0..passes {
|
|
sqlx::query("UPDATE auth.users SET email_verified_at = NULL WHERE id = $1")
|
|
.bind(f.user_id)
|
|
.execute(pool)
|
|
.await
|
|
.unwrap();
|
|
let t = Instant::now();
|
|
narrow_stamp(pool, f.user_id).await;
|
|
after.push(t.elapsed().as_secs_f64() * 1e3);
|
|
}
|
|
let (bm, bp50, bp95) = stats(before);
|
|
let (am, ap50, ap95) = stats(after);
|
|
|
|
// [3b] OIDC repeat-login profile sync — the guarded narrow UPDATE in
|
|
// its no-op case (same avatar, already verified). This arm is the
|
|
// evidence for why production ALSO short-circuits app-side: even a
|
|
// 0-row guarded UPDATE ships the ≤512 KiB avatar parameter over the
|
|
// wire just to compare it server-side, so the shipped shape compares
|
|
// against the already-fetched row in memory and issues NO query on
|
|
// the repeat-login common case (the guarded UPDATE remains as the
|
|
// write path when something actually changed, and as a belt-and-
|
|
// braces guard).
|
|
sqlx::query("UPDATE auth.users SET email_verified_at = NOW(), image = $2 WHERE id = $1")
|
|
.bind(f.user_id)
|
|
.bind(&f.image)
|
|
.execute(pool)
|
|
.await
|
|
.unwrap();
|
|
let mut sync_noop = Vec::with_capacity(passes);
|
|
for _ in 0..passes {
|
|
let t = Instant::now();
|
|
let res = sqlx::query(
|
|
"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(f.user_id)
|
|
.bind(&f.image)
|
|
.execute(pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(res.rows_affected(), 0, "no-op path must not write");
|
|
sync_noop.push(t.elapsed().as_secs_f64() * 1e3);
|
|
}
|
|
let (sm, sp50, sp95) = stats(sync_noop);
|
|
|
|
println!("\n## [3] Email-verified stamp (user with 256 KiB avatar)");
|
|
println!("| arm | mean ms | p50 ms | p95 ms |");
|
|
println!("| BEFORE full-row update_user | {bm:>7.3} | {bp50:>7.3} | {bp95:>7.3} |");
|
|
println!("| AFTER narrow conditional | {am:>7.3} | {ap50:>7.3} | {ap95:>7.3} |");
|
|
println!("| AFTER oidc guarded no-op | {sm:>7.3} | {sp50:>7.3} | {sp95:>7.3} |");
|
|
println!(
|
|
"# {:.2}x faster per stamp; guarded no-op still ships the avatar param \
|
|
({:.2}x) — hence the app-side skip (0 queries) shipped in production",
|
|
bm / am,
|
|
bm / sm
|
|
);
|
|
|
|
sqlx::query("DELETE FROM auth.users WHERE id = $1")
|
|
.bind(f.user_id)
|
|
.execute(pool)
|
|
.await
|
|
.ok();
|
|
if am >= bm {
|
|
eprintln!("GATE FAIL [3]: AFTER not faster — rollback");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
async fn section_rotation(pool: &PgPool) {
|
|
let passes: usize = env_or("BENCH_PASSES", 200);
|
|
let f = seed_auth_user(pool, "rot").await;
|
|
|
|
// BEFORE: revoke txn (verbatim) + create txn (verbatim).
|
|
async fn before_rotate(pool: &PgPool, user_id: Uuid, old: Uuid) -> Uuid {
|
|
let mut tx = pool.begin().await.expect("begin");
|
|
let _row =
|
|
sqlx::query("UPDATE auth.sessions SET revoked = true WHERE id = $1 RETURNING user_id")
|
|
.bind(old)
|
|
.fetch_optional(&mut *tx)
|
|
.await
|
|
.expect("revoke");
|
|
tx.commit().await.expect("commit");
|
|
create_session_txn(pool, user_id).await
|
|
}
|
|
|
|
// AFTER: one fused transaction (same three statements, one txn).
|
|
async fn after_rotate(pool: &PgPool, user_id: Uuid, old: Uuid) -> Uuid {
|
|
let sid = Uuid::new_v4();
|
|
let mut tx = pool.begin().await.expect("begin");
|
|
sqlx::query("UPDATE auth.sessions SET revoked = true WHERE id = $1 RETURNING user_id")
|
|
.bind(old)
|
|
.fetch_optional(&mut *tx)
|
|
.await
|
|
.expect("revoke");
|
|
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(sid)
|
|
.bind(user_id)
|
|
.bind(format!("rt-{sid}"))
|
|
.bind(Utc::now() + chrono::Duration::days(30))
|
|
.bind(Option::<String>::None)
|
|
.bind(Option::<String>::None)
|
|
.bind(Utc::now())
|
|
.bind(false)
|
|
.bind(Uuid::new_v4())
|
|
.execute(&mut *tx)
|
|
.await
|
|
.expect("insert");
|
|
sqlx::query(
|
|
"UPDATE auth.users SET last_login_at = NOW(), updated_at = NOW() WHERE id = $1",
|
|
)
|
|
.bind(user_id)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.expect("stamp");
|
|
tx.commit().await.expect("commit");
|
|
sid
|
|
}
|
|
|
|
// Gate: both arms leave old session revoked + new session live.
|
|
let s0 = create_session_txn(pool, f.user_id).await;
|
|
let s1 = before_rotate(pool, f.user_id, s0).await;
|
|
let s2 = after_rotate(pool, f.user_id, s1).await;
|
|
let states: Vec<(Uuid, bool)> =
|
|
sqlx::query_as("SELECT id, revoked FROM auth.sessions WHERE user_id = $1")
|
|
.bind(f.user_id)
|
|
.fetch_all(pool)
|
|
.await
|
|
.unwrap();
|
|
let get = |id: Uuid| states.iter().find(|(s, _)| *s == id).map(|(_, r)| *r);
|
|
assert_eq!(get(s0), Some(true), "s0 revoked");
|
|
assert_eq!(get(s1), Some(true), "s1 revoked by after_rotate");
|
|
assert_eq!(get(s2), Some(false), "s2 live");
|
|
println!("# [4] gate: fused rotation leaves identical session states — OK");
|
|
|
|
let mut cur = s2;
|
|
let mut before = Vec::with_capacity(passes);
|
|
for _ in 0..passes {
|
|
let t = Instant::now();
|
|
cur = before_rotate(pool, f.user_id, cur).await;
|
|
before.push(t.elapsed().as_secs_f64() * 1e3);
|
|
}
|
|
let mut after = Vec::with_capacity(passes);
|
|
for _ in 0..passes {
|
|
let t = Instant::now();
|
|
cur = after_rotate(pool, f.user_id, cur).await;
|
|
after.push(t.elapsed().as_secs_f64() * 1e3);
|
|
}
|
|
let (bm, bp50, bp95) = stats(before);
|
|
let (am, ap50, ap95) = stats(after);
|
|
println!("\n## [4] Refresh-token rotation");
|
|
println!("| arm | mean ms | p50 ms | p95 ms |");
|
|
println!("| BEFORE 2 transactions | {bm:>7.3} | {bp50:>7.3} | {bp95:>7.3} |");
|
|
println!("| AFTER 1 transaction | {am:>7.3} | {ap50:>7.3} | {ap95:>7.3} |");
|
|
println!("# {:.2}x faster per rotation", bm / am);
|
|
|
|
sqlx::query("DELETE FROM auth.sessions WHERE user_id = $1")
|
|
.bind(f.user_id)
|
|
.execute(pool)
|
|
.await
|
|
.ok();
|
|
sqlx::query("DELETE FROM auth.users WHERE id = $1")
|
|
.bind(f.user_id)
|
|
.execute(pool)
|
|
.await
|
|
.ok();
|
|
if am >= bm {
|
|
eprintln!("GATE FAIL [4]: AFTER not faster — rollback");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// [5] WOPI CheckFileInfo triple — serial vs join! (real engine + file repo)
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
struct WopiSeed {
|
|
caller: Uuid,
|
|
drive_id: Uuid,
|
|
root_folder: Uuid,
|
|
blob_hash: String,
|
|
file_ids: Vec<Uuid>,
|
|
}
|
|
|
|
async fn wopi_seed(pool: &PgPool, n_files: usize) -> WopiSeed {
|
|
let mut tx = pool.begin().await.expect("begin");
|
|
let caller: Uuid = sqlx::query_scalar(
|
|
"INSERT INTO auth.users (username, email, role)
|
|
VALUES ('bench12_wopi', 'bench12_wopi@bench.invalid', 'user') RETURNING id",
|
|
)
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.expect("seed caller");
|
|
let drive_id: Uuid =
|
|
sqlx::query_scalar("INSERT INTO storage.drives (kind) VALUES ('shared') RETURNING id")
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.expect("seed drive");
|
|
let root_folder: Uuid = sqlx::query_scalar(
|
|
"INSERT INTO storage.folders (name, path, lpath, drive_id)
|
|
VALUES ('Bench12 WOPI', '/Bench12 WOPI', 'x', $1) RETURNING id",
|
|
)
|
|
.bind(drive_id)
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.expect("seed folder");
|
|
sqlx::query("UPDATE storage.drives SET root_folder_id = $1 WHERE id = $2")
|
|
.bind(root_folder)
|
|
.bind(drive_id)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.expect("stamp root");
|
|
sqlx::query(
|
|
"INSERT INTO storage.role_grants
|
|
(subject_type, subject_id, resource_type, resource_id, role, granted_by)
|
|
VALUES ('user', $1, 'drive', $2, 'editor'::storage.grant_role, $1)",
|
|
)
|
|
.bind(caller)
|
|
.bind(drive_id)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.expect("seed grant");
|
|
let blob_hash = "bench12wopi00000000000000000000000000000000000000000000000000b1".to_string();
|
|
sqlx::query("INSERT INTO storage.blobs (hash, size, ref_count) VALUES ($1, 1, 1)")
|
|
.bind(&blob_hash)
|
|
.execute(&mut *tx)
|
|
.await
|
|
.expect("seed blob");
|
|
let mut file_ids = Vec::with_capacity(n_files);
|
|
for i in 0..n_files {
|
|
let id: Uuid = sqlx::query_scalar(
|
|
"INSERT INTO storage.files (name, folder_id, blob_hash, size, mime_type, drive_id)
|
|
VALUES ($1, $2, $3, 1, 'application/vnd.oasis.opendocument.text', $4) RETURNING id",
|
|
)
|
|
.bind(format!("bench12-{i:04}.odt"))
|
|
.bind(root_folder)
|
|
.bind(&blob_hash)
|
|
.bind(drive_id)
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.expect("seed file");
|
|
file_ids.push(id);
|
|
}
|
|
tx.commit().await.expect("commit");
|
|
WopiSeed {
|
|
caller,
|
|
drive_id,
|
|
root_folder,
|
|
blob_hash,
|
|
file_ids,
|
|
}
|
|
}
|
|
|
|
async fn wopi_cleanup(pool: &PgPool, s: &WopiSeed) {
|
|
let _ = sqlx::query("DELETE FROM storage.role_grants WHERE resource_id = $1")
|
|
.bind(s.drive_id)
|
|
.execute(pool)
|
|
.await;
|
|
let _ = sqlx::query("DELETE FROM storage.files WHERE drive_id = $1")
|
|
.bind(s.drive_id)
|
|
.execute(pool)
|
|
.await;
|
|
let _ = sqlx::query("DELETE FROM storage.drives WHERE id = $1")
|
|
.bind(s.drive_id)
|
|
.execute(pool)
|
|
.await;
|
|
let _ = sqlx::query("DELETE FROM storage.folders WHERE id = $1")
|
|
.bind(s.root_folder)
|
|
.execute(pool)
|
|
.await;
|
|
let _ = sqlx::query("DELETE FROM storage.blobs WHERE hash = $1")
|
|
.bind(&s.blob_hash)
|
|
.execute(pool)
|
|
.await;
|
|
let _ = sqlx::query("DELETE FROM auth.users WHERE id = $1")
|
|
.bind(s.caller)
|
|
.execute(pool)
|
|
.await;
|
|
}
|
|
|
|
fn wopi_engine(pool: &Arc<PgPool>) -> (Arc<PgAclEngine>, Arc<FileBlobReadRepository>) {
|
|
let folder_repo = Arc::new(FolderDbRepository::new(pool.clone()));
|
|
let backend = Arc::new(LocalBlobBackend::new(std::path::Path::new(
|
|
"/tmp/bench12-wopi-blobs",
|
|
)));
|
|
let dedup = Arc::new(DedupService::new(backend, pool.clone(), pool.clone()));
|
|
let file_repo = Arc::new(FileBlobReadRepository::new(
|
|
pool.clone(),
|
|
dedup,
|
|
folder_repo.clone(),
|
|
));
|
|
let group_repo = Arc::new(SubjectGroupPgRepository::new(pool.clone()));
|
|
(
|
|
Arc::new(PgAclEngine::new(
|
|
pool.clone(),
|
|
folder_repo,
|
|
file_repo.clone(),
|
|
group_repo,
|
|
Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
|
)),
|
|
file_repo,
|
|
)
|
|
}
|
|
|
|
/// BEFORE, verbatim handler shape: require(Read) → get_file → check(Update).
|
|
async fn wopi_before(
|
|
engine: &Arc<PgAclEngine>,
|
|
files: &Arc<FileBlobReadRepository>,
|
|
caller: Uuid,
|
|
file_id: Uuid,
|
|
) -> (String, bool) {
|
|
engine
|
|
.require(
|
|
Subject::User(caller),
|
|
Permission::Read,
|
|
Resource::File(file_id),
|
|
)
|
|
.await
|
|
.expect("read");
|
|
let file = files.get_file(&file_id.to_string()).await.expect("file");
|
|
let can_write = engine
|
|
.check(
|
|
Subject::User(caller),
|
|
Permission::Update,
|
|
Resource::File(file_id),
|
|
)
|
|
.await
|
|
.unwrap_or(false);
|
|
(file.name().to_string(), can_write)
|
|
}
|
|
|
|
/// AFTER: the three independent lookups overlapped.
|
|
async fn wopi_after(
|
|
engine: &Arc<PgAclEngine>,
|
|
files: &Arc<FileBlobReadRepository>,
|
|
caller: Uuid,
|
|
file_id: Uuid,
|
|
) -> (String, bool) {
|
|
let id_str = file_id.to_string();
|
|
let (read, file, can_write) = tokio::join!(
|
|
engine.require(
|
|
Subject::User(caller),
|
|
Permission::Read,
|
|
Resource::File(file_id)
|
|
),
|
|
files.get_file(&id_str),
|
|
engine.check(
|
|
Subject::User(caller),
|
|
Permission::Update,
|
|
Resource::File(file_id)
|
|
),
|
|
);
|
|
read.expect("read");
|
|
let file = file.expect("file");
|
|
(file.name().to_string(), can_write.unwrap_or(false))
|
|
}
|
|
|
|
async fn section_wopi(pool: &Arc<PgPool>) {
|
|
let n_files: usize = env_or("BENCH_WOPI_FILES", 100);
|
|
let warm_iters: usize = env_or("BENCH_WARM_ITERS", 2000);
|
|
let seed = wopi_seed(pool, n_files).await;
|
|
|
|
// Equivalence gate (fresh engines so both arms run the same cold path).
|
|
let (e1, f1) = wopi_engine(pool);
|
|
let (e2, f2) = wopi_engine(pool);
|
|
for id in seed.file_ids.iter().take(10) {
|
|
let b = wopi_before(&e1, &f1, seed.caller, *id).await;
|
|
let a = wopi_after(&e2, &f2, seed.caller, *id).await;
|
|
assert_eq!(b, a, "wopi results differ");
|
|
}
|
|
println!("# [5] gate: serial and join! results identical (10 files) — OK");
|
|
|
|
// COLD arms: fresh engine, one triple per file (the first CheckFileInfo
|
|
// per file per TTL window).
|
|
let (ec, fc) = wopi_engine(pool);
|
|
let t = Instant::now();
|
|
for id in &seed.file_ids {
|
|
std::hint::black_box(wopi_before(&ec, &fc, seed.caller, *id).await);
|
|
}
|
|
let cold_before = t.elapsed().as_secs_f64() * 1e3 / n_files as f64;
|
|
let (ec2, fc2) = wopi_engine(pool);
|
|
let t = Instant::now();
|
|
for id in &seed.file_ids {
|
|
std::hint::black_box(wopi_after(&ec2, &fc2, seed.caller, *id).await);
|
|
}
|
|
let cold_after = t.elapsed().as_secs_f64() * 1e3 / n_files as f64;
|
|
|
|
// WARM arms: same engine, authz caches hot — get_file dominates.
|
|
let (ew, fw) = wopi_engine(pool);
|
|
for id in &seed.file_ids {
|
|
wopi_before(&ew, &fw, seed.caller, *id).await;
|
|
}
|
|
let t = Instant::now();
|
|
for i in 0..warm_iters {
|
|
let id = seed.file_ids[i % n_files];
|
|
std::hint::black_box(wopi_before(&ew, &fw, seed.caller, id).await);
|
|
}
|
|
let warm_before = t.elapsed().as_secs_f64() * 1e3 / warm_iters as f64;
|
|
let t = Instant::now();
|
|
for i in 0..warm_iters {
|
|
let id = seed.file_ids[i % n_files];
|
|
std::hint::black_box(wopi_after(&ew, &fw, seed.caller, id).await);
|
|
}
|
|
let warm_after = t.elapsed().as_secs_f64() * 1e3 / warm_iters as f64;
|
|
|
|
println!("\n## [5] WOPI CheckFileInfo triple (real PgAclEngine)");
|
|
println!("| arm | cold ms/call | warm ms/call |");
|
|
println!("| BEFORE serial | {cold_before:>9.3} | {warm_before:>9.3} |");
|
|
println!("| AFTER join! | {cold_after:>9.3} | {warm_after:>9.3} |");
|
|
println!(
|
|
"# cold {:.2}x, warm {:.2}x",
|
|
cold_before / cold_after,
|
|
warm_before / warm_after
|
|
);
|
|
|
|
wopi_cleanup(pool, &seed).await;
|
|
if cold_after >= cold_before && warm_after >= warm_before {
|
|
eprintln!("GATE FAIL [5]: join! not faster on either arm — rollback");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// [6] Upload quota pair — two serial point reads vs one fused SELECT
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
|
enum QuotaVerdict {
|
|
Ok,
|
|
UserQuotaExceeded,
|
|
DriveQuotaExceeded,
|
|
DriveNotFound,
|
|
}
|
|
|
|
/// BEFORE, verbatim: `check_storage_quota` (narrow user read) then
|
|
/// `check_drive_quota` (drive point read), serial.
|
|
async fn quota_before(
|
|
pool: &PgPool,
|
|
user_id: Uuid,
|
|
drive_id: Uuid,
|
|
additional: u64,
|
|
) -> QuotaVerdict {
|
|
let (used, quota): (i64, i64) = sqlx::query_as(
|
|
"SELECT storage_used_bytes, storage_quota_bytes FROM auth.users WHERE id = $1",
|
|
)
|
|
.bind(user_id)
|
|
.fetch_one(pool)
|
|
.await
|
|
.expect("user quota row");
|
|
if quota > 0 {
|
|
let additional_i = additional as i64;
|
|
if additional_i > quota || used + additional_i > quota {
|
|
return QuotaVerdict::UserQuotaExceeded;
|
|
}
|
|
}
|
|
let row: Option<(i64, Option<i64>)> =
|
|
sqlx::query_as("SELECT used_bytes, quota_bytes FROM storage.drives WHERE id = $1")
|
|
.bind(drive_id)
|
|
.fetch_optional(pool)
|
|
.await
|
|
.expect("drive quota row");
|
|
let Some((dused, dquota)) = row else {
|
|
return QuotaVerdict::DriveNotFound;
|
|
};
|
|
let Some(dquota) = dquota else {
|
|
return QuotaVerdict::Ok;
|
|
};
|
|
if (dused as i128) + (additional as i128) > dquota as i128 {
|
|
return QuotaVerdict::DriveQuotaExceeded;
|
|
}
|
|
QuotaVerdict::Ok
|
|
}
|
|
|
|
/// Fused row: `(user_used, user_quota, drive_used, drive_quota, drive_found)`.
|
|
type QuotaPairRow = (i64, i64, Option<i64>, Option<i64>, bool);
|
|
|
|
/// AFTER: one fused round-trip; verdict precedence identical (user envelope
|
|
/// first, then drive existence, then drive cap).
|
|
async fn quota_after(
|
|
pool: &PgPool,
|
|
user_id: Uuid,
|
|
drive_id: Uuid,
|
|
additional: u64,
|
|
) -> QuotaVerdict {
|
|
let row: Option<QuotaPairRow> = sqlx::query_as(
|
|
r#"
|
|
SELECT u.storage_used_bytes, u.storage_quota_bytes,
|
|
d.used_bytes, d.quota_bytes, (d.id IS NOT NULL) AS drive_found
|
|
FROM auth.users u
|
|
LEFT JOIN storage.drives d ON d.id = $2
|
|
WHERE u.id = $1
|
|
"#,
|
|
)
|
|
.bind(user_id)
|
|
.bind(drive_id)
|
|
.fetch_optional(pool)
|
|
.await
|
|
.expect("fused quota row");
|
|
let Some((used, quota, dused, dquota, drive_found)) = row else {
|
|
// user missing — out of scope here (upload paths resolve the caller
|
|
// first); keep the BEFORE panic semantics.
|
|
panic!("user quota row");
|
|
};
|
|
if quota > 0 {
|
|
let additional_i = additional as i64;
|
|
if additional_i > quota || used + additional_i > quota {
|
|
return QuotaVerdict::UserQuotaExceeded;
|
|
}
|
|
}
|
|
if !drive_found {
|
|
return QuotaVerdict::DriveNotFound;
|
|
}
|
|
match dquota {
|
|
None => QuotaVerdict::Ok,
|
|
Some(dq) => {
|
|
if (dused.unwrap_or(0) as i128) + (additional as i128) > dq as i128 {
|
|
QuotaVerdict::DriveQuotaExceeded
|
|
} else {
|
|
QuotaVerdict::Ok
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn section_quota(pool: &PgPool) {
|
|
let iters: usize = env_or("BENCH_WARM_ITERS", 2000);
|
|
|
|
// Fixtures: user 10 GiB quota / 1 GiB used; capped drive; unlimited drive.
|
|
let user_ok: Uuid = sqlx::query_scalar(
|
|
"INSERT INTO auth.users (username, email, role, storage_quota_bytes, storage_used_bytes)
|
|
VALUES ('bench12_quota', 'bench12_quota@bench.invalid', 'user', 10737418240, 1073741824)
|
|
RETURNING id",
|
|
)
|
|
.fetch_one(pool)
|
|
.await
|
|
.expect("seed quota user");
|
|
let drive_cap: Uuid = sqlx::query_scalar(
|
|
"INSERT INTO storage.drives (kind, quota_bytes, used_bytes)
|
|
VALUES ('shared', 5368709120, 4294967296) RETURNING id",
|
|
)
|
|
.fetch_one(pool)
|
|
.await
|
|
.expect("seed capped drive");
|
|
let drive_unl: Uuid =
|
|
sqlx::query_scalar("INSERT INTO storage.drives (kind) VALUES ('shared') RETURNING id")
|
|
.fetch_one(pool)
|
|
.await
|
|
.expect("seed unlimited drive");
|
|
let drive_missing = Uuid::new_v4();
|
|
|
|
// Verdict-identity gate across the scenario matrix.
|
|
let scenarios: &[(Uuid, Uuid, u64)] = &[
|
|
(user_ok, drive_cap, 1024), // ok
|
|
(user_ok, drive_cap, 2 * 1024 * 1024 * 1024), // drive cap exceeded
|
|
(user_ok, drive_cap, 20 * 1024 * 1024 * 1024), // user envelope exceeded (precedence)
|
|
(user_ok, drive_unl, 8 * 1024 * 1024 * 1024), // unlimited drive, user ok
|
|
(user_ok, drive_missing, 1024), // drive missing
|
|
];
|
|
for (u, d, add) in scenarios {
|
|
let b = quota_before(pool, *u, *d, *add).await;
|
|
let a = quota_after(pool, *u, *d, *add).await;
|
|
assert_eq!(b, a, "verdict differs for add={add}");
|
|
}
|
|
println!("# [6] gate: verdict identity across 5 scenarios (incl. precedence) — OK");
|
|
|
|
let t = Instant::now();
|
|
for i in 0..iters {
|
|
std::hint::black_box(quota_before(pool, user_ok, drive_cap, (i % 4096) as u64).await);
|
|
}
|
|
let before_ms = t.elapsed().as_secs_f64() * 1e3 / iters as f64;
|
|
let t = Instant::now();
|
|
for i in 0..iters {
|
|
std::hint::black_box(quota_after(pool, user_ok, drive_cap, (i % 4096) as u64).await);
|
|
}
|
|
let after_ms = t.elapsed().as_secs_f64() * 1e3 / iters as f64;
|
|
|
|
println!("\n## [6] Upload quota pair (per NC chunk PUT / upload gate)");
|
|
println!("| arm | ms/check |");
|
|
println!("| BEFORE 2 serial point reads | {before_ms:>7.3} |");
|
|
println!("| AFTER 1 fused read | {after_ms:>7.3} |");
|
|
println!(
|
|
"# {:.2}x faster, 1 query saved per check",
|
|
before_ms / after_ms
|
|
);
|
|
|
|
sqlx::query("DELETE FROM storage.drives WHERE id IN ($1, $2)")
|
|
.bind(drive_cap)
|
|
.bind(drive_unl)
|
|
.execute(pool)
|
|
.await
|
|
.ok();
|
|
sqlx::query("DELETE FROM auth.users WHERE id = $1")
|
|
.bind(user_ok)
|
|
.execute(pool)
|
|
.await
|
|
.ok();
|
|
if after_ms >= before_ms {
|
|
eprintln!("GATE FAIL [6]: fused read not faster — rollback");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
|
|
async fn main() {
|
|
let _ = dotenvy::dotenv();
|
|
let url = std::env::var("DATABASE_URL").expect("DATABASE_URL required (see .env)");
|
|
let pool = Arc::new(
|
|
PgPoolOptions::new()
|
|
.max_connections(8)
|
|
.connect(&url)
|
|
.await
|
|
.expect("connect"),
|
|
);
|
|
|
|
println!("#################################################################");
|
|
println!("# Round-12 query-shape pack");
|
|
println!("#################################################################");
|
|
|
|
section_sharee(&pool).await;
|
|
section_login_stamp(&pool).await;
|
|
section_email_stamp(&pool).await;
|
|
section_rotation(&pool).await;
|
|
section_wopi(&pool).await;
|
|
section_quota(&pool).await;
|
|
|
|
println!("\nGATE PASS (all sections)");
|
|
}
|