Skip to main content

max / makenotwork

4.8 KB · 144 lines History Blame Raw
1 //! Database access layer.
2 //!
3 //! Each submodule handles a specific domain: users, projects, items, etc.
4 //! Types (id_types, validated_types, enums, models) are re-exported flat.
5 //! Query functions live in their submodules: `db::users::get_user_by_id()`.
6
7 pub mod admin_alerts;
8 pub(crate) mod analytics;
9 pub(crate) mod auth;
10 pub mod blog_posts;
11 pub(crate) mod builds;
12 pub mod bundles;
13 pub(crate) mod cart;
14 pub(crate) mod categories;
15 pub(crate) mod chapters;
16 pub mod collections; // pub so the integration test crate can exercise the layer directly
17 pub(crate) mod content_insertions;
18 pub mod creator_tiers;
19 pub mod custom_domains;
20 pub(crate) mod custom_links;
21 pub mod custom_pages;
22 pub mod discover; // pub so the integration test crate can assert facet-count correctness directly
23 pub(crate) mod email_signups;
24 pub(crate) mod email_suppressions;
25 mod enums;
26 pub(crate) mod fan_plus;
27 pub(crate) mod follows;
28 pub mod gallery_images;
29 pub mod git_access_tokens;
30 pub mod git_repos;
31 pub(crate) mod health;
32 mod id_types;
33 pub mod idempotency; // pub so the integration test crate can exercise it directly
34 pub mod imports; // pub so the integration test crate can exercise the reaper/heartbeat layer directly
35 pub mod invites; // pub so the integration test crate can exercise redemption races directly
36 pub mod issues;
37 pub(crate) mod item_sections;
38 pub mod items;
39 pub mod license_keys;
40 pub mod mailing_lists;
41 pub(crate) mod media_files;
42 mod models;
43 pub(crate) mod moderation;
44 pub(crate) mod monitor;
45 pub(crate) mod oauth;
46 pub(crate) mod ota;
47 pub mod page_views;
48 pub mod pagination;
49 pub mod passkeys; // pub so the integration test crate can exercise the layer directly
50 pub mod patches;
51 pub mod pending_refunds;
52 pub mod pending_s3_deletions;
53 pub(crate) mod pending_uploads;
54 pub mod platform_credits;
55 pub(crate) mod project_members;
56 pub(crate) mod project_sections;
57 pub mod projects;
58 pub(crate) mod promo_codes;
59 pub mod repo_collaborators;
60 pub(crate) mod reports;
61 pub(crate) mod scan_admin_actions;
62 pub mod scan_jobs; // pub so the integration test crate can exercise the reaper/heartbeat layer directly
63 pub mod scanning; // pub so the integration test crate can exercise the quarantine purge/stamp layer directly
64 pub(crate) mod scheduler_jobs;
65 pub mod sessions;
66 pub mod ssh_keys;
67 mod subscription_writer;
68 pub(crate) mod subscriptions;
69 pub mod synckit; // pub so the integration test crate can exercise compaction directly
70 pub mod synckit_billing;
71 pub mod tags; // pub so the integration test crate can assert facet-count correctness directly
72 pub mod tips;
73 pub(crate) mod totp;
74 pub mod transactions;
75 pub mod users;
76 mod validated_types;
77 pub mod versions;
78 pub mod waitlist;
79 pub mod webhook_events;
80 pub(crate) mod wishlists;
81
82 pub use enums::*;
83 pub use id_types::*;
84 pub use models::*;
85 pub use validated_types::*;
86
87 use crate::error::Result;
88 use sqlx::PgPool;
89
90 /// Check the sandbox per-IP cap under an advisory lock on a single connection.
91 ///
92 /// Acquires a session-level advisory lock, runs the count query, and unlocks; /// all on the same connection. Returns the active sandbox count.
93 ///
94 /// This avoids the bug where `advisory_lock` + `advisory_unlock` through a pool
95 /// use different connections, leaving locks permanently held.
96 ///
97 /// Uses `pg_try_advisory_lock` to avoid blocking under burst load; if the lock
98 /// is already held, returns an error rather than waiting.
99 pub async fn check_sandbox_cap(pool: &PgPool, lock_key: i64, ip: &str) -> Result<i64> {
100 let mut conn = pool
101 .acquire()
102 .await
103 .map_err(|e| crate::error::AppError::Internal(anyhow::anyhow!("pool acquire: {e}")))?;
104
105 // Try to acquire lock (non-blocking), all on the same connection
106 let acquired: bool = sqlx::query_scalar("SELECT pg_try_advisory_lock($1)")
107 .bind(lock_key)
108 .fetch_one(&mut *conn)
109 .await?;
110
111 if !acquired {
112 return Err(crate::error::AppError::Internal(anyhow::anyhow!(
113 "sandbox cap check: could not acquire advisory lock"
114 )));
115 }
116
117 let count_result: Result<i64> = sqlx::query_scalar(
118 r"
119 SELECT COUNT(*) FROM users u
120 JOIN user_sessions us ON us.user_id = u.id
121 WHERE u.is_sandbox = TRUE
122 AND u.sandbox_expires_at > NOW()
123 AND us.ip_address = $1
124 ",
125 )
126 .bind(ip)
127 .fetch_one(&mut *conn)
128 .await
129 .map_err(Into::into);
130
131 // Release the advisory lock on EVERY exit path, not just the success one.
132 // If the COUNT above errored, an early `?` would return the connection to
133 // the pool with the session-level lock still held, it would only clear
134 // when `max_lifetime` rotates the connection out (up to 30 min later),
135 // silently wedging the per-IP lock key in the meantime. Best-effort unlock
136 // (a failed unlock is itself cleared by connection rotation).
137 let _ = sqlx::query("SELECT pg_advisory_unlock($1)")
138 .bind(lock_key)
139 .execute(&mut *conn)
140 .await;
141
142 count_result
143 }
144