Compare commits

...
Author SHA1 Message Date
Brandon Thomas 61f0b453dd fix connection pooling 2026-07-03 16:06:11 -04:00
Brandon Thomas 48f26bf621 log for providers 2026-07-03 15:15:10 -04:00
Brandon Thomas d2fb989e78 pre-event log 2026-07-03 15:02:26 -04:00
9 changed files with 238 additions and 168 deletions
@@ -1,12 +1,22 @@
use log::warn;
use sqlx::{Executor, MySql};
use artcraft_router::api::router_provider::RouterProvider;
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
use mysql_queries::queries::debug_logs::insert_debug_log::{insert_debug_log, InsertDebugLogArgs};
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
use tokens::tokens::users::UserToken;
/// Request-scoped identity for debug log inserts, so deep pipeline code can
/// log without threading four separate arguments.
pub struct GenerationDebugLogContext<'a> {
pub event_token: &'a DebugLogEventToken,
pub user_token: &'a UserToken,
pub ip_address: &'a str,
pub request_url: &'a str,
}
/// Best-effort info-level debug log of an inbound generation HTTP request.
/// Never fails the request — insert errors are logged and swallowed.
pub async fn insert_generation_request_debug_log<'e, 'c: 'e, E>(
@@ -60,3 +70,47 @@ pub async fn insert_generation_failure_debug_log<'e, 'c: 'e, E>(
warn!("Failed to insert generation failure debug log: {:?}", err);
}
}
/// The debug log type for an outbound request to the given provider, or
/// `None` for providers we don't capture.
pub fn provider_request_debug_log_type(provider: RouterProvider) -> Option<DebugLogType> {
match provider {
RouterProvider::Fal => Some(DebugLogType::FalRequest),
RouterProvider::GrokApi => Some(DebugLogType::GrokApiRequest),
RouterProvider::Seedance2Pro => Some(DebugLogType::KinoviRequest),
// First-party: the HttpRequest log already captures the user request.
RouterProvider::Artcraft => None,
// No DebugLogType variant yet; add one if GmiCloud routing is revived.
RouterProvider::GmiCloud => None,
}
}
/// Best-effort info-level debug log of an outbound provider generation
/// request. MUST be inserted during the pre-request DB phase — on the same
/// open connection as the session/billing/HTTP-request writes, BEFORE that
/// connection is released and the external provider call is made — so the
/// outbound payload is captured even when the upload/enqueue fails, without
/// ever touching the pool.
/// Never fails the request — insert errors are logged and swallowed.
pub async fn insert_provider_request_debug_log<'e, 'c: 'e, E>(
context: &'e GenerationDebugLogContext<'e>,
debug_log_type: DebugLogType,
outbound_request_debug: &'e str,
mysql_executor: E,
) where
E: 'c + Executor<'c, Database = MySql>,
{
if let Err(err) = insert_debug_log(InsertDebugLogArgs {
apriori_debug_log_event_token: Some(context.event_token),
maybe_creator_user_token: Some(context.user_token),
debug_log_type,
maybe_log_level: Some(DebugLogLevel::Info),
maybe_ip_address: Some(context.ip_address),
maybe_url: Some(context.request_url),
message: outbound_request_debug,
mysql_executor,
phantom: Default::default(),
}).await {
warn!("Failed to insert {} debug log: {:?}", debug_log_type, err);
}
}
@@ -8,6 +8,7 @@ use sqlx::Acquire;
use artcraft_api_defs::omni_api::generate_requests::omni_api_image_generate_request::OmniApiImageGenerateRequest;
use artcraft_api_defs::omni_gen::generate_response::omni_gen_image_generate_response::OmniGenImageGenerateResponse;
use artcraft_router::generate::generate_image::generate_image_response::GenerateImageResponse;
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
use enums::by_table::prompts::prompt_type::PromptType;
@@ -16,7 +17,6 @@ use enums::common::generation::common_model_type::CommonModelType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::platform_type::PlatformType;
use http_server_common::request::get_request_ip::get_request_ip;
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
use mysql_queries::queries::debug_logs::insert_debug_log::{insert_debug_log, InsertDebugLogArgs};
use mysql_queries::queries::generic_inference::api_providers::seedance2pro::insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token::KinoviVersion;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
@@ -28,6 +28,7 @@ use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::generate::common::generation_debug_logs::GenerationDebugLogContext;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::omni_api::generate::image::check_request::check_request;
use crate::http_server::endpoints::omni_api::generate::image::hydrate_to_router_request::hydrate_to_router_request;
@@ -36,8 +37,8 @@ use crate::http_server::endpoints::omni_api::generate::image::insert_db_job::ins
use crate::http_server::endpoints::omni_api::generate::image::insert_db_job::insert_seedance2pro_jobs::{insert_seedance2pro_jobs, InsertSeedance2proJobsArgs};
use crate::http_server::endpoints::omni_api::generate::image::insert_db_job::shared_job_args::SharedJobArgs;
use crate::http_server::endpoints::omni_api::generate::image::pipeline_v2::run_pipeline_v2::{run_pipeline_v2, RunPipelineV2Args};
use crate::http_server::user_lookup::user_session::session_utils::lookup::user_session_feature_flags::UserSessionFeatureFlags;
use crate::http_server::user_lookup::api_keys::require_api_key_user::require_api_key_user;
use crate::http_server::user_lookup::user_session::session_utils::lookup::user_session_feature_flags::UserSessionFeatureFlags;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
use crate::util::lookup::lookup_media_files_as_cdn_url_list_and_map::lookup_media_files_as_cdn_url_list_and_map;
@@ -152,16 +153,25 @@ pub async fn omni_api_image_generate_handler(
// ==================== PIPELINE ==================== //
// NB: Release the pooled DB connection before the (slow, external) generation call so we don't
// hold a pool slot idle while waiting on the provider — that's what starves the pool and causes
// PoolTimedOut on unrelated endpoints. We re-acquire below to write the result.
drop(mysql_connection);
// NB: The pipeline takes over the connection for its remaining pre-request DB writes (billing,
// outbound provider request debug log) and releases it before the (slow, external) generation
// call — holding a pool slot across that call is what starves the pool and causes PoolTimedOut
// on unrelated endpoints. We re-acquire below to write the result.
let debug_log_context = GenerationDebugLogContext {
event_token: &debug_log_event_token,
user_token,
ip_address: &ip_address,
request_url: &request_url,
};
let pipeline_result = run_pipeline_v2(RunPipelineV2Args {
router_builder: &router_builder,
server_state: &server_state,
user_token,
resolved_media: &resolved_media,
debug_log_context: &debug_log_context,
mysql_connection,
}).await;
// ==================== DEBUG LOG: PIPELINE ERROR ==================== //
@@ -191,25 +201,8 @@ pub async fn omni_api_image_generate_handler(
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
// ==================== DEBUG LOG: FAL REQUEST ==================== //
if let GenerateImageResponse::Fal(ref fal_payload) = pipeline_result.response {
if let Some(ref outbound_request) = fal_payload.maybe_outbound_request {
if let Err(err) = insert_debug_log(InsertDebugLogArgs {
apriori_debug_log_event_token: Some(&debug_log_event_token),
maybe_creator_user_token: Some(user_token),
debug_log_type: DebugLogType::FalRequest,
maybe_log_level: Some(DebugLogLevel::Info),
maybe_ip_address: Some(&ip_address),
maybe_url: Some(&request_url),
message: &format!("{:#?}", outbound_request),
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
}).await {
warn!("Failed to insert Fal request debug log: {:?}", err);
}
}
}
// NB: Outbound provider requests (Fal/Grok/Kinovi) are debug-logged inside
// the pipeline BEFORE the send, so the payload is captured even on failure.
// ==================== WRITE RESULT ==================== //
@@ -17,6 +17,9 @@ use tokens::tokens::users::UserToken;
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::generate::common::generation_debug_logs::{
insert_provider_request_debug_log, provider_request_debug_log_type, GenerationDebugLogContext,
};
use crate::http_server::endpoints::omni_api::generate::image::pipeline_result::ImagePipelineResult;
use crate::http_server::endpoints::omni_api::shared_utils::map_seedance2pro_router_error::map_router_error_to_web_error;
use crate::state::server_state::ServerState;
@@ -27,6 +30,11 @@ pub struct RunPipelineV2Args<'a> {
pub server_state: &'a ServerState,
pub user_token: &'a UserToken,
pub resolved_media: &'a MediaFilesAsCdnUrlListAndMap,
pub debug_log_context: &'a GenerationDebugLogContext<'a>,
/// The handler's open connection. The pipeline uses it for its remaining
/// pre-request DB writes (billing, outbound-request debug log) and releases
/// it BEFORE the external provider call.
pub mysql_connection: sqlx::pool::PoolConnection<sqlx::MySql>,
}
// NB: This pipeline does an external generation call (`finalize_and_generate`) that can take many
@@ -41,6 +49,8 @@ pub async fn run_pipeline_v2(
server_state,
user_token,
resolved_media,
debug_log_context,
mut mysql_connection,
} = args;
let hydrated_builder = apply_hydrated_media_inputs(
@@ -57,24 +67,36 @@ pub async fn run_pipeline_v2(
let apriori_job_token = InferenceJobToken::generate();
let maybe_wallet_ledger_entry_token = if cost > 0 {
// NB: Short-lived connection — released before the external call below.
let mut billing_connection = server_state.mysql_pool.acquire().await
.map_err(|err| {
warn!("Failed to acquire MySQL connection for image billing: {:?}", err);
CommonWebError::from_error(err)
})?;
// Billed on the handler's connection (same pre-request DB phase).
let deduction = attempt_wallet_deduction_else_common_web_error(
user_token,
Some(apriori_job_token.as_str()),
cost,
&mut billing_connection,
&mut mysql_connection,
).await?;
Some(deduction.ledger_entry_token)
} else {
None
};
// NB: No pooled DB connection is held across this external generation call.
// Debug-log the outbound provider request BEFORE the send — still on the
// handler's connection — so the payload is captured even when the
// upload/enqueue fails.
if let Some(debug_log_type) = provider_request_debug_log_type(draft_or_request.get_provider()) {
insert_provider_request_debug_log(
debug_log_context,
debug_log_type,
&format!("{:#?}", draft_or_request),
&mut *mysql_connection,
).await;
}
// NB: Done with pre-request DB writes. Release the pooled connection before
// the (slow, external) provider call — holding it across that call is what
// starves the pool and causes PoolTimedOut. No pooled DB connection is held
// across the external generation call below.
drop(mysql_connection);
let response = finalize_and_generate(draft_or_request, server_state, resolved_media).await?;
Ok(ImagePipelineResult {
@@ -32,6 +32,7 @@ use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::generate::common::generation_debug_logs::GenerationDebugLogContext;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::omni_api::generate::video::check_request::check_request;
use crate::http_server::endpoints::omni_api::generate::video::ingest_url_inputs::ingest_url_inputs;
@@ -217,10 +218,17 @@ pub async fn omni_api_video_generate_handler(
warn!("Failed to insert HTTP request debug log: {:?}", err);
}
// NB: Release the pooled DB connection before the (slow, external) generation call so we don't
// hold a pool slot idle while waiting on the provider — that's what starves the pool and causes
// PoolTimedOut on unrelated endpoints. We re-acquire below to write the result.
drop(mysql_connection);
// NB: The pipeline takes over the connection for its remaining pre-request DB writes (billing,
// outbound provider request debug log) and releases it before the (slow, external) generation
// call — holding a pool slot across that call is what starves the pool and causes PoolTimedOut
// on unrelated endpoints. We re-acquire below to write the result.
let debug_log_context = GenerationDebugLogContext {
event_token: &debug_log_event_token,
user_token,
ip_address: &ip_address,
request_url: &request_url,
};
let pipeline_result = run_pipeline_v2(RunPipelineV2Args {
router_builder: &router_builder,
@@ -229,6 +237,8 @@ pub async fn omni_api_video_generate_handler(
media_file_to_url_map: &media_file_to_url_map,
kinovi_character_id_map: &kinovi_character_id_map,
kinovi_account,
debug_log_context: &debug_log_context,
mysql_connection,
}).await;
// ==================== DEBUG LOG: PIPELINE ERROR ==================== //
@@ -258,45 +268,8 @@ pub async fn omni_api_video_generate_handler(
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
// ==================== DEBUG LOG: FAL REQUEST ==================== //
if let GenerateVideoResponse::Fal(ref fal_payload) = pipeline_result.response {
if let Some(ref outbound_request) = fal_payload.maybe_outbound_request {
if let Err(err) = insert_debug_log(InsertDebugLogArgs {
apriori_debug_log_event_token: Some(&debug_log_event_token),
maybe_creator_user_token: Some(user_token),
debug_log_type: DebugLogType::FalRequest,
maybe_log_level: Some(DebugLogLevel::Info),
maybe_ip_address: Some(&ip_address),
maybe_url: Some(&request_url),
message: &format!("{:#?}", outbound_request),
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
}).await {
warn!("Failed to insert Fal request debug log: {:?}", err);
}
}
}
// ==================== DEBUG LOG: GROK API REQUEST ==================== //
if let GenerateVideoResponse::Grok(ref grok_payload) = pipeline_result.response {
if let Some(ref outbound_request) = grok_payload.maybe_outbound_request {
if let Err(err) = insert_debug_log(InsertDebugLogArgs {
apriori_debug_log_event_token: Some(&debug_log_event_token),
maybe_creator_user_token: Some(user_token),
debug_log_type: DebugLogType::GrokApiRequest,
maybe_log_level: Some(DebugLogLevel::Info),
maybe_ip_address: Some(&ip_address),
maybe_url: Some(&request_url),
message: &format!("{:#?}", outbound_request),
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
}).await {
warn!("Failed to insert Grok API request debug log: {:?}", err);
}
}
}
// NB: Outbound provider requests (Fal/Grok/Kinovi) are debug-logged inside
// the pipeline BEFORE the send, so the payload is captured even on failure.
// ==================== WRITE RESULT ==================== //
@@ -15,6 +15,9 @@ use tokens::tokens::users::UserToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoint_helpers::refund_wallet_after_api_failure::refund_wallet_after_api_failure;
use crate::http_server::endpoints::generate::common::generation_debug_logs::{
insert_provider_request_debug_log, provider_request_debug_log_type, GenerationDebugLogContext,
};
use crate::http_server::endpoints::omni_api::generate::video::helpers::bill_wallet::bill_wallet;
use crate::http_server::endpoints::omni_api::generate::video::helpers::build_router_client::build_router_client;
use crate::http_server::endpoints::omni_api::generate::video::helpers::pipeline_result::PipelineResult;
@@ -31,6 +34,11 @@ pub struct RunPipelineV2Args<'a> {
pub media_file_to_url_map: &'a Option<HashMap<MediaFileToken, String>>,
pub kinovi_character_id_map: &'a Option<HashMap<CharacterToken, String>>,
pub kinovi_account: KinoviAccount,
pub debug_log_context: &'a GenerationDebugLogContext<'a>,
/// The handler's open connection. The pipeline uses it for its remaining
/// pre-request DB writes (billing, outbound-request debug log) and releases
/// it BEFORE the external provider call.
pub mysql_connection: sqlx::pool::PoolConnection<sqlx::MySql>,
}
// NB: This pipeline does an external generation call (`upload_and_generate`) that can take many
@@ -45,6 +53,8 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result<PipelineResu
media_file_to_url_map,
kinovi_character_id_map,
kinovi_account,
debug_log_context,
mut mysql_connection,
} = args;
let mut router_builder = router_builder.clone();
@@ -138,15 +148,25 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result<PipelineResu
info!("v2 estimated cost: {} credits (estimates: {:?})", cost, cost_estimates);
// 3. Bill wallet (short-lived connection — released before the external call below).
let billing = {
let mut billing_connection = server_state.mysql_pool.acquire().await
.map_err(|err| {
error!("Failed to acquire MySQL connection for billing: {:?}", err);
CommonWebError::from_error(err)
})?;
bill_wallet(user_token, cost, &mut billing_connection).await?
};
// 3. Bill wallet on the handler's connection (same pre-request DB phase).
let billing = bill_wallet(user_token, cost, &mut mysql_connection).await?;
// Debug-log the outbound provider request BEFORE the send — still on the
// handler's connection — so the payload is captured even when the
// upload/enqueue fails.
if let Some(debug_log_type) = provider_request_debug_log_type(provider) {
insert_provider_request_debug_log(
debug_log_context,
debug_log_type,
&format!("{:#?}", draft_or_request),
&mut *mysql_connection,
).await;
}
// NB: Done with pre-request DB writes. Release the pooled connection before
// the (slow, external) provider call — holding it across that call is what
// starves the pool and causes PoolTimedOut. Post-send writes re-acquire.
drop(mysql_connection);
// 4. Upload media (if draft) and generate video.
// The entire block is wrapped so Kinovi failures trigger a refund.
@@ -27,6 +27,7 @@ use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::generate::common::generation_debug_logs::GenerationDebugLogContext;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::omni_gen::generate::image::hydrate_to_router_request::hydrate_to_router_request;
use crate::http_server::endpoints::omni_gen::generate::image::insert_db_job::insert_fal_job::{insert_fal_job, InsertFalJobArgs};
@@ -150,16 +151,25 @@ pub async fn omni_gen_image_generate_handler(
// ==================== PIPELINE ==================== //
// NB: Release the pooled DB connection before the (slow, external) generation call so we don't
// hold a pool slot idle while waiting on the provider — that's what starves the pool and causes
// PoolTimedOut on unrelated endpoints. We re-acquire below to write the result.
drop(mysql_connection);
// NB: The pipeline takes over the connection for its remaining pre-request DB writes (billing,
// outbound provider request debug log) and releases it before the (slow, external) generation
// call — holding a pool slot across that call is what starves the pool and causes PoolTimedOut
// on unrelated endpoints. We re-acquire below to write the result.
let debug_log_context = GenerationDebugLogContext {
event_token: &debug_log_event_token,
user_token,
ip_address: &ip_address,
request_url: &request_url,
};
let pipeline_result = run_pipeline_v2(RunPipelineV2Args {
router_builder: &router_builder,
server_state: &server_state,
user_token,
resolved_media: &resolved_media,
debug_log_context: &debug_log_context,
mysql_connection,
}).await;
// ==================== DEBUG LOG: PIPELINE ERROR ==================== //
@@ -189,25 +199,8 @@ pub async fn omni_gen_image_generate_handler(
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
// ==================== DEBUG LOG: FAL REQUEST ==================== //
if let GenerateImageResponse::Fal(ref fal_payload) = pipeline_result.response {
if let Some(ref outbound_request) = fal_payload.maybe_outbound_request {
if let Err(err) = insert_debug_log(InsertDebugLogArgs {
apriori_debug_log_event_token: Some(&debug_log_event_token),
maybe_creator_user_token: Some(user_token),
debug_log_type: DebugLogType::FalRequest,
maybe_log_level: Some(DebugLogLevel::Info),
maybe_ip_address: Some(&ip_address),
maybe_url: Some(&request_url),
message: &format!("{:#?}", outbound_request),
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
}).await {
warn!("Failed to insert Fal request debug log: {:?}", err);
}
}
}
// NB: Outbound provider requests (Fal/Grok/Kinovi) are debug-logged inside
// the pipeline BEFORE the send, so the payload is captured even on failure.
// ==================== WRITE RESULT ==================== //
@@ -17,6 +17,9 @@ use tokens::tokens::users::UserToken;
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::generate::common::generation_debug_logs::{
insert_provider_request_debug_log, provider_request_debug_log_type, GenerationDebugLogContext,
};
use crate::http_server::endpoints::omni_gen::generate::image::pipeline_result::ImagePipelineResult;
use crate::http_server::endpoints::omni_gen::shared_utils::map_seedance2pro_router_error::map_router_error_to_web_error;
use crate::state::server_state::ServerState;
@@ -27,6 +30,11 @@ pub struct RunPipelineV2Args<'a> {
pub server_state: &'a ServerState,
pub user_token: &'a UserToken,
pub resolved_media: &'a MediaFilesAsCdnUrlListAndMap,
pub debug_log_context: &'a GenerationDebugLogContext<'a>,
/// The handler's open connection. The pipeline uses it for its remaining
/// pre-request DB writes (billing, outbound-request debug log) and releases
/// it BEFORE the external provider call.
pub mysql_connection: sqlx::pool::PoolConnection<sqlx::MySql>,
}
// NB: This pipeline does an external generation call (`finalize_and_generate`) that can take many
@@ -41,6 +49,8 @@ pub async fn run_pipeline_v2(
server_state,
user_token,
resolved_media,
debug_log_context,
mut mysql_connection,
} = args;
let hydrated_builder = apply_hydrated_media_inputs(
@@ -57,24 +67,36 @@ pub async fn run_pipeline_v2(
let apriori_job_token = InferenceJobToken::generate();
let maybe_wallet_ledger_entry_token = if cost > 0 {
// NB: Short-lived connection — released before the external call below.
let mut billing_connection = server_state.mysql_pool.acquire().await
.map_err(|err| {
warn!("Failed to acquire MySQL connection for image billing: {:?}", err);
CommonWebError::from_error(err)
})?;
// Billed on the handler's connection (same pre-request DB phase).
let deduction = attempt_wallet_deduction_else_common_web_error(
user_token,
Some(apriori_job_token.as_str()),
cost,
&mut billing_connection,
&mut mysql_connection,
).await?;
Some(deduction.ledger_entry_token)
} else {
None
};
// NB: No pooled DB connection is held across this external generation call.
// Debug-log the outbound provider request BEFORE the send — still on the
// handler's connection — so the payload is captured even when the
// upload/enqueue fails.
if let Some(debug_log_type) = provider_request_debug_log_type(draft_or_request.get_provider()) {
insert_provider_request_debug_log(
debug_log_context,
debug_log_type,
&format!("{:#?}", draft_or_request),
&mut *mysql_connection,
).await;
}
// NB: Done with pre-request DB writes. Release the pooled connection before
// the (slow, external) provider call — holding it across that call is what
// starves the pool and causes PoolTimedOut. No pooled DB connection is held
// across the external generation call below.
drop(mysql_connection);
let response = finalize_and_generate(draft_or_request, server_state, resolved_media).await?;
Ok(ImagePipelineResult {
@@ -31,6 +31,7 @@ use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::generate::common::generation_debug_logs::GenerationDebugLogContext;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::omni_gen::generate::video::helpers::hydrate_router_request::hydrate_to_router_request;
use crate::http_server::endpoints::omni_gen::generate::video::helpers::resolve_kinovi_character_ids::resolve_kinovi_character_ids;
@@ -209,10 +210,17 @@ pub async fn omni_gen_video_generate_handler(
warn!("Failed to insert HTTP request debug log: {:?}", err);
}
// NB: Release the pooled DB connection before the (slow, external) generation call so we don't
// hold a pool slot idle while waiting on the provider — that's what starves the pool and causes
// PoolTimedOut on unrelated endpoints. We re-acquire below to write the result.
drop(mysql_connection);
// NB: The pipeline takes over the connection for its remaining pre-request DB writes (billing,
// outbound provider request debug log) and releases it before the (slow, external) generation
// call — holding a pool slot across that call is what starves the pool and causes PoolTimedOut
// on unrelated endpoints. We re-acquire below to write the result.
let debug_log_context = GenerationDebugLogContext {
event_token: &debug_log_event_token,
user_token,
ip_address: &ip_address,
request_url: &request_url,
};
let pipeline_result = run_pipeline_v2(RunPipelineV2Args {
router_builder: &router_builder,
@@ -221,6 +229,8 @@ pub async fn omni_gen_video_generate_handler(
media_file_to_url_map: &media_file_to_url_map,
kinovi_character_id_map: &kinovi_character_id_map,
kinovi_account,
debug_log_context: &debug_log_context,
mysql_connection,
}).await;
// ==================== DEBUG LOG: PIPELINE ERROR ==================== //
@@ -250,45 +260,8 @@ pub async fn omni_gen_video_generate_handler(
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
// ==================== DEBUG LOG: FAL REQUEST ==================== //
if let GenerateVideoResponse::Fal(ref fal_payload) = pipeline_result.response {
if let Some(ref outbound_request) = fal_payload.maybe_outbound_request {
if let Err(err) = insert_debug_log(InsertDebugLogArgs {
apriori_debug_log_event_token: Some(&debug_log_event_token),
maybe_creator_user_token: Some(user_token),
debug_log_type: DebugLogType::FalRequest,
maybe_log_level: Some(DebugLogLevel::Info),
maybe_ip_address: Some(&ip_address),
maybe_url: Some(&request_url),
message: &format!("{:#?}", outbound_request),
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
}).await {
warn!("Failed to insert Fal request debug log: {:?}", err);
}
}
}
// ==================== DEBUG LOG: GROK API REQUEST ==================== //
if let GenerateVideoResponse::Grok(ref grok_payload) = pipeline_result.response {
if let Some(ref outbound_request) = grok_payload.maybe_outbound_request {
if let Err(err) = insert_debug_log(InsertDebugLogArgs {
apriori_debug_log_event_token: Some(&debug_log_event_token),
maybe_creator_user_token: Some(user_token),
debug_log_type: DebugLogType::GrokApiRequest,
maybe_log_level: Some(DebugLogLevel::Info),
maybe_ip_address: Some(&ip_address),
maybe_url: Some(&request_url),
message: &format!("{:#?}", outbound_request),
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
}).await {
warn!("Failed to insert Grok API request debug log: {:?}", err);
}
}
}
// NB: Outbound provider requests (Fal/Grok/Kinovi) are debug-logged inside
// the pipeline BEFORE the send, so the payload is captured even on failure.
// ==================== WRITE RESULT ==================== //
@@ -15,6 +15,9 @@ use tokens::tokens::users::UserToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoint_helpers::refund_wallet_after_api_failure::refund_wallet_after_api_failure;
use crate::http_server::endpoints::generate::common::generation_debug_logs::{
insert_provider_request_debug_log, provider_request_debug_log_type, GenerationDebugLogContext,
};
use crate::http_server::endpoints::omni_gen::generate::video::helpers::bill_wallet::bill_wallet;
use crate::http_server::endpoints::omni_gen::generate::video::helpers::build_router_client::build_router_client;
use crate::http_server::endpoints::omni_gen::generate::video::helpers::pipeline_result::PipelineResult;
@@ -31,6 +34,11 @@ pub struct RunPipelineV2Args<'a> {
pub media_file_to_url_map: &'a Option<HashMap<MediaFileToken, String>>,
pub kinovi_character_id_map: &'a Option<HashMap<CharacterToken, String>>,
pub kinovi_account: KinoviAccount,
pub debug_log_context: &'a GenerationDebugLogContext<'a>,
/// The handler's open connection. The pipeline uses it for its remaining
/// pre-request DB writes (billing, outbound-request debug log) and releases
/// it BEFORE the external provider call.
pub mysql_connection: sqlx::pool::PoolConnection<sqlx::MySql>,
}
// NB: This pipeline does an external generation call (`upload_and_generate`) that can take many
@@ -45,6 +53,8 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result<PipelineResu
media_file_to_url_map,
kinovi_character_id_map,
kinovi_account,
debug_log_context,
mut mysql_connection,
} = args;
let mut router_builder = router_builder.clone();
@@ -141,15 +151,25 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result<PipelineResu
info!("v2 estimated cost: {} credits (estimates: {:?})", cost, cost_estimates);
// 3. Bill wallet (short-lived connection — released before the external call below).
let billing = {
let mut billing_connection = server_state.mysql_pool.acquire().await
.map_err(|err| {
error!("Failed to acquire MySQL connection for billing: {:?}", err);
CommonWebError::from_error(err)
})?;
bill_wallet(user_token, cost, &mut billing_connection).await?
};
// 3. Bill wallet on the handler's connection (same pre-request DB phase).
let billing = bill_wallet(user_token, cost, &mut mysql_connection).await?;
// Debug-log the outbound provider request BEFORE the send — still on the
// handler's connection — so the payload is captured even when the
// upload/enqueue fails.
if let Some(debug_log_type) = provider_request_debug_log_type(provider) {
insert_provider_request_debug_log(
debug_log_context,
debug_log_type,
&format!("{:#?}", draft_or_request),
&mut *mysql_connection,
).await;
}
// NB: Done with pre-request DB writes. Release the pooled connection before
// the (slow, external) provider call — holding it across that call is what
// starves the pool and causes PoolTimedOut. Post-send writes re-acquire.
drop(mysql_connection);
// 4. Upload media (if draft) and generate video.
// The entire block is wrapped so Kinovi failures trigger a refund.