Skip to main content

max / makenotwork

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