max / makenotwork
5 files changed,
+40 insertions,
-25 deletions
| @@ -763,8 +763,11 @@ pub async fn get_subscriptions_for_export_page( | |||
| 763 | 763 | ||
| 764 | 764 | // ── Event log ── | |
| 765 | 765 | ||
| 766 | - | /// Log a subscription webhook event for debugging and idempotency. | |
| 767 | - | /// The UNIQUE index on stripe_event_id makes duplicate events a no-op. | |
| 766 | + | /// Log a subscription webhook event for debugging, idempotency, and revenue | |
| 767 | + | /// reconciliation. The `ON CONFLICT (stripe_event_id) DO NOTHING` makes a | |
| 768 | + | /// redelivered event a silent no-op — never an error — so an `Err` return is | |
| 769 | + | /// always a genuine DB failure, which callers log at `error!` (a dropped | |
| 770 | + | /// reconciliation row is worth alerting on, not burying at `warn!`). | |
| 768 | 771 | #[tracing::instrument(skip_all)] | |
| 769 | 772 | pub async fn log_subscription_event( | |
| 770 | 773 | pool: &PgPool, |
| @@ -44,7 +44,7 @@ pub(super) async fn handle_invoice_payment_succeeded( | |||
| 44 | 44 | &state.db, None, event_id, "invoice.payment_succeeded.synckit_app_sub", | |
| 45 | 45 | &serde_json::json!({"stripe_sub_id": stripe_sub_id, "is_renewal": is_renewal}), | |
| 46 | 46 | ).await { | |
| 47 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 47 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 48 | 48 | } | |
| 49 | 49 | return Ok(()); | |
| 50 | 50 | } | |
| @@ -64,7 +64,7 @@ pub(super) async fn handle_invoice_payment_succeeded( | |||
| 64 | 64 | &state.db, None, event_id, "invoice.payment_succeeded.synckit", | |
| 65 | 65 | &serde_json::json!({"stripe_sub_id": stripe_sub_id, "synckit_app_id": app_id.to_string()}), | |
| 66 | 66 | ).await { | |
| 67 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 67 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 68 | 68 | } | |
| 69 | 69 | return Ok(()); | |
| 70 | 70 | } | |
| @@ -154,7 +154,7 @@ pub(super) async fn handle_invoice_payment_succeeded( | |||
| 154 | 154 | &state.db, None, event_id, "invoice.payment_succeeded.fan_plus", | |
| 155 | 155 | &serde_json::json!({"stripe_sub_id": stripe_sub_id, "is_renewal": is_renewal}), | |
| 156 | 156 | ).await { | |
| 157 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 157 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 158 | 158 | } | |
| 159 | 159 | return Ok(()); | |
| 160 | 160 | } | |
| @@ -167,7 +167,7 @@ pub(super) async fn handle_invoice_payment_succeeded( | |||
| 167 | 167 | &state.db, None, event_id, "invoice.payment_succeeded.creator_tier", | |
| 168 | 168 | &serde_json::json!({"stripe_sub_id": stripe_sub_id, "is_renewal": is_renewal}), | |
| 169 | 169 | ).await { | |
| 170 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 170 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 171 | 171 | } | |
| 172 | 172 | return Ok(()); | |
| 173 | 173 | } | |
| @@ -205,7 +205,7 @@ pub(super) async fn handle_invoice_payment_succeeded( | |||
| 205 | 205 | &state.db, sub_id, event_id, "invoice.payment_succeeded", | |
| 206 | 206 | &serde_json::json!({"stripe_sub_id": stripe_sub_id, "is_renewal": is_renewal}), | |
| 207 | 207 | ).await { | |
| 208 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 208 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 209 | 209 | } | |
| 210 | 210 | ||
| 211 | 211 | Ok(()) | |
| @@ -231,7 +231,7 @@ pub(super) async fn handle_invoice_payment_failed( | |||
| 231 | 231 | &state.db, None, event_id, "invoice.payment_failed.synckit", | |
| 232 | 232 | &serde_json::json!({"stripe_sub_id": stripe_sub_id, "synckit_app_id": app_id.to_string()}), | |
| 233 | 233 | ).await { | |
| 234 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 234 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 235 | 235 | } | |
| 236 | 236 | if let Some(ref wam) = state.wam { | |
| 237 | 237 | let title = format!("SyncKit app payment failed: {app_id}"); | |
| @@ -248,7 +248,7 @@ pub(super) async fn handle_invoice_payment_failed( | |||
| 248 | 248 | &state.db, None, event_id, "invoice.payment_failed.fan_plus", | |
| 249 | 249 | &serde_json::json!({"stripe_sub_id": stripe_sub_id}), | |
| 250 | 250 | ).await { | |
| 251 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 251 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 252 | 252 | } | |
| 253 | 253 | return Ok(()); | |
| 254 | 254 | } | |
| @@ -262,7 +262,7 @@ pub(super) async fn handle_invoice_payment_failed( | |||
| 262 | 262 | &state.db, None, event_id, "invoice.payment_failed.creator_tier", | |
| 263 | 263 | &serde_json::json!({"stripe_sub_id": stripe_sub_id}), | |
| 264 | 264 | ).await { | |
| 265 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 265 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 266 | 266 | } | |
| 267 | 267 | return Ok(()); | |
| 268 | 268 | } | |
| @@ -275,7 +275,7 @@ pub(super) async fn handle_invoice_payment_failed( | |||
| 275 | 275 | &state.db, sub_id, event_id, "invoice.payment_failed", | |
| 276 | 276 | &serde_json::json!({"stripe_sub_id": stripe_sub_id}), | |
| 277 | 277 | ).await { | |
| 278 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 278 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 279 | 279 | } | |
| 280 | 280 | ||
| 281 | 281 | // Create WAM ticket for subscription payment failures |
| @@ -141,7 +141,7 @@ pub(super) async fn handle_purchase_checkout_completed( | |||
| 141 | 141 | &state.db, None, event_id, "checkout.session.completed.purchase", | |
| 142 | 142 | &serde_json::json!({"session_id": session_id}), | |
| 143 | 143 | ).await { | |
| 144 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 144 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 145 | 145 | } | |
| 146 | 146 | ||
| 147 | 147 | // Check for a pending refund that arrived before this payment webhook. | |
| @@ -396,7 +396,7 @@ pub(super) async fn handle_subscription_checkout_completed( | |||
| 396 | 396 | &state.db, Some(sub.id), event_id, "checkout.session.completed.subscription", | |
| 397 | 397 | &serde_json::json!({"session_id": session_id, "stripe_subscription_id": stripe_subscription_id}), | |
| 398 | 398 | ).await { | |
| 399 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 399 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 400 | 400 | } | |
| 401 | 401 | ||
| 402 | 402 | Ok(()) | |
| @@ -463,7 +463,7 @@ pub(super) async fn handle_fan_plus_checkout_completed( | |||
| 463 | 463 | &state.db, None, event_id, "checkout.session.completed.fan_plus", | |
| 464 | 464 | &serde_json::json!({"session_id": session_id, "stripe_subscription_id": stripe_subscription_id}), | |
| 465 | 465 | ).await { | |
| 466 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 466 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 467 | 467 | } | |
| 468 | 468 | ||
| 469 | 469 | Ok(()) | |
| @@ -575,7 +575,7 @@ pub(super) async fn handle_creator_tier_checkout_completed( | |||
| 575 | 575 | "tier": sub.tier, | |
| 576 | 576 | }), | |
| 577 | 577 | ).await { | |
| 578 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 578 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 579 | 579 | } | |
| 580 | 580 | ||
| 581 | 581 | Ok(()) |
| @@ -53,7 +53,7 @@ pub(super) async fn handle_subscription_updated( | |||
| 53 | 53 | &state.db, None, event_id, "customer.subscription.updated.synckit", | |
| 54 | 54 | &serde_json::json!({"status": sub.status, "synckit_app_id": app_id.to_string()}), | |
| 55 | 55 | ).await { | |
| 56 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 56 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 57 | 57 | } | |
| 58 | 58 | return Ok(()); | |
| 59 | 59 | } | |
| @@ -76,7 +76,7 @@ pub(super) async fn handle_subscription_updated( | |||
| 76 | 76 | &state.db, None, event_id, "customer.subscription.updated.synckit_app_sub", | |
| 77 | 77 | &serde_json::json!({"status": sub.status}), | |
| 78 | 78 | ).await { | |
| 79 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 79 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 80 | 80 | } | |
| 81 | 81 | return Ok(()); | |
| 82 | 82 | } | |
| @@ -102,7 +102,7 @@ pub(super) async fn handle_subscription_updated( | |||
| 102 | 102 | &state.db, None, event_id, "customer.subscription.updated.fan_plus", | |
| 103 | 103 | &serde_json::json!({"status": status_str}), | |
| 104 | 104 | ).await { | |
| 105 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 105 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 106 | 106 | } | |
| 107 | 107 | return Ok(()); | |
| 108 | 108 | } | |
| @@ -124,7 +124,7 @@ pub(super) async fn handle_subscription_updated( | |||
| 124 | 124 | &state.db, None, event_id, "customer.subscription.updated.creator_tier", | |
| 125 | 125 | &serde_json::json!({"status": status_str}), | |
| 126 | 126 | ).await { | |
| 127 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 127 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 128 | 128 | } | |
| 129 | 129 | return Ok(()); | |
| 130 | 130 | } | |
| @@ -145,7 +145,7 @@ pub(super) async fn handle_subscription_updated( | |||
| 145 | 145 | &state.db, sub_id, event_id, "customer.subscription.updated", | |
| 146 | 146 | &serde_json::json!({"status": status.to_string()}), | |
| 147 | 147 | ).await { | |
| 148 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 148 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 149 | 149 | } | |
| 150 | 150 | ||
| 151 | 151 | Ok(()) | |
| @@ -167,7 +167,7 @@ pub(super) async fn handle_subscription_deleted( | |||
| 167 | 167 | &state.db, None, event_id, "customer.subscription.deleted.synckit", | |
| 168 | 168 | &serde_json::json!({"stripe_sub_id": stripe_sub_id, "synckit_app_id": app_id.to_string()}), | |
| 169 | 169 | ).await { | |
| 170 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 170 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 171 | 171 | } | |
| 172 | 172 | return Ok(()); | |
| 173 | 173 | } | |
| @@ -187,7 +187,7 @@ pub(super) async fn handle_subscription_deleted( | |||
| 187 | 187 | &state.db, None, event_id, "customer.subscription.deleted.synckit_app_sub", | |
| 188 | 188 | &serde_json::json!({"stripe_sub_id": stripe_sub_id}), | |
| 189 | 189 | ).await { | |
| 190 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 190 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 191 | 191 | } | |
| 192 | 192 | return Ok(()); | |
| 193 | 193 | } | |
| @@ -210,7 +210,7 @@ pub(super) async fn handle_subscription_deleted( | |||
| 210 | 210 | &state.db, None, event_id, "customer.subscription.deleted.fan_plus", | |
| 211 | 211 | &serde_json::json!({"stripe_sub_id": stripe_sub_id}), | |
| 212 | 212 | ).await { | |
| 213 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 213 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 214 | 214 | } | |
| 215 | 215 | return Ok(()); | |
| 216 | 216 | } | |
| @@ -229,7 +229,7 @@ pub(super) async fn handle_subscription_deleted( | |||
| 229 | 229 | &state.db, None, event_id, "customer.subscription.deleted.creator_tier", | |
| 230 | 230 | &serde_json::json!({"stripe_sub_id": stripe_sub_id}), | |
| 231 | 231 | ).await { | |
| 232 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 232 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 233 | 233 | } | |
| 234 | 234 | return Ok(()); | |
| 235 | 235 | } | |
| @@ -264,7 +264,7 @@ pub(super) async fn handle_subscription_deleted( | |||
| 264 | 264 | &state.db, sub_id, event_id, "customer.subscription.deleted", | |
| 265 | 265 | &serde_json::json!({"stripe_sub_id": stripe_sub_id}), | |
| 266 | 266 | ).await { | |
| 267 | - | tracing::warn!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 267 | + | tracing::error!(event_id = %event_id, error = ?e, "failed to log subscription event"); | |
| 268 | 268 | } | |
| 269 | 269 | ||
| 270 | 270 | Ok(()) |
| @@ -54,11 +54,23 @@ impl S3Client { | |||
| 54 | 54 | "s3-storage", | |
| 55 | 55 | ); | |
| 56 | 56 | ||
| 57 | + | // Bound every S3 operation so a hung endpoint (e.g. a stalled | |
| 58 | + | // HeadObject on a blob-confirm path) can't wedge a caller forever. These | |
| 59 | + | // apply to establishing the connection and to a single attempt's | |
| 60 | + | // round-trip to first byte of the response — they do NOT cap the time | |
| 61 | + | // spent streaming a large object body, so big uploads/downloads are | |
| 62 | + | // unaffected. The SDK's default retry policy still applies per attempt. | |
| 63 | + | let timeout_config = aws_sdk_s3::config::timeout::TimeoutConfig::builder() | |
| 64 | + | .connect_timeout(Duration::from_secs(10)) | |
| 65 | + | .operation_attempt_timeout(Duration::from_secs(60)) | |
| 66 | + | .build(); | |
| 67 | + | ||
| 57 | 68 | let s3_config = aws_sdk_s3::Config::builder() | |
| 58 | 69 | .behavior_version(BehaviorVersion::latest()) | |
| 59 | 70 | .region(Region::new(config.region.clone())) | |
| 60 | 71 | .endpoint_url(&config.endpoint) | |
| 61 | 72 | .credentials_provider(credentials) | |
| 73 | + | .timeout_config(timeout_config) | |
| 62 | 74 | .force_path_style(true) | |
| 63 | 75 | .build(); | |
| 64 | 76 |