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