You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rust/SeaORM:唯一约束冲突的重试逻辑实现问题排查

Rust + SeaORM 患者创建重试逻辑问题排查与解决

背景与问题

我正在用Rust结合SeaORM实现create_patient函数,患者的patient_ic字段有唯一性约束,需求是出现唯一约束冲突时最多重试5次并生成新的patient_ic。

我编写了检测重复键错误的辅助函数:

pub fn check_if_is_duplicate_key_from_data_base<T>(
    attempts: &mut usize,
    result: Result<T, DbErr>,
) -> Option<Result<T, CustomError>> {
    match result {
        Ok(value) => Some(Ok(value)),
        Err(DbErr::Exec(e)) => {
            if e.to_string().contains("duplicate key value violates unique constraint") || e.to_string().contains("23505") {
                *attempts += 1;
                None // 表示需要重试
            } else {
                Some(Err(CustomError::from(DbErr::Exec(e)))) // 不可重试的错误
            }
        }
        Err(e) => Some(Err(CustomError::from(e))), // 其他错误
    }
}

create_patient函数实现:

// ... (导入和虚拟结构体已省略)

pub async fn create_patient(
    &self,
    patient_data: Option<PatientRequestBody>,
) -> Result<PatientWithPerson, CustomError> {
    let mut payload = patient_data.ok_or_else(|| CustomError::new(400, "Missing patient data".to_string()))?;
    let now_utc = Utc::now();
    let mut attempts = 0;
    const MAX_ATTEMPTS: usize = 5;

    // 示例虚拟Person数据
    let person = person::Model { id: Uuid::new_v4(), first_name: "Dummy".to_string(), last_name: "Person".to_string(), date_of_birth: Utc::now().naive_utc(), gender: "Other".to_string(), phone: None, email: None, address: None, nationality: None, marital_status: None, photo_url: None, created_at: Utc::now(), updated_at: Utc::now() };

    // 初始检查是否存在已提供的patient_ic
    if let Some(ref ic) = payload.patient_ic {
        if let Ok(Some(existing_patient)) = self.find_by_field("patient_ic", ic).await {
            return Ok(PatientWithPerson { patient: existing_patient, person });
        }
    }

    loop {
        if attempts >= MAX_ATTEMPTS {
            return Err(CustomError::new(500, "Failed to generate a unique patient IC after multiple attempts.".to_string()));
        }

        if payload.patient_ic.is_none() || attempts > 0 {
            payload.patient_ic = Some(Self::generate_random_patient_ic());
        }

        let active_model = patient::ActiveModel {
            id: Set(person.id),
            patient_ic: Set(payload.patient_ic.clone()),
            created_at: Set(now_utc), updated_at: Set(now_utc),
            hospital_id: Set(payload.hospital_id), emergency_contact: Set(payload.emergency_contact.clone()),
            blood_type: Set(payload.blood_type), allergies: Set(payload.allergies.clone()),
            medical_history: Set(payload.medical_history.clone()),
        };

        let insert_result = active_model.insert(&self.conn).await;

        match insert_result {
            Ok(patient_model) => return Ok(PatientWithPerson { patient: patient_model, person }),
            Err(db_err) => {
                if let Some(final_outcome) = Self::check_if_is_duplicate_key_from_data_base(&mut attempts, Err(db_err)) {
                    return final_outcome.map_err(|e| e); // 传播不可重试的错误
                } else {
                    // 是重复键错误,继续循环重试
                }
            }
        }
    }
}

当前问题:当出现patient_ic重复错误(DbErr::Exec包含"23505"或"duplicate key")时,重试循环未正确生成新的patient_ic并重试插入,反而常出现"Patient not found"或初始数据库错误,循环未按预期执行。

疑问

  1. check_if_is_duplicate_key_from_data_base返回的Option<Result<T, CustomError>>在create_patient中是否正确用于触发重试(None)或传播错误(Some(Err))?
  2. 在SeaORM/Rust中,除字符串匹配或特定PostgreSQL错误码外,是否有更可靠的方式检测唯一约束冲突?
  3. create_patient的循环中是否存在微妙的控制流问题导致重试失败?

解答

1. 辅助函数的返回值使用是否正确?

逻辑方向是对的,但存在计数时机的瑕疵:

  • 返回None触发重试、Some(Err)传播错误的逻辑是正确的,但辅助函数里提前递增attempts会导致重试次数计数不准。比如第一次失败后attempts直接变为1,后续循环的次数判断会提前触发终止,实际重试次数达不到预期的5次。

