Compare commits

...
Author SHA1 Message Date
Brandon Thomas bf09c7f689 cleanup 2026-03-03 10:02:24 -05:00
Brandon Thomas 59ea176d4c simulate job failure 2026-03-03 09:52:11 -05:00
32 changed files with 204 additions and 2 deletions
@@ -62,6 +62,7 @@ pub async fn insert_generic_inference_job_for_fal_queue<'e, 'c : 'e, E>(args: In
maybe_avt_token: args.maybe_avt_token,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
starting_job_status_override: None,
mysql_executor: args.mysql_executor,
phantom: args.phantom,
};
@@ -0,0 +1,70 @@
use idempotency::uuid::generate_random_uuid;
use sqlx::{Executor, MySql};
use std::marker::PhantomData;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::prompts::PromptToken;
use tokens::tokens::users::UserToken;
use crate::errors::database_query_error::DatabaseQueryError;
use crate::payloads::generic_inference_args::generic_inference_args::GenericInferenceArgs;
use crate::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use crate::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue_with_apriori_job_token::{
insert_generic_inference_job_for_fal_queue_with_apriori_job_token,
InsertGenericInferenceForFalWithAprioriJobTokenArgs,
};
/// Same as [`InsertGenericInferenceForFalArgs`] but without `maybe_external_third_party_id`,
/// since no external Fal call was made. The inserted job will have status
/// [`JobStatusPlus::CompleteFailure`] immediately, making it a synthetic failed job useful
/// for testing error-handling paths on the frontend without touching real inference services.
pub struct InsertGenericInferenceForFalMockFailureArgs<'e, 'c, E>
where E: 'e + Executor<'c, Database = MySql>
{
pub uuid_idempotency_token: &'e str,
pub fal_category: FalCategory,
pub maybe_inference_args: Option<GenericInferenceArgs>,
pub maybe_prompt_token: Option<&'e PromptToken>,
pub maybe_creator_user_token: Option<&'e UserToken>,
pub maybe_avt_token: Option<&'e AnonymousVisitorTrackingToken>,
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
pub async fn insert_generic_inference_job_for_fal_queue_mock_failure<'e, 'c: 'e, E>(
args: InsertGenericInferenceForFalMockFailureArgs<'e, 'c, E>,
) -> Result<InferenceJobToken, DatabaseQueryError>
where E: 'e + Executor<'c, Database = MySql>
{
let job_token = InferenceJobToken::generate();
let synthetic_external_id = format!("synthetic_{}", generate_random_uuid());
let inner_args = InsertGenericInferenceForFalWithAprioriJobTokenArgs {
uuid_idempotency_token: args.uuid_idempotency_token,
apriori_job_token: &job_token,
maybe_external_third_party_id: &synthetic_external_id,
fal_category: args.fal_category,
maybe_inference_args: args.maybe_inference_args,
maybe_prompt_token: args.maybe_prompt_token,
maybe_creator_user_token: args.maybe_creator_user_token,
maybe_avt_token: args.maybe_avt_token,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
starting_job_status_override: Some(JobStatusPlus::CompleteFailure),
mysql_executor: args.mysql_executor,
phantom: args.phantom,
};
insert_generic_inference_job_for_fal_queue_with_apriori_job_token(inner_args).await
}
@@ -46,6 +46,10 @@ pub struct InsertGenericInferenceForFalWithAprioriJobTokenArgs<'e, 'c, E>
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
/// Override the initial job status. Defaults to `Pending` when `None`.
/// Set to `Some(JobStatusPlus::CompleteFailure)` for mock/test failure jobs.
pub starting_job_status_override: Option<JobStatusPlus>,
pub mysql_executor: E,
// TODO: Not sure if this works to tell the compiler we need the lifetime annotation.
@@ -86,7 +90,7 @@ pub async fn insert_generic_inference_job_for_fal_queue_with_apriori_job_token<'
let maybe_external_third_party = InferenceJobExternalThirdParty::Fal;
const STATUS : JobStatusPlus = JobStatusPlus::Pending;
let status = args.starting_job_status_override.unwrap_or(JobStatusPlus::Pending);
let query = sqlx::query!(
r#"
@@ -152,7 +156,7 @@ SET
args.creator_ip_address,
args.creator_set_visibility.to_str(),
STATUS.to_str(),
status.to_str(),
);
let query_result = query.execute(args.mysql_executor)
@@ -1,4 +1,5 @@
pub mod get_inference_job_by_fal_id;
pub mod insert_generic_inference_job_for_fal_queue;
pub mod insert_generic_inference_job_for_fal_queue_mock_failure;
pub mod insert_generic_inference_job_for_fal_queue_with_apriori_job_token;
pub mod mark_fal_generic_inference_job_successfully_done;
@@ -0,0 +1,7 @@
/// Returns `true` if the prompt contains the secret test phrase that triggers
/// a synthetic job failure. The job record is inserted with
/// `JobStatusPlus::CompleteFailure` immediately, without calling any external
/// inference service or performing billing.
pub fn is_job_failure_test(prompt: &str) -> bool {
prompt.to_ascii_lowercase().contains("artcraft_test_job_failure")
}
@@ -1 +1,2 @@
pub mod job_failure_test;
pub mod payments_error_test;
@@ -278,6 +278,7 @@ pub async fn gemini_25_flash_edit_image_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -300,6 +300,7 @@ pub async fn gpt_image_1_edit_image_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -252,6 +252,7 @@ pub async fn qwen_edit_image_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -2,6 +2,7 @@ use std::sync::Arc;
use crate::billing::wallets::temporary_test_wallet_deduction::temporary_test_wallet_deduction;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::generate::common::job_failure_test::is_job_failure_test;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
@@ -20,9 +21,12 @@ use log::{error, info, warn};
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue_mock_failure::{insert_generic_inference_job_for_fal_queue_mock_failure, InsertGenericInferenceForFalMockFailureArgs};
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use sqlx::Acquire;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::users::UserToken;
use utoipa::ToSchema;
/// Flux 1 Dev text to image
@@ -90,6 +94,18 @@ pub async fn generate_flux_1_dev_text_to_image_handler(
CommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string())
})?;
// Secret test hook: insert a synthetic "complete_failure" job without calling Fal.
if is_job_failure_test(request.prompt.as_deref().unwrap_or("")) {
return insert_mock_failure_job(
&http_request,
request.prompt.as_deref(),
&request.uuid_idempotency_token,
maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token.as_ref(),
&mut mysql_connection,
).await;
}
// // TODO: This is test code
// let credits = match request.num_images {
// Some(GenerateFlux1DevTextToImageNumImages::One) => 100,
@@ -227,3 +243,80 @@ pub async fn generate_flux_1_dev_text_to_image_handler(
inference_job_token: job_token,
}))
}
/// Inserts a synthetic `CompleteFailure` job without calling Fal.
/// Used when the prompt triggers the secret job-failure test phrase.
async fn insert_mock_failure_job(
http_request: &HttpRequest,
prompt: Option<&str>,
uuid_idempotency_token: &str,
maybe_creator_user_token: Option<&UserToken>,
maybe_avt_token: Option<&AnonymousVisitorTrackingToken>,
mysql_connection: &mut sqlx::pool::PoolConnection<sqlx::MySql>,
) -> Result<Json<GenerateFlux1DevTextToImageResponse>, CommonWebError> {
let ip_address = get_request_ip(http_request);
let mut transaction = mysql_connection
.begin()
.await
.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token,
maybe_model_type: Some(ModelType::Flux1Dev),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: prompt,
maybe_negative_prompt: None,
maybe_other_args: None,
creator_ip_address: &ip_address,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let prompt_token = match prompt_result {
Ok(token) => Some(token),
Err(err) => {
warn!("Error inserting prompt for mock failure job: {:?}", err);
None
}
};
let mock_result = insert_generic_inference_job_for_fal_queue_mock_failure(InsertGenericInferenceForFalMockFailureArgs {
uuid_idempotency_token,
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token,
maybe_avt_token,
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let job_token = match mock_result {
Ok(token) => token,
Err(err) => {
warn!("Error inserting mock failure job: {:?}", err);
return Err(CommonWebError::ServerError);
}
};
transaction
.commit()
.await
.map_err(|err| {
error!("Error committing mock failure transaction: {:?}", err);
CommonWebError::ServerError
})?;
Ok(Json(GenerateFlux1DevTextToImageResponse {
success: true,
inference_job_token: job_token,
}))
}
@@ -187,6 +187,7 @@ pub async fn generate_flux_pro_11_text_to_image_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -191,6 +191,7 @@ pub async fn generate_flux_pro_11_ultra_text_to_image_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -234,6 +234,7 @@ pub async fn generate_gpt_image_1_text_to_image_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -299,6 +299,7 @@ pub async fn bytedance_seedream_v4_multi_function_image_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -297,6 +297,7 @@ pub async fn bytedance_seedream_v4p5_multi_function_image_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -387,6 +387,7 @@ pub async fn gpt_image_1p5_multi_function_image_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -330,6 +330,7 @@ pub async fn nano_banana_multi_function_image_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -345,6 +345,7 @@ pub async fn nano_banana_pro_multi_function_image_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -248,6 +248,7 @@ pub async fn generate_kling_1_6_pro_video_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -233,6 +233,7 @@ pub async fn generate_kling_2_1_master_video_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -248,6 +248,7 @@ pub async fn generate_kling_2_1_pro_video_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -263,6 +263,7 @@ pub async fn generate_seedance_1_0_lite_image_to_video_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -240,6 +240,7 @@ pub async fn generate_seedance_1_0_pro_image_to_video_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -246,6 +246,7 @@ pub async fn generate_veo_2_image_to_video_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -239,6 +239,7 @@ pub async fn generate_veo_3_fast_image_to_video_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -247,6 +247,7 @@ pub async fn generate_veo_3_image_to_video_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -317,6 +317,7 @@ pub async fn kling_2p5_turbo_pro_multi_function_video_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -316,6 +316,7 @@ pub async fn kling_2p6_pro_multi_function_video_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -336,6 +336,7 @@ pub async fn sora_2_multi_function_video_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -338,6 +338,7 @@ pub async fn sora_2_pro_multi_function_video_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -401,6 +401,7 @@ pub async fn veo_3p1_fast_multi_function_video_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;
@@ -401,6 +401,7 @@ pub async fn veo_3p1_multi_function_video_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
phantom: Default::default(),
}).await;