| 16 |
16 |
|
//! get the next band (90%/100%) instead. This is an acceptable v1 quirk; if
|
| 17 |
17 |
|
//! it bites we'll split `last_warning_pct` into two columns.
|
| 18 |
18 |
|
|
| 19 |
|
- |
use crate::db::synckit_billing;
|
|
19 |
+ |
use std::sync::Arc;
|
|
20 |
+ |
|
|
21 |
+ |
use crate::constants::SYNCKIT_WARNINGS_PER_TICK;
|
|
22 |
+ |
use crate::db::synckit_billing::{self, WarningCandidate};
|
|
23 |
+ |
use crate::email::EmailClient;
|
| 20 |
24 |
|
use crate::AppState;
|
| 21 |
25 |
|
|
| 22 |
|
- |
/// Run one pass: fetch breach candidates, send warning emails, update
|
| 23 |
|
- |
/// `last_warning_pct`. Errors on individual apps are logged and skipped.
|
|
26 |
+ |
/// Run one pass: fetch a bounded slice of breach candidates and hand each
|
|
27 |
+ |
/// warning to the bounded background pool to send + stamp.
|
|
28 |
+ |
///
|
|
29 |
+ |
/// CHRONIC fix (ultra-fuzz Runs 1-3): this runs inside the single-threaded
|
|
30 |
+ |
/// scheduler tick, so it must never `.await` a network send here — a slow mail
|
|
31 |
+ |
/// provider times N breaching apps would block every other periodic job. The
|
|
32 |
+ |
/// tick does only bounded DB work (a `LIMIT`ed fetch); the actual `email.send`
|
|
33 |
+ |
/// happens on `state.bg`, which is concurrency-capped and fire-and-forget.
|
|
34 |
+ |
/// Warned apps stamp `last_warning_pct` and drop out of the candidate set, so
|
|
35 |
+ |
/// any overflow beyond the per-tick limit drains on subsequent ticks.
|
| 24 |
36 |
|
#[tracing::instrument(skip_all)]
|
| 25 |
37 |
|
pub(super) async fn check_and_send_warnings(state: &AppState) {
|
| 26 |
|
- |
let candidates = match synckit_billing::get_apps_needing_warning(&state.db).await {
|
| 27 |
|
- |
Ok(c) => c,
|
| 28 |
|
- |
Err(e) => {
|
| 29 |
|
- |
tracing::error!(error = ?e, "synckit warnings: query failed");
|
| 30 |
|
- |
return;
|
| 31 |
|
- |
}
|
| 32 |
|
- |
};
|
|
38 |
+ |
let candidates =
|
|
39 |
+ |
match synckit_billing::get_apps_needing_warning(&state.db, SYNCKIT_WARNINGS_PER_TICK).await {
|
|
40 |
+ |
Ok(c) => c,
|
|
41 |
+ |
Err(e) => {
|
|
42 |
+ |
tracing::error!(error = ?e, "synckit warnings: query failed");
|
|
43 |
+ |
return;
|
|
44 |
+ |
}
|
|
45 |
+ |
};
|
| 33 |
46 |
|
|
| 34 |
47 |
|
if candidates.is_empty() { return; }
|
| 35 |
|
- |
tracing::info!(count = candidates.len(), "synckit warnings: candidates");
|
|
48 |
+ |
tracing::info!(count = candidates.len(), "synckit warnings: enqueuing");
|
| 36 |
49 |
|
|
| 37 |
50 |
|
for c in candidates {
|
| 38 |
|
- |
let url = format!(
|
| 39 |
|
- |
"{}/sync/apps/{}/billing",
|
| 40 |
|
- |
state.config.host_url, c.app_id,
|
| 41 |
|
- |
);
|
| 42 |
|
- |
if let Err(e) = state.email.send_synckit_usage_warning(
|
|
51 |
+ |
let email = state.email.clone();
|
|
52 |
+ |
let db = state.db.clone();
|
|
53 |
+ |
let host_url = state.config.host_url.clone();
|
|
54 |
+ |
// Enqueue the slow send (and its stamp) onto the bounded pool so the
|
|
55 |
+ |
// tick returns immediately. Fire-and-forget: on queue overflow the
|
|
56 |
+ |
// task is dropped and the band re-fires next tick.
|
|
57 |
+ |
state.bg.spawn("synckit-usage-warning", async move {
|
|
58 |
+ |
send_warning(email, db, host_url, c).await;
|
|
59 |
+ |
});
|
|
60 |
+ |
}
|
|
61 |
+ |
}
|
|
62 |
+ |
|
|
63 |
+ |
/// Send one usage-warning email, then stamp the band so it doesn't re-fire.
|
|
64 |
+ |
/// Runs on the background pool, never on the scheduler tick. A send failure
|
|
65 |
+ |
/// skips the stamp so the band re-fires next tick (preferred over losing the
|
|
66 |
+ |
/// notification); a stamp failure logs and also re-fires.
|
|
67 |
+ |
async fn send_warning(email: EmailClient, db: sqlx::PgPool, host_url: Arc<str>, c: WarningCandidate) {
|
|
68 |
+ |
let url = format!("{host_url}/sync/apps/{}/billing", c.app_id);
|
|
69 |
+ |
if let Err(e) = email
|
|
70 |
+ |
.send_synckit_usage_warning(
|
| 43 |
71 |
|
&c.creator_email,
|
| 44 |
72 |
|
&c.app_name,
|
| 45 |
73 |
|
c.dimension,
|
| 48 |
76 |
|
c.used,
|
| 49 |
77 |
|
c.limit,
|
| 50 |
78 |
|
&url,
|
| 51 |
|
- |
).await {
|
| 52 |
|
- |
tracing::error!(
|
| 53 |
|
- |
error = ?e,
|
| 54 |
|
- |
app_id = %c.app_id,
|
| 55 |
|
- |
dimension = c.dimension,
|
| 56 |
|
- |
key = c.key.as_deref().unwrap_or(""),
|
| 57 |
|
- |
pct = c.threshold_pct,
|
| 58 |
|
- |
"synckit warnings: send failed",
|
| 59 |
|
- |
);
|
| 60 |
|
- |
continue;
|
| 61 |
|
- |
}
|
| 62 |
|
- |
// Stamp the band on the right table: per-key warnings stamp the
|
| 63 |
|
- |
// per-key row, app-wide stamps the app row. Failure to stamp logs
|
| 64 |
|
- |
// and continues — next tick will re-fire, which is preferable to
|
| 65 |
|
- |
// silently losing the notification.
|
| 66 |
|
- |
let stamp = match c.key.as_deref() {
|
| 67 |
|
- |
Some(k) => {
|
| 68 |
|
- |
synckit_billing::update_key_warning_pct(
|
| 69 |
|
- |
&state.db, c.app_id, k, c.threshold_pct,
|
| 70 |
|
- |
).await
|
| 71 |
|
- |
}
|
| 72 |
|
- |
None => {
|
| 73 |
|
- |
synckit_billing::update_warning_pct(
|
| 74 |
|
- |
&state.db, c.app_id, c.threshold_pct,
|
| 75 |
|
- |
).await
|
| 76 |
|
- |
}
|
| 77 |
|
- |
};
|
| 78 |
|
- |
if let Err(e) = stamp {
|
| 79 |
|
- |
tracing::error!(
|
| 80 |
|
- |
error = ?e,
|
| 81 |
|
- |
app_id = %c.app_id,
|
| 82 |
|
- |
key = c.key.as_deref().unwrap_or(""),
|
| 83 |
|
- |
"synckit warnings: stamp failed (will re-fire next tick)",
|
| 84 |
|
- |
);
|
| 85 |
|
- |
}
|
|
79 |
+ |
)
|
|
80 |
+ |
.await
|
|
81 |
+ |
{
|
|
82 |
+ |
tracing::error!(
|
|
83 |
+ |
error = ?e,
|
|
84 |
+ |
app_id = %c.app_id,
|
|
85 |
+ |
dimension = c.dimension,
|
|
86 |
+ |
key = c.key.as_deref().unwrap_or(""),
|
|
87 |
+ |
pct = c.threshold_pct,
|
|
88 |
+ |
"synckit warnings: send failed",
|
|
89 |
+ |
);
|
|
90 |
+ |
return;
|
|
91 |
+ |
}
|
|
92 |
+ |
// Stamp the band on the right table: per-key warnings stamp the per-key
|
|
93 |
+ |
// row, app-wide stamps the app row.
|
|
94 |
+ |
let stamp = match c.key.as_deref() {
|
|
95 |
+ |
Some(k) => synckit_billing::update_key_warning_pct(&db, c.app_id, k, c.threshold_pct).await,
|
|
96 |
+ |
None => synckit_billing::update_warning_pct(&db, c.app_id, c.threshold_pct).await,
|
|
97 |
+ |
};
|
|
98 |
+ |
if let Err(e) = stamp {
|
|
99 |
+ |
tracing::error!(
|
|
100 |
+ |
error = ?e,
|
|
101 |
+ |
app_id = %c.app_id,
|
|
102 |
+ |
key = c.key.as_deref().unwrap_or(""),
|
|
103 |
+ |
"synckit warnings: stamp failed (will re-fire next tick)",
|
|
104 |
+ |
);
|
| 86 |
105 |
|
}
|
| 87 |
106 |
|
}
|
| 88 |
107 |
|
|