1use std::fmt::Debug;
12use std::sync::Arc;
13
14use async_trait::async_trait;
15use cdk_common::database::{self, DbTransactionFinalizer, Error, MintDatabase};
16use cdk_common::QuoteId;
17
18use crate::common::migrate;
19use crate::database::{ConnectionWithTransaction, DatabaseExecutor};
20use crate::pool::{DatabasePool, Pool, PooledResource};
21use crate::stmt::query;
22
23mod auth;
24mod completed_operations;
25mod keys;
26mod keyvalue;
27mod proofs;
28mod quotes;
29mod saga;
30mod signatures;
31
32#[rustfmt::skip]
33mod migrations {
34 include!(concat!(env!("OUT_DIR"), "/migrations_mint.rs"));
35}
36
37pub use auth::SQLMintAuthDatabase;
38#[cfg(feature = "prometheus")]
39use cdk_prometheus::MintMetricGuard;
40use migrations::MIGRATIONS;
41
42#[derive(Debug, Clone)]
44pub struct SQLMintDatabase<RM>
45where
46 RM: DatabasePool + 'static,
47{
48 pub(crate) pool: Arc<Pool<RM>>,
49}
50
51#[allow(missing_debug_implementations)]
53pub struct SQLTransaction<RM>
54where
55 RM: DatabasePool + 'static,
56{
57 pub(crate) inner: ConnectionWithTransaction<RM::Connection, PooledResource<RM>>,
58}
59
60fn quote_lock_keys(quote_ids: &[QuoteId]) -> Vec<String> {
62 let mut keys = quote_ids
63 .iter()
64 .map(|quote_id| format!("cdk:quote:{quote_id}"))
65 .collect::<Vec<_>>();
66 keys.sort();
67 keys.dedup();
68 keys
69}
70
71impl<RM> SQLMintDatabase<RM>
72where
73 RM: DatabasePool + 'static,
74{
75 pub async fn new<X>(db: X) -> Result<Self, Error>
77 where
78 X: Into<RM::Config>,
79 {
80 let pool = Pool::new(db.into());
81
82 Self::migrate(pool.get().await.map_err(|e| Error::Database(Box::new(e)))?).await?;
83
84 Ok(Self { pool })
85 }
86
87 async fn begin_transaction_from_pool(
88 pool: &Arc<Pool<RM>>,
89 ) -> Result<Box<dyn database::MintTransaction<Error> + Send + Sync>, Error> {
90 let tx = SQLTransaction {
91 inner: ConnectionWithTransaction::new(
92 pool.get().await.map_err(|e| Error::Database(Box::new(e)))?,
93 )
94 .await?,
95 };
96
97 Ok(Box::new(tx))
98 }
99
100 async fn migrate(conn: PooledResource<RM>) -> Result<(), Error> {
102 let tx = ConnectionWithTransaction::new(conn).await?;
103 migrate(&tx, RM::Connection::name(), MIGRATIONS).await?;
104 tx.commit().await?;
105 Ok(())
106 }
107}
108
109impl<RM> SQLTransaction<RM>
110where
111 RM: DatabasePool + 'static,
112{
113 async fn take_quote_locks(&mut self, quote_ids: &[QuoteId]) -> Result<bool, Error> {
115 if quote_ids.is_empty() || RM::Connection::name() != "postgres" {
116 return Ok(false);
117 }
118
119 query(
120 r#"
121 SELECT pg_advisory_xact_lock(hashtextextended(key, 0))
122 FROM (
123 SELECT key FROM unnest(ARRAY[:keys]::TEXT[]) AS t(key) ORDER BY key
124 ) sorted
125 "#,
126 )?
127 .bind_vec("keys", quote_lock_keys(quote_ids))?
128 .execute(&self.inner)
129 .await?;
130
131 Ok(true)
132 }
133}
134
135#[async_trait]
136impl<RM> database::MintTransaction<Error> for SQLTransaction<RM>
137where
138 RM: DatabasePool + 'static,
139{
140 async fn lock_quotes(&mut self, quote_ids: &[QuoteId]) -> Result<bool, Error> {
141 self.take_quote_locks(quote_ids).await
142 }
143}
144
145#[async_trait]
146impl<RM> DbTransactionFinalizer for SQLTransaction<RM>
147where
148 RM: DatabasePool + 'static,
149{
150 type Err = Error;
151
152 async fn commit(self: Box<Self>) -> Result<(), Error> {
153 #[cfg(feature = "prometheus")]
154 let metrics = MintMetricGuard::new("transaction_commit");
155
156 let result = self.inner.commit().await;
157
158 #[cfg(feature = "prometheus")]
159 {
160 metrics.record(result.is_ok());
161 }
162
163 Ok(result?)
164 }
165
166 async fn rollback(self: Box<Self>) -> Result<(), Error> {
167 #[cfg(feature = "prometheus")]
168 let metrics = MintMetricGuard::new("transaction_rollback");
169
170 let result = self.inner.rollback().await;
171
172 #[cfg(feature = "prometheus")]
173 {
174 metrics.record(result.is_ok());
175 }
176 Ok(result?)
177 }
178}
179
180#[async_trait]
181impl<RM> MintDatabase<Error> for SQLMintDatabase<RM>
182where
183 RM: DatabasePool + 'static,
184{
185 async fn begin_transaction(
186 &self,
187 ) -> Result<Box<dyn database::MintTransaction<Error> + Send + Sync>, Error> {
188 Self::begin_transaction_from_pool(&self.pool).await
189 }
190}
191
192#[cfg(all(test, feature = "prometheus"))]
193mod tests {
194 use std::fmt;
195 use std::sync::atomic::AtomicBool;
196 use std::sync::Arc;
197 use std::time::Duration;
198
199 use cdk_common::database::{DbTransactionFinalizer, Error as DatabaseError};
200 use cdk_prometheus::METRICS;
201
202 use super::SQLTransaction;
203 use crate::database::{
204 ConnectionWithTransaction, DatabaseConnector, DatabaseExecutor, DatabaseTransaction,
205 };
206 use crate::pool::{DatabaseConfig, DatabasePool, Error as PoolError, Pool};
207 use crate::stmt::{Column, Statement};
208
209 #[derive(Debug, Clone)]
210 struct TestConfig {
211 fail_commit: bool,
212 fail_rollback: bool,
213 }
214
215 impl DatabaseConfig for TestConfig {
216 fn max_size(&self) -> usize {
217 1
218 }
219
220 fn default_timeout(&self) -> Duration {
221 Duration::from_millis(10)
222 }
223 }
224
225 #[derive(Debug)]
226 struct TestResourceError;
227
228 impl fmt::Display for TestResourceError {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 f.write_str("test resource error")
231 }
232 }
233
234 impl std::error::Error for TestResourceError {}
235
236 #[derive(Debug)]
237 struct TestConnection {
238 fail_commit: bool,
239 fail_rollback: bool,
240 }
241
242 #[async_trait::async_trait]
243 impl DatabaseExecutor for TestConnection {
244 fn name() -> &'static str {
245 "test"
246 }
247
248 async fn execute(&self, _statement: Statement) -> Result<usize, DatabaseError> {
249 Ok(0)
250 }
251
252 async fn fetch_one(
253 &self,
254 _statement: Statement,
255 ) -> Result<Option<Vec<Column>>, DatabaseError> {
256 Ok(None)
257 }
258
259 async fn fetch_all(
260 &self,
261 _statement: Statement,
262 ) -> Result<Vec<Vec<Column>>, DatabaseError> {
263 Ok(Vec::new())
264 }
265
266 async fn pluck(&self, _statement: Statement) -> Result<Option<Column>, DatabaseError> {
267 Ok(None)
268 }
269
270 async fn batch(&self, _statement: Statement) -> Result<(), DatabaseError> {
271 Ok(())
272 }
273 }
274
275 #[derive(Debug)]
276 struct TestTransaction;
277
278 #[async_trait::async_trait]
279 impl DatabaseTransaction<TestConnection> for TestTransaction {
280 async fn commit(conn: &mut TestConnection) -> Result<(), DatabaseError> {
281 if conn.fail_commit {
282 Err(DatabaseError::Internal("commit failed".to_owned()))
283 } else {
284 Ok(())
285 }
286 }
287
288 async fn begin(_conn: &mut TestConnection) -> Result<(), DatabaseError> {
289 Ok(())
290 }
291
292 async fn rollback(conn: &mut TestConnection) -> Result<(), DatabaseError> {
293 if conn.fail_rollback {
294 Err(DatabaseError::Internal("rollback failed".to_owned()))
295 } else {
296 Ok(())
297 }
298 }
299 }
300
301 impl DatabaseConnector for TestConnection {
302 type Transaction = TestTransaction;
303 }
304
305 #[derive(Debug)]
306 struct TestPool;
307
308 impl DatabasePool for TestPool {
309 type Connection = TestConnection;
310 type Config = TestConfig;
311 type Error = TestResourceError;
312
313 fn new_resource(
314 config: &Self::Config,
315 _stale: Arc<AtomicBool>,
316 _timeout: Duration,
317 ) -> Result<Self::Connection, PoolError<Self::Error>> {
318 Ok(TestConnection {
319 fail_commit: config.fail_commit,
320 fail_rollback: config.fail_rollback,
321 })
322 }
323 }
324
325 async fn new_transaction(fail_commit: bool, fail_rollback: bool) -> SQLTransaction<TestPool> {
326 let pool = Pool::<TestPool>::new(TestConfig {
327 fail_commit,
328 fail_rollback,
329 });
330 let conn = pool
331 .get()
332 .await
333 .expect("test resource should be checked out");
334 let inner = ConnectionWithTransaction::new(conn)
335 .await
336 .expect("test transaction should begin");
337
338 SQLTransaction { inner }
339 }
340
341 fn labels_match(
342 metric: &cdk_prometheus::prometheus::proto::Metric,
343 labels: &[(&str, &str)],
344 ) -> bool {
345 labels.iter().all(|(name, value)| {
346 metric
347 .get_label()
348 .iter()
349 .any(|label| label.name() == *name && label.value() == *value)
350 })
351 }
352
353 fn counter_value(name: &str, labels: &[(&str, &str)]) -> f64 {
354 for family in METRICS.registry().gather() {
355 if family.name() != name {
356 continue;
357 }
358
359 for metric in family.get_metric() {
360 if labels_match(metric, labels) {
361 return metric.get_counter().value();
362 }
363 }
364 }
365
366 0.0
367 }
368
369 fn gauge_value(name: &str, labels: &[(&str, &str)]) -> f64 {
370 for family in METRICS.registry().gather() {
371 if family.name() != name {
372 continue;
373 }
374
375 for metric in family.get_metric() {
376 if labels_match(metric, labels) {
377 return metric.get_gauge().value();
378 }
379 }
380 }
381
382 0.0
383 }
384
385 fn histogram_count(name: &str, labels: &[(&str, &str)]) -> f64 {
386 for family in METRICS.registry().gather() {
387 if family.name() != name {
388 continue;
389 }
390
391 for metric in family.get_metric() {
392 if labels_match(metric, labels) {
393 return metric.get_histogram().sample_count() as f64;
394 }
395 }
396 }
397
398 0.0
399 }
400
401 #[tokio::test(flavor = "current_thread")]
402 async fn transaction_commit_records_success_duration_and_balances_in_flight() {
403 let _lock = crate::metrics_test_lock::lock().await;
404 let operation = "transaction_commit";
405 let labels = [("operation", operation), ("status", "success")];
406 let in_flight_labels = [("operation", operation)];
407
408 let success_before = counter_value("cdk_mint_operations_total", &labels);
409 let duration_count_before = histogram_count("cdk_mint_operation_duration_seconds", &labels);
410 let in_flight_before = gauge_value("cdk_mint_in_flight_requests", &in_flight_labels);
411
412 let tx = new_transaction(false, false).await;
413 Box::new(tx)
414 .commit()
415 .await
416 .expect("transaction commit should succeed");
417
418 assert_eq!(
419 counter_value("cdk_mint_operations_total", &labels),
420 success_before + 1.0
421 );
422 assert_eq!(
423 histogram_count("cdk_mint_operation_duration_seconds", &labels),
424 duration_count_before + 1.0
425 );
426 assert_eq!(
427 gauge_value("cdk_mint_in_flight_requests", &in_flight_labels),
428 in_flight_before
429 );
430 }
431
432 #[tokio::test(flavor = "current_thread")]
433 async fn transaction_commit_records_error_duration_and_balances_in_flight() {
434 let _lock = crate::metrics_test_lock::lock().await;
435 let operation = "transaction_commit";
436 let labels = [("operation", operation), ("status", "error")];
437 let in_flight_labels = [("operation", operation)];
438
439 let error_before = counter_value("cdk_mint_operations_total", &labels);
440 let duration_count_before = histogram_count("cdk_mint_operation_duration_seconds", &labels);
441 let in_flight_before = gauge_value("cdk_mint_in_flight_requests", &in_flight_labels);
442
443 let tx = new_transaction(true, false).await;
444 Box::new(tx)
445 .commit()
446 .await
447 .expect_err("transaction commit should fail");
448
449 assert_eq!(
450 counter_value("cdk_mint_operations_total", &labels),
451 error_before + 1.0
452 );
453 assert_eq!(
454 histogram_count("cdk_mint_operation_duration_seconds", &labels),
455 duration_count_before + 1.0
456 );
457 assert_eq!(
458 gauge_value("cdk_mint_in_flight_requests", &in_flight_labels),
459 in_flight_before
460 );
461 }
462
463 #[tokio::test(flavor = "current_thread")]
464 async fn transaction_rollback_records_success_duration_and_balances_in_flight() {
465 let _lock = crate::metrics_test_lock::lock().await;
466 let operation = "transaction_rollback";
467 let labels = [("operation", operation), ("status", "success")];
468 let in_flight_labels = [("operation", operation)];
469
470 let success_before = counter_value("cdk_mint_operations_total", &labels);
471 let duration_count_before = histogram_count("cdk_mint_operation_duration_seconds", &labels);
472 let in_flight_before = gauge_value("cdk_mint_in_flight_requests", &in_flight_labels);
473
474 let tx = new_transaction(false, false).await;
475 Box::new(tx)
476 .rollback()
477 .await
478 .expect("transaction rollback should succeed");
479
480 assert_eq!(
481 counter_value("cdk_mint_operations_total", &labels),
482 success_before + 1.0
483 );
484 assert_eq!(
485 histogram_count("cdk_mint_operation_duration_seconds", &labels),
486 duration_count_before + 1.0
487 );
488 assert_eq!(
489 gauge_value("cdk_mint_in_flight_requests", &in_flight_labels),
490 in_flight_before
491 );
492 }
493}
494
495#[cfg(test)]
496mod quote_lock_tests {
497 use super::*;
498
499 #[test]
500 fn quote_lock_keys_are_sorted_and_deduplicated() {
501 let first = QuoteId::BASE64("YQ==".to_owned());
502 let second = QuoteId::BASE64("Yg==".to_owned());
503 let mut expected = vec![format!("cdk:quote:{first}"), format!("cdk:quote:{second}")];
504 expected.sort();
505
506 assert_eq!(quote_lock_keys(&[second.clone(), first, second]), expected);
507 }
508}