| 71 |
71 |
|
use crate::error::Result;
|
| 72 |
72 |
|
use sqlx::PgPool;
|
| 73 |
73 |
|
|
| 74 |
|
- |
/// Acquire a session-level PostgreSQL advisory lock (blocks until available).
|
| 75 |
|
- |
pub async fn advisory_lock(pool: &PgPool, key: i64) -> Result<()> {
|
| 76 |
|
- |
sqlx::query("SELECT pg_advisory_lock($1)")
|
| 77 |
|
- |
.bind(key)
|
| 78 |
|
- |
.execute(pool)
|
| 79 |
|
- |
.await?;
|
| 80 |
|
- |
Ok(())
|
| 81 |
|
- |
}
|
|
74 |
+ |
/// Check the sandbox per-IP cap under an advisory lock on a single connection.
|
|
75 |
+ |
///
|
|
76 |
+ |
/// Acquires a session-level advisory lock, runs the count query, and unlocks —
|
|
77 |
+ |
/// all on the same connection. Returns the active sandbox count.
|
|
78 |
+ |
///
|
|
79 |
+ |
/// This avoids the bug where `advisory_lock` + `advisory_unlock` through a pool
|
|
80 |
+ |
/// use different connections, leaving locks permanently held.
|
|
81 |
+ |
pub async fn check_sandbox_cap(pool: &PgPool, lock_key: i64, ip: &str) -> Result<i64> {
|
|
82 |
+ |
let mut conn = pool.acquire().await.map_err(|e| {
|
|
83 |
+ |
crate::error::AppError::Internal(anyhow::anyhow!("pool acquire: {}", e))
|
|
84 |
+ |
})?;
|
| 82 |
85 |
|
|
| 83 |
|
- |
/// Release a session-level PostgreSQL advisory lock.
|
| 84 |
|
- |
pub async fn advisory_unlock(pool: &PgPool, key: i64) -> Result<()> {
|
| 85 |
|
- |
sqlx::query("SELECT pg_advisory_unlock($1)")
|
| 86 |
|
- |
.bind(key)
|
| 87 |
|
- |
.execute(pool)
|
|
86 |
+ |
// Lock, count, unlock — all on the same connection
|
|
87 |
+ |
sqlx::query("SELECT pg_advisory_lock($1)")
|
|
88 |
+ |
.bind(lock_key)
|
|
89 |
+ |
.execute(&mut *conn)
|
| 88 |
90 |
|
.await?;
|
| 89 |
|
- |
Ok(())
|
|
91 |
+ |
|
|
92 |
+ |
let count: i64 = sqlx::query_scalar(
|
|
93 |
+ |
r#"
|
|
94 |
+ |
SELECT COUNT(*) FROM users u
|
|
95 |
+ |
JOIN user_sessions us ON us.user_id = u.id
|
|
96 |
+ |
WHERE u.is_sandbox = TRUE
|
|
97 |
+ |
AND u.sandbox_expires_at > NOW()
|
|
98 |
+ |
AND us.ip_address = $1
|
|
99 |
+ |
"#,
|
|
100 |
+ |
)
|
|
101 |
+ |
.bind(ip)
|
|
102 |
+ |
.fetch_one(&mut *conn)
|
|
103 |
+ |
.await?;
|
|
104 |
+ |
|
|
105 |
+ |
sqlx::query("SELECT pg_advisory_unlock($1)")
|
|
106 |
+ |
.bind(lock_key)
|
|
107 |
+ |
.execute(&mut *conn)
|
|
108 |
+ |
.await?;
|
|
109 |
+ |
|
|
110 |
+ |
Ok(count)
|
| 90 |
111 |
|
}
|