Skip to main content

max / makenotwork

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