Skip to content

Commit d71540a

Browse files
Optimize DB pooling, add monitoring; enhance escrow disputes & support docs (#240)
1 parent 23730e4 commit d71540a

8 files changed

Lines changed: 371 additions & 7 deletions

File tree

SUPPORT.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Support Setup
2+
3+
This document contains a minimal, repo-tracked plan to set up customer support infrastructure.
4+
5+
Goals:
6+
- Ticketing: integrate Zendesk/Intercom
7+
- In-app help center: docs and FAQ
8+
- Support email: support@example.com (placeholder)
9+
- Basic SLA and escalation templates
10+
11+
Checklist:
12+
- [ ] Provision Zendesk or Intercom workspace
13+
- [ ] Configure email routing to support@example.com
14+
- [ ] Create knowledge base and import `docs/FAQ.md`
15+
- [ ] Add in-app help center links (frontend changes required)
16+
- [ ] Train support team and document SLAs
17+
- [ ] Add basic analytics and alerting for ticket volume
18+
19+
Notes:
20+
- This file is a starting point and does not perform any automated provisioning.
21+
- For operational deployment, runbook steps should be executed by the platform team.

backend/src/config.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,36 @@ pub struct DatabaseConfig {
3333
pub url: String,
3434
#[serde(default = "default_database_pool_size")]
3535
pub max_pool_size: usize,
36+
#[serde(default = "default_database_min_pool_size")]
37+
pub min_pool_size: usize,
38+
#[serde(default = "default_pool_resize_threshold_high")]
39+
pub resize_threshold_high: f64,
40+
#[serde(default = "default_pool_resize_threshold_low")]
41+
pub resize_threshold_low: f64,
42+
#[serde(default = "default_pool_resize_step")]
43+
pub pool_resize_step: usize,
3644
}
3745

3846
fn default_database_pool_size() -> usize {
3947
16
4048
}
4149

50+
fn default_database_min_pool_size() -> usize {
51+
4
52+
}
53+
54+
fn default_pool_resize_threshold_high() -> f64 {
55+
75.0
56+
}
57+
58+
fn default_pool_resize_threshold_low() -> f64 {
59+
25.0
60+
}
61+
62+
fn default_pool_resize_step() -> usize {
63+
2
64+
}
65+
4266
#[derive(Debug, Clone, Serialize, Deserialize)]
4367
pub struct ServerConfig {
4468
pub port: u16,
@@ -216,6 +240,10 @@ impl Default for Config {
216240
database: DatabaseConfig {
217241
url: "postgres://localhost/BLINKS".to_string(),
218242
max_pool_size: 16,
243+
min_pool_size: 4,
244+
resize_threshold_high: 75.0,
245+
resize_threshold_low: 25.0,
246+
pool_resize_step: 2,
219247
},
220248
server: ServerConfig { port: 3000 },
221249
jwt: JwtConfig {

backend/src/db.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
use deadpool_postgres::{Manager, ManagerConfig, Pool, RecyclingMethod, Runtime};
22
use std::str::FromStr;
33
use tokio_postgres::NoTls;
4+
use tokio::time::{sleep, Duration};
5+
use crate::service::MetricsService;
6+
use std::sync::Arc;
7+
use std::cmp;
48

59
pub type DbPool = Pool;
610

@@ -38,6 +42,104 @@ pub async fn run_migrations(database_url: &str) -> Result<(), Box<dyn std::error
3842
Ok(())
3943
}
4044

45+
/// Start a background task that monitors database connections and updates metrics.
46+
///
47+
/// - `database_url`: Postgres connection string used for monitoring queries.
48+
/// - `configured_max_size`: the configured pool max size from configuration.
49+
/// - `check_interval_secs`: how often to poll Postgres for connection counts.
50+
///
51+
/// Returns a JoinHandle for the spawned task. The task will run until cancelled.
52+
pub fn start_db_pool_monitoring(
53+
database_url: String,
54+
configured_max_size: usize,
55+
check_interval_secs: u64,
56+
) -> tokio::task::JoinHandle<()> {
57+
tokio::spawn(async move {
58+
loop {
59+
match tokio_postgres::Config::from_str(&database_url)
60+
.and_then(|cfg| cfg.connect(NoTls))
61+
{
62+
Ok((client, connection)) => {
63+
// detach connection handling
64+
tokio::spawn(async move {
65+
if let Err(e) = connection.await {
66+
tracing::warn!(error = %e, "Postgres monitor connection error");
67+
}
68+
});
69+
70+
// Query active connections for this database
71+
match client
72+
.query_one(
73+
"SELECT count(*) FROM pg_stat_activity WHERE datname = current_database()",
74+
&[],
75+
)
76+
.await
77+
{
78+
Ok(row) => {
79+
let active: i64 = row.get(0);
80+
let active_usize = cmp::min(active as usize, 1_000_000);
81+
MetricsService::update_db_pool_status(configured_max_size, active_usize);
82+
}
83+
Err(e) => tracing::warn!(error = %e, "Failed to query pg_stat_activity"),
84+
}
85+
86+
let _ = client.close().await;
87+
}
88+
Err(e) => tracing::error!(error = %e, "Failed to connect to Postgres for monitoring"),
89+
}
90+
91+
sleep(Duration::from_secs(check_interval_secs)).await;
92+
}
93+
})
94+
}
95+
96+
/// Health check for database connectivity.
97+
pub async fn health_check_db(database_url: &str) -> bool {
98+
match tokio_postgres::Config::from_str(database_url)
99+
.and_then(|cfg| cfg.connect(NoTls))
100+
.await
101+
{
102+
Ok((mut client, connection)) => {
103+
// drive connection
104+
tokio::spawn(async move {
105+
let _ = connection.await;
106+
});
107+
108+
let res = client.query_one("SELECT 1", &[]).await.is_ok();
109+
let _ = client.close().await;
110+
res
111+
}
112+
Err(_) => false,
113+
}
114+
}
115+
116+
/// Recommend a new pool size based on current utilization and configuration.
117+
/// This function does not mutate or rebuild the pool; it only suggests a size.
118+
pub fn recommend_pool_size(
119+
current_max: usize,
120+
active_connections: usize,
121+
min_pool_size: usize,
122+
resize_step: usize,
123+
high_threshold: f64,
124+
low_threshold: f64,
125+
) -> usize {
126+
if current_max == 0 {
127+
return current_max;
128+
}
129+
130+
let utilization = (active_connections as f64 / current_max as f64) * 100.0;
131+
132+
if utilization >= high_threshold {
133+
// scale up
134+
current_max.saturating_add(resize_step)
135+
} else if utilization <= low_threshold && current_max > min_pool_size {
136+
// scale down but not below min
137+
current_max.saturating_sub(resize_step).max(min_pool_size)
138+
} else {
139+
current_max
140+
}
141+
}
142+
41143
/// Reset migrations for testing purposes
42144
/// This drops all tables, types, and the migration history to allow re-running migrations
43145
/// WARNING: Only use this in test environments! This will destroy all data in the database.

backend/src/http/metrics.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,11 @@ pub async fn prometheus_metrics() -> impl IntoResponse {
6161
/// - errorRate: number (percentage)
6262
pub async fn json_metrics(State(services): State<Arc<ServiceContainer>>) -> Json<MetricsResponse> {
6363
// Update database pool metrics
64-
let db_pool_size = services.db_pool.status().size;
65-
MetricsService::update_db_pool_metrics(db_pool_size);
64+
let status = services.db_pool.status();
65+
let db_pool_size = status.size;
66+
// active connections = size - available (deadpool status exposes `available`)
67+
let active_connections = db_pool_size.saturating_sub(status.available);
68+
MetricsService::update_db_pool_status(db_pool_size, active_connections);
6669

6770
let detailed = MetricsService::get_detailed_metrics();
6871

backend/src/service/metrics_service.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,20 @@ lazy_static! {
6262
)
6363
.expect("Can't create db_pool_connections metric");
6464

65+
/// Database pool max size gauge
66+
pub static ref DB_POOL_MAX_SIZE: Gauge = register_gauge!(
67+
"db_pool_max_size",
68+
"Configured maximum size of the database connection pool"
69+
)
70+
.expect("Can't create db_pool_max_size metric");
71+
72+
/// Database pool utilization percentage (0.0 - 100.0)
73+
pub static ref DB_POOL_UTILIZATION: Gauge = register_gauge!(
74+
"db_pool_utilization_percent",
75+
"Current database pool utilization as a percentage"
76+
)
77+
.expect("Can't create db_pool_utilization_percent metric");
78+
6579
pub static ref CACHE_EVENTS_TOTAL: CounterVec = register_counter_vec!(
6680
"cache_events_total",
6781
"Total cache events by operation and outcome",
@@ -422,7 +436,22 @@ pub struct AlertPayload {
422436

423437
/// Update database pool metrics
424438
pub fn update_db_pool_metrics(active_connections: usize) {
439+
// Backwards-compatible: treat argument as pool size when no active count available
440+
DB_POOL_CONNECTIONS.set(active_connections as f64);
441+
DB_POOL_MAX_SIZE.set(active_connections as f64);
442+
DB_POOL_UTILIZATION.set(100.0);
443+
}
444+
445+
/// Update DB pool status with both configured max size and active connections
446+
pub fn update_db_pool_status(pool_max_size: usize, active_connections: usize) {
447+
DB_POOL_MAX_SIZE.set(pool_max_size as f64);
425448
DB_POOL_CONNECTIONS.set(active_connections as f64);
449+
let utilization = if pool_max_size == 0 {
450+
0.0
451+
} else {
452+
(active_connections as f64 / pool_max_size as f64) * 100.0
453+
};
454+
DB_POOL_UTILIZATION.set(utilization);
426455
}
427456

428457
pub fn record_cache_event(operation: &str, outcome: &str) {

0 commit comments

Comments
 (0)