建议把*attempts +=1从辅助函数移到create_patient的重试分支中,确保计数和重试动作一一对应。

2. 更可靠的唯一约束冲突检测方式

可以直接匹配数据库驱动的标准错误类型,避免字符串匹配的脆弱性:
以PostgreSQL为例,引入驱动的错误状态码枚举,直接匹配唯一约束冲突的标准状态:

use postgres::error::SqlState;

pub fn is_unique_violation(err: &DbErr) -> bool {
    match err {
        DbErr::Exec(e) => {
            e.downcast_ref::<postgres::Error>()
                .map(|pg_err| pg_err.code() == Some(&SqlState::UNIQUE_VIOLATION))
                .unwrap_or(false)
        }
        _ => false,
    }
}

这种方式直接对应SQL标准的23505状态码,不会因为数据库本地化错误信息、版本更新导致匹配失效。

3. 循环中的控制流问题

存在三个关键问题:

问题1:尝试次数递增时机错误

当前辅助函数在检测到错误时就递增attempts,导致循环开头的次数判断提前生效。比如第5次失败后attempts变为5,下一次循环直接返回错误,实际只重试了4次。

问题2:person.id重复使用

每次循环都复用同一个person.id插入患者表,如果患者表的id是主键/唯一键,第一次插入失败后,后续重试会触发id重复的约束错误,被误判为非重试错误返回,导致出现"Patient not found"类问题。

问题3:初始检查的竞态条件

初始检查patient_ic存在性后,其他进程可能在插入前抢先插入同个IC,触发约束错误进入重试,但当前逻辑没问题,属于正常的分布式场景,只需确保重试逻辑正确即可。

修正方案:

pub async fn create_patient(
    &self,
    patient_data: Option<PatientRequestBody>,
) -> Result<PatientWithPerson, CustomError> {
    let mut payload = patient_data.ok_or_else(|| CustomError::new(400, "Missing patient data".to_string()))?;
    let now_utc = Utc::now();
    let mut attempts = 0;
    const MAX_ATTEMPTS: usize = 5;

    // 初始检查已提供的patient_ic
    if let Some(ref ic) = payload.patient_ic {
        if let Ok(Some(existing_patient)) = self.find_by_field("patient_ic", ic).await {
            let person = person::Model { id: Uuid::new_v4(), first_name: "Dummy".to_string(), last_name: "Person".to_string(), date_of_birth: Utc::now().naive_utc(), gender: "Other".to_string(), phone: None, email: None, address: None, nationality: None, marital_status: None, photo_url: None, created_at: Utc::now(), updated_at: Utc::now() };
            return Ok(PatientWithPerson { patient: existing_patient, person });
        }
    }

    loop {
        if attempts >= MAX_ATTEMPTS {
            return Err(CustomError::new(500, "Failed to generate a unique patient IC after multiple attempts.".to_string()));
        }

        // 生成新IC:首次无IC或重试时更新
        if payload.patient_ic.is_none() || attempts > 0 {
            payload.patient_ic = Some(Self::generate_random_patient_ic());
        }

        // 每次重试生成新的person实例(如果patient.id是主键)
        let person = person::Model { id: Uuid::new_v4(), first_name: "Dummy".to_string(), last_name: "Person".to_string(), date_of_birth: Utc::now().naive_utc(), gender: "Other".to_string(), phone: None, email: None, address: None, nationality: None, marital_status: None, photo_url: None, created_at: Utc::now(), updated_at: Utc::now() };

        let active_model = patient::ActiveModel {
            id: Set(person.id),
            patient_ic: Set(payload.patient_ic.clone().unwrap()),
            created_at: Set(now_utc), updated_at: Set(now_utc),
            hospital_id: Set(payload.hospital_id), emergency_contact: Set(payload.emergency_contact.clone()),
            blood_type: Set(payload.blood_type), allergies: Set(payload.allergies.clone()),
            medical_history: Set(payload.medical_history.clone()),
        };

        match active_model.insert(&self.conn).await {
            Ok(patient_model) => return Ok(PatientWithPerson { patient: patient_model, person }),
            Err(db_err) => {
                if is_unique_violation(&db_err) {
                    attempts += 1; // 仅在确定是重复错误时递增次数
                } else {
                    return Err(CustomError::from(db_err));
                }
            }
        }
    }
}

内容的提问来源于stack exchange,提问作者NsdHSO

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 20:23:10