diff --git a/crates/lib/mysql_queries/src/errors/database_query_error.rs b/crates/lib/mysql_queries/src/errors/database_query_error.rs new file mode 100644 index 0000000000..af27d42d79 --- /dev/null +++ b/crates/lib/mysql_queries/src/errors/database_query_error.rs @@ -0,0 +1,47 @@ + +#[derive(Debug)] +pub enum DatabaseQueryError { + /// A duplicate idempotency token error occurred. + /// This should be surfaced as a 400 to the user. + IdempotencyDuplicateKeyError, + + /// An uncategorized error occurred. + /// This will likely result in a 500 for the user. + SqlxError(sqlx::Error), + + /// An uncategorized non-database error occurred. + /// This will likely result in a 500 for the user. + AnyhowError(anyhow::Error), +} + +impl From for DatabaseQueryError { + fn from(err: anyhow::Error) -> Self { + DatabaseQueryError::AnyhowError(err) + } +} + +impl From for DatabaseQueryError { + fn from(err: sqlx::Error) -> Self { + DatabaseQueryError::SqlxError(err) + } +} + +impl DatabaseQueryError { + /// Whether we should surface this failure as a 400 to the user. + /// This could be any field (for now we only have the idempotency token). + pub fn is_400_error(&self) -> bool { + match self { + DatabaseQueryError::IdempotencyDuplicateKeyError => true, + _ => false, + } + } + + /// Whether we should surface this failure as a 400 to the user. + /// Specifically, if we know it was the idempotency token. + pub fn had_duplicate_idempotency_token(&self) -> bool { + match self { + DatabaseQueryError::IdempotencyDuplicateKeyError => true, + _ => false, + } + } +} diff --git a/crates/lib/mysql_queries/src/errors/mod.rs b/crates/lib/mysql_queries/src/errors/mod.rs new file mode 100644 index 0000000000..6cfdb32c7b --- /dev/null +++ b/crates/lib/mysql_queries/src/errors/mod.rs @@ -0,0 +1 @@ +pub mod database_query_error; \ No newline at end of file diff --git a/crates/lib/mysql_queries/src/lib.rs b/crates/lib/mysql_queries/src/lib.rs index ca82d4a683..fb987a2b9e 100644 --- a/crates/lib/mysql_queries/src/lib.rs +++ b/crates/lib/mysql_queries/src/lib.rs @@ -7,6 +7,7 @@ pub mod common_inputs; pub mod complex_models; pub mod composite_keys; pub mod config; +pub mod errors; pub mod helpers; pub mod mediators; pub mod payloads; diff --git a/crates/lib/mysql_queries/src/queries/generic_inference/web/insert_generic_inference_job.rs b/crates/lib/mysql_queries/src/queries/generic_inference/web/insert_generic_inference_job.rs index 837c2ac55c..aef8a7cbbd 100644 --- a/crates/lib/mysql_queries/src/queries/generic_inference/web/insert_generic_inference_job.rs +++ b/crates/lib/mysql_queries/src/queries/generic_inference/web/insert_generic_inference_job.rs @@ -1,4 +1,5 @@ use anyhow::anyhow; +use sqlx::error::DatabaseError; use sqlx::MySqlPool; use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory; @@ -6,12 +7,12 @@ use enums::by_table::generic_inference_jobs::inference_input_source_token_type:: use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType; use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType; use enums::common::visibility::Visibility; -use errors::AnyhowResult; use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken; use tokens::tokens::generic_inference_jobs::InferenceJobToken; use tokens::tokens::media_files::MediaFileToken; use tokens::tokens::users::UserToken; +use crate::errors::database_query_error::DatabaseQueryError; use crate::payloads::generic_inference_args::generic_inference_args::GenericInferenceArgs; pub struct InsertGenericInferenceArgs<'a> { @@ -30,7 +31,7 @@ pub struct InsertGenericInferenceArgs<'a> { pub maybe_input_source_token_type: Option, // For jobs that perform "downloads", this is the URL to download. - // NB: Some jobs aren't using this field yet and will pack the URL inside of + // NB: Some jobs aren't using this field yet and will pack the URL inside // the "GenericInferenceArgs" field. The goal is to migrate them to this // top-level field eventually. pub maybe_download_url: Option<&'a str>, @@ -58,7 +59,9 @@ pub struct InsertGenericInferenceArgs<'a> { pub mysql_pool: &'a MySqlPool, } -pub async fn insert_generic_inference_job(args: InsertGenericInferenceArgs<'_>) -> AnyhowResult<(InferenceJobToken, u64)> { +pub async fn insert_generic_inference_job(args: InsertGenericInferenceArgs<'_>) + -> Result<(InferenceJobToken, u64), DatabaseQueryError> +{ let job_token = InferenceJobToken::generate(); let serialized_args_payload = serde_json::ser::to_string(&args.maybe_inference_args) @@ -157,7 +160,19 @@ SET res.last_insert_id() }, Err(err) => { - return Err(anyhow!("error inserting new generic inference job: {:?}", err)); + if let Some(db_err) = err.as_database_error() { + // NB: SQLSTATE[23000]: Integrity constraint violation + // NB: MySQL Error Code 1062: Duplicate key insertion (this is harder to access) + let is_integrity_violation = db_err.code().as_deref() == Some("23000"); + let is_duplicate_key = db_err.message().contains("Duplicate entry"); + let is_idempotency_error = db_err.message().contains("uuid_idempotency_token"); + + if is_integrity_violation && is_duplicate_key && is_idempotency_error { + return Err(DatabaseQueryError::IdempotencyDuplicateKeyError); + } + } + + return Err(DatabaseQueryError::SqlxError(err)); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/animation/enqueue_face_animation.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/animation/enqueue_face_animation.rs index 106031fd94..d607a3ae8f 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/animation/enqueue_face_animation.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/animation/enqueue_face_animation.rs @@ -326,6 +326,9 @@ pub async fn enqueue_face_animation_handler( Ok((job_token, _id)) => job_token, Err(err) => { warn!("New generic inference job creation DB error: {:?}", err); + if err.had_duplicate_idempotency_token() { + return Err(EnqueueFaceAnimationError::BadInput("Duplicate idempotency token".to_string())); + } return Err(EnqueueFaceAnimationError::ServerError); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/animation/enqueue_rerender_animation.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/animation/enqueue_rerender_animation.rs index 0dc417459a..e658edbbee 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/animation/enqueue_rerender_animation.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/animation/enqueue_rerender_animation.rs @@ -229,6 +229,9 @@ pub async fn enqueue_rerender_animation_handler( Ok((job_token, _id)) => job_token, Err(err) => { warn!("New generic inference job creation DB error: {:?}", err); + if err.had_duplicate_idempotency_token() { + return Err(EnqueueRerenderAnimationError::BadInput("Duplicate idempotency token".to_string())); + } return Err(EnqueueRerenderAnimationError::ServerError); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/conversion/enqueue_fbx_to_gltf_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/conversion/enqueue_fbx_to_gltf_handler.rs index 5f66fabd53..531b37c587 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/conversion/enqueue_fbx_to_gltf_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/conversion/enqueue_fbx_to_gltf_handler.rs @@ -220,6 +220,9 @@ pub async fn enqueue_fbx_to_gltf_handler( Ok((job_token, _id)) => job_token, Err(err) => { warn!("New generic inference job creation DB error: {:?}", err); + if err.had_duplicate_idempotency_token() { + return Err(EnqueueFbxToGltfRequestError::BadInput("Duplicate idempotency token".to_string())); + } return Err(EnqueueFbxToGltfRequestError::ServerError); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/conversion/enqueue_render_engine_scene_to_video_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/conversion/enqueue_render_engine_scene_to_video_handler.rs index 0ca77c8ab6..13f5a22f2b 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/conversion/enqueue_render_engine_scene_to_video_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/conversion/enqueue_render_engine_scene_to_video_handler.rs @@ -278,6 +278,9 @@ pub async fn enqueue_render_engine_scene_to_video_handler( Ok((job_token, _id)) => job_token, Err(err) => { warn!("New generic inference job creation DB error: {:?}", err); + if err.had_duplicate_idempotency_token() { + return Err(EnqueueBvhToWorkflowRequestError::BadInput("Duplicate idempotency token".to_string())); + } return Err(EnqueueBvhToWorkflowRequestError::ServerError); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/image_gen/enqueue_image_generation.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/image_gen/enqueue_image_generation.rs index 978c07198e..328d2b246a 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/image_gen/enqueue_image_generation.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/image_gen/enqueue_image_generation.rs @@ -440,6 +440,9 @@ pub async fn enqueue_image_generation_request( Ok((job_token, _id)) => job_token, Err(err) => { warn!("New generic inference job creation DB error: {:?}", err); + if err.had_duplicate_idempotency_token() { + return Err(EnqueueImageGenRequestError::BadInput("Duplicate idempotency token".to_string())); + } return Err(EnqueueImageGenRequestError::ServerError); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/mocap/enqueue_mocapnet.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/mocap/enqueue_mocapnet.rs index fd7be91624..1b4b04df7f 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/mocap/enqueue_mocapnet.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/mocap/enqueue_mocapnet.rs @@ -233,6 +233,9 @@ pub async fn enqueue_mocapnet_handler( Ok((job_token, _id)) => job_token, Err(err) => { warn!("New generic inference job creation DB error: {:?}", err); + if err.had_duplicate_idempotency_token() { + return Err(EnqueueMocapnetError::BadInput("Duplicate idempotency token".to_string())); + } return Err(EnqueueMocapnetError::ServerError); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/model_download/enqueue_gptsovits_model_download_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/model_download/enqueue_gptsovits_model_download_handler.rs index 75f11e2eea..edfae65ca2 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/model_download/enqueue_gptsovits_model_download_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/model_download/enqueue_gptsovits_model_download_handler.rs @@ -206,6 +206,9 @@ pub async fn enqueue_gptsovits_model_download_handler( Ok((job_token, _id)) => job_token, Err(err) => { warn!("New generic inference job creation DB error: {:?}", err); + if err.had_duplicate_idempotency_token() { + return Err(EnqueueGptSovitsModelDownloadError::BadInput("Duplicate idempotency token".to_string())); + } return Err(EnqueueGptSovitsModelDownloadError::ServerError); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts_handler/enqueue_infer_tts_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts_handler/enqueue_infer_tts_handler.rs index 93f41d0b94..8d8954ea94 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts_handler/enqueue_infer_tts_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts_handler/enqueue_infer_tts_handler.rs @@ -448,6 +448,9 @@ pub async fn enqueue_infer_tts_handler( Ok((inference_job_token, _id)) => inference_job_token, Err(err) => { warn!("New (generic) tts inference job creation DB error: {:?}", err); + if err.had_duplicate_idempotency_token() { + return Err(InferTtsError::BadInput("Duplicate idempotency token".to_string())); + } return Err(InferTtsError::ServerError); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/inference/enqueue_voice_conversion_inference.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/inference/enqueue_voice_conversion_inference.rs index e71a5d154e..060b3348ff 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/inference/enqueue_voice_conversion_inference.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/inference/enqueue_voice_conversion_inference.rs @@ -388,6 +388,9 @@ pub async fn enqueue_voice_conversion_inference_handler( Ok((job_token, _id)) => job_token, Err(err) => { warn!("New generic inference job creation DB error: {:?}", err); + if err.had_duplicate_idempotency_token() { + return Err(EnqueueVoiceConversionInferenceError::BadInput("Duplicate idempotency token".to_string())); + } return Err(EnqueueVoiceConversionInferenceError::ServerError); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/inference/enqueue_tts_request.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/inference/enqueue_tts_request.rs index 7997f43b1d..921636c173 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/inference/enqueue_tts_request.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/inference/enqueue_tts_request.rs @@ -20,14 +20,14 @@ use enums::common::visibility::Visibility; use http_server_common::request::get_request_header_optional::get_request_header_optional; use http_server_common::request::get_request_ip::get_request_ip; use mysql_queries::payloads::generic_inference_args::generic_inference_args::{ - GenericInferenceArgs, - InferenceCategoryAbbreviated, - PolymorphicInferenceArgs, + GenericInferenceArgs, + InferenceCategoryAbbreviated, + PolymorphicInferenceArgs, }; use mysql_queries::payloads::generic_inference_args::tts_payload::TTSArgs; use mysql_queries::queries::generic_inference::web::insert_generic_inference_job::{ - insert_generic_inference_job, - InsertGenericInferenceArgs, + insert_generic_inference_job, + InsertGenericInferenceArgs, }; use tokens::tokens::generic_inference_jobs::InferenceJobToken; use tokens::tokens::users::UserToken; @@ -220,6 +220,9 @@ pub async fn enqueue_tts_request( Ok((job_token, _id)) => job_token, Err(err) => { warn!("New generic inference job creation DB error: {:?}", err); + if err.had_duplicate_idempotency_token() { + return Err(EnqueueTTSRequestError::BadInput("Duplicate idempotency token".to_string())); + } return Err(EnqueueTTSRequestError::ServerError); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/voices/create_voice.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/voices/create_voice.rs index ea0f16b85b..fb6458b2ca 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/voices/create_voice.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/voices/create_voice.rs @@ -206,6 +206,9 @@ pub async fn create_voice_handler( Ok((job_token, _id)) => job_token, Err(err) => { warn!("New generic inference job creation DB error: {:?}", err); + if err.had_duplicate_idempotency_token() { + return Err(EnqueueCreateVoiceRequestError::BadInput("Duplicate idempotency token".to_string())); + } return Err(EnqueueCreateVoiceRequestError::ServerError); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue/enqueue_live_portrait_workflow_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue/enqueue_live_portrait_workflow_handler.rs index 69a8b0798f..e385e4e90d 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue/enqueue_live_portrait_workflow_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue/enqueue_live_portrait_workflow_handler.rs @@ -30,6 +30,7 @@ use tokens::tokens::media_files::MediaFileToken; use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session; use crate::configs::plans::plan_category::PlanCategory; +use crate::http_server::endpoints::workflows::enqueue::vst_common::vst_error::VstError; use crate::http_server::headers::get_routing_tag_header::get_routing_tag_header; use crate::http_server::headers::has_debug_header::has_debug_header; use crate::http_server::requests::get_request_domain_branding::{DomainBranding, get_request_domain_branding}; @@ -284,6 +285,9 @@ pub async fn enqueue_live_portrait_workflow_handler( Ok((job_token, _id)) => job_token, Err(err) => { warn!("New generic inference job creation DB error: {:?}", err); + if err.had_duplicate_idempotency_token() { + return Err(EnqueueLivePortraitWorkflowError::BadInput("Duplicate idempotency token".to_string())); + } return Err(EnqueueLivePortraitWorkflowError::ServerError); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue/enqueue_studio_workflow_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue/enqueue_studio_workflow_handler.rs index 630119b0bc..3641cdfb83 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue/enqueue_studio_workflow_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue/enqueue_studio_workflow_handler.rs @@ -333,6 +333,9 @@ pub async fn enqueue_studio_workflow_handler( Ok((job_token, _id)) => job_token, Err(err) => { warn!("New generic inference job creation DB error: {:?}", err); + if err.had_duplicate_idempotency_token() { + return Err(VstError::BadInput("Duplicate idempotency token".to_string())); + } return Err(VstError::ServerError); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue/enqueue_video_style_transfer_workflow_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue/enqueue_video_style_transfer_workflow_handler.rs index bc540ec507..0847b10282 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue/enqueue_video_style_transfer_workflow_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue/enqueue_video_style_transfer_workflow_handler.rs @@ -348,6 +348,9 @@ pub async fn enqueue_video_style_transfer_workflow_handler( Ok((job_token, _id)) => job_token, Err(err) => { warn!("New generic inference job creation DB error: {:?}", err); + if err.had_duplicate_idempotency_token() { + return Err(VstError::BadInput("Duplicate idempotency token".to_string())); + } return Err(VstError::ServerError); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue_comfy_ui_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue_comfy_ui_handler.rs index ddfc55dcd9..2ff31ea6ae 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue_comfy_ui_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue_comfy_ui_handler.rs @@ -324,6 +324,9 @@ pub async fn enqueue_comfy_ui_handler( Ok((job_token, _id)) => job_token, Err(err) => { warn!("New generic inference job creation DB error: {:?}", err); + if err.had_duplicate_idempotency_token() { + return Err(EnqueueComfyError::BadInput("Duplicate idempotency token".to_string())); + } return Err(EnqueueComfyError::ServerError); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue_video_style_transfer_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue_video_style_transfer_handler.rs index 4881e37fe7..9c77d976c2 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue_video_style_transfer_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue_video_style_transfer_handler.rs @@ -477,6 +477,9 @@ pub async fn enqueue_video_style_transfer_handler( Ok((job_token, _id)) => job_token, Err(err) => { warn!("New generic inference job creation DB error: {:?}", err); + if err.had_duplicate_idempotency_token() { + return Err(EnqueueVideoStyleTransferError::BadInput("Duplicate idempotency token".to_string())); + } return Err(EnqueueVideoStyleTransferError::ServerError); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue_workflow_upload_request.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue_workflow_upload_request.rs index 4cce86eb8c..df55d7be31 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue_workflow_upload_request.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue_workflow_upload_request.rs @@ -299,6 +299,9 @@ pub async fn enqueue_workflow_upload_request( Ok((job_token, _id)) => job_token, Err(err) => { warn!("New generic inference job creation DB error: {:?}", err); + if err.had_duplicate_idempotency_token() { + return Err(EnqueueWorkFlowRequestError::BadInput("Duplicate idempotency token".to_string())); + } return Err(EnqueueWorkFlowRequestError::ServerError); } };