Mục lục
- Mục Tiêu Bài Học
sqlx::Error8+ Variants Tổng QuanPgDatabaseError+ SQLSTATE 5-Character Code- SQLSTATE Phổ Biến Shop API Cần Xử Lý
- Refactor
From<sqlx::Error> for AppErrorChi Tiết - Domain Error Wrap
sqlx::Error(B54 Continued) - Retry Strategy Cho
40001Serialization Failure - Verify Error Response Envelope
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu
sqlx::Errorvariants đầy đủ (8+ variant chính, role mỗi loại). - Hiểu
PgDatabaseErrorvà SQLSTATE 5-character code chuẩn Postgres. - Map SQLSTATE phổ biến:
23505unique,23503FK,23514check,40001serialization,23502not-null. - Refactor
From<sqlx::Error> for AppErrorchi tiết per-SQLSTATE qua helper tách logic. - Thêm AppError variant mới:
Conflict,ForeignKeyViolation,CheckViolation,ServiceUnavailable,SerializationFailure. - Pattern domain error wrap
sqlx::Error(B54OrderErrorcontinued). - Hiểu khi nào retry vs fail-fast:
40001retry,23505fail-fast.
sqlx::Error 8+ Variants Tổng Quan
sqlx::Error là enum non-exhaustive chứa mọi tình huống lỗi có thể xảy ra khi giao tiếp với database. sqlx 0.8 (workspace lock B51) có hơn 12 variant chính, mỗi variant đại diện 1 nhóm nguyên nhân khác nhau.
// Tóm tắt variant chính của sqlx::Error (sqlx 0.8)
pub enum Error {
Configuration(Box<dyn StdError + Send + Sync>), // config parse fail
Database(Box<dyn DatabaseError>), // error từ Postgres
Io(std::io::Error), // network/file IO
Tls(Box<dyn StdError + Send + Sync>), // TLS handshake fail
Protocol(String), // sai protocol Postgres
RowNotFound, // fetch_one không có row
TypeNotFound { type_name: String }, // Rust type không match
ColumnDecode { index: String, source: Box<...> },// decode 1 column fail
Decode(Box<dyn StdError + Send + Sync>), // generic decode fail
PoolTimedOut, // acquire pool timeout
PoolClosed, // pool đã close
WorkerCrashed, // sqlx worker crash
// ... non-exhaustive
}
Nhóm theo nguồn gốc giúp dễ map sang HTTP status:
- Database-side —
Database(chính, wrapPgDatabaseError),RowNotFound. Phía Postgres phản hồi cụ thể. - Connection-side —
Io,Tls,Protocol,PoolTimedOut,PoolClosed,WorkerCrashed. Lỗi tầng kết nối hoặc pool quản lý. - Codec-side —
TypeNotFound,ColumnDecode,Decode,Configuration. Lỗi parse / encode giữa Rust struct và Postgres type — thường là bug schema mismatch.
Bảng mapping sang status code Shop API:
sqlx::Error variant | Status code | AppError variant
-------------------------+-------------+-----------------------------
RowNotFound | 404 | NotFound
Database (SQLSTATE) | varies | Conflict / FK / Check / ...
Configuration / Tls | 500 | Internal
Io / Protocol | 500 | Internal
PoolTimedOut | 503 | ServiceUnavailable
PoolClosed | 500 | Internal
TypeNotFound / Decode | 500 | Internal (server bug)
ColumnDecode | 500 | Internal (schema mismatch)
WorkerCrashed | 500 | Internal
Quyết định lock Shop API: tập trung mapping chi tiết cho RowNotFound + Database (SQLSTATE) + PoolTimedOut (3 trường hợp chiếm >95% error production); phần còn lại default về Internal 500 + log chi tiết qua tracing::error! để debug.
PgDatabaseError + SQLSTATE 5-Character Code
Khi Postgres trả lỗi, sqlx bọc qua sqlx::Error::Database(Box<dyn DatabaseError>). Inner type cụ thể là sqlx::postgres::PgDatabaseError exposed metadata phong phú: SQLSTATE code 5 ký tự + tên constraint + tên table/column + message.
use sqlx::error::DatabaseError;
match err {
sqlx::Error::Database(db_err) => {
let code = db_err.code().unwrap_or_default(); // vd "23505"
let constraint = db_err.constraint().unwrap_or(""); // vd "products_slug_key"
let table = db_err.table().unwrap_or(""); // vd "products"
let message = db_err.message(); // human-readable
tracing::error!(
?code, ?constraint, ?table,
"postgres database error: {}", message
);
}
_ => {}
}
SQLSTATE là chuẩn quy ước trong SQL standard (lần đầu trong SQL-92), Postgres tuân theo: 5 ký tự code phân loại lỗi theo class + specific. Format XYZAB:
- 2 ký tự đầu (
XY) — class (general category). Vd23= Integrity Constraint Violation,40= Transaction Rollback,42= Syntax Error,53= Insufficient Resources. - 3 ký tự sau (
ZAB) — specific code trong class. Vd23505= Unique Violation (class 23 + specific 505),23503= Foreign Key Violation,23514= Check Violation.
Class phổ biến cần biết khi build REST API:
Class | Tên | Use case
------+--------------------------------------+----------------------------
00 | Successful Completion | (không phải error)
23 | Integrity Constraint Violation | UNIQUE/FK/CHECK/NOT NULL
40 | Transaction Rollback | serialization, deadlock
42 | Syntax Error or Access Rule | SQL sai cú pháp, permission
53 | Insufficient Resources | quá nhiều connection, disk full
57 | Operator Intervention | admin shutdown DB
58 | System Error (external to PG) | OS error
XX | Internal Error | bug Postgres
Reference đầy đủ ở Postgres documentation appendix A. Mỗi version Postgres thêm code mới nhưng giữ backward — code đã định nghĩa không bao giờ đổi nghĩa.
SQLSTATE Phổ Biến Shop API Cần Xử Lý
7 SQLSTATE Shop API thường gặp trong production. Mỗi code map sang AppError + HTTP status theo nguyên tắc lock từ B3.
23505 UNIQUE_VIOLATION — insert/update vi phạm UNIQUE constraint. Use case Shop API: tạo product với slug trùng (constraint products_slug_key), register email trùng (users_email_key), webhook tạo lại payment với payment_intent_id trùng. Map: HTTP 409 Conflict. sqlx helper: db_err.is_unique_violation() trả true.
23503 FOREIGN_KEY_VIOLATION — insert FK trỏ tới row không tồn tại, hoặc delete row đang được FK reference. Use case Shop API: insert order_items với product_id không tồn tại (FK order_items_product_id_fkey với ON DELETE RESTRICT lock B54); xóa product đang trong order. Map: HTTP 409 Conflict (data integrity) hoặc 422 Unprocessable Entity (input không hợp lệ semantic) — lock Shop API chọn 409 vì semantic gần hơn với "không thể thực hiện vì conflict với state hiện tại". sqlx helper: db_err.is_foreign_key_violation().
23514 CHECK_VIOLATION — vi phạm CHECK constraint trên cột. Use case: insert products với stock < 0 hoặc price <= 0 (CHECK trong migration B51); insert orders với total <= 0 (CHECK lock B54). Map: HTTP 422 Unprocessable Entity — input parse được nhưng vi phạm business rule. Đây là defense in depth — validation tầng app (B41 ValidatedJson) thường bắt trước, nhưng CHECK ở DB là lưới an toàn cuối. sqlx helper: db_err.is_check_violation().
23502 NOT_NULL_VIOLATION — insert/update field NOT NULL với giá trị NULL. Use case: thiếu field bắt buộc. Map: HTTP 422. Đây gần như luôn là server bug — validation tầng DTO phải bắt trước qua #[validate(length(min = 1))]; nếu reach DB nghĩa là handler quên check.
40001 SERIALIZATION_FAILURE — 2 transaction concurrent ghi cùng row ở isolation SERIALIZABLE, Postgres detect conflict và abort 1 transaction. Use case: hiếm trong Shop API hiện tại vì lock B54 dùng READ COMMITTED default, nhưng có thể xảy ra khi nâng isolation cho report. Map: retry rồi mới fail (Bước 7) — đây là transient error.
40P01 DEADLOCK_DETECTED — 2 transaction lock chéo nhau (T1 giữ row A chờ row B, T2 giữ row B chờ row A). Postgres detect cycle và abort 1 trong 2. Use case: 2 order cùng update 2 product theo thứ tự khác nhau. Map: retry hoặc fail 503. Tránh bằng cách lock row theo thứ tự nhất quán (vd luôn lock theo product_id ASC).
53300 TOO_MANY_CONNECTIONS — Postgres đạt giới hạn max_connections (default 100). Use case: app scale quá nhanh, sự cố leak connection. Map: HTTP 503 Service Unavailable + Retry-After header.
Refactor From<sqlx::Error> for AppError Chi Tiết
B52 đã có impl From<sqlx::Error> for AppError với 3 case cơ bản (RowNotFound → NotFound, unique violation → Conflict, default → Internal). B55 mở rộng thành full mapping qua helper map_db_error tách logic cho Database variant.
Trước hết, mở rộng AppError với 4 variant mới:
// File: crates/shop-common/src/error.rs
use sqlx::error::DatabaseError;
#[derive(Debug, thiserror::Error)]
pub enum AppError {
// ... 14 variant cũ (B10 + B41 + B48) giữ nguyên
#[error("conflict: {0}")]
Conflict(String),
#[error("foreign key violation: {constraint} (table: {table})")]
ForeignKeyViolation { constraint: String, table: String },
#[error("check constraint violation: {0}")]
CheckViolation(String),
#[error("service temporarily unavailable")]
ServiceUnavailable,
#[error("transaction serialization failure (retry recommended)")]
SerializationFailure,
}
Lưu ý: Conflict đã có sẵn ở B16 nhưng dùng cho generic case (vd optimistic lock). B55 reuse cùng variant cho unique violation — gộp về 1 variant cho consistency, format string riêng để client phân biệt qua field code.
Refactor impl From<sqlx::Error> for AppError đầy đủ:
// File: crates/shop-common/src/error.rs
impl From<sqlx::Error> for AppError {
fn from(err: sqlx::Error) -> Self {
match err {
sqlx::Error::RowNotFound => {
AppError::NotFound("row not found".into())
}
sqlx::Error::Database(db_err) => map_db_error(&*db_err),
sqlx::Error::PoolTimedOut => AppError::ServiceUnavailable,
sqlx::Error::PoolClosed => {
tracing::error!("database pool closed");
AppError::Internal(anyhow::anyhow!("database unavailable"))
}
sqlx::Error::Io(e) => {
tracing::error!(?e, "sqlx IO error");
AppError::Internal(anyhow::anyhow!("database io error"))
}
sqlx::Error::Tls(e) => {
tracing::error!(?e, "sqlx TLS error");
AppError::Internal(anyhow::anyhow!("database tls error"))
}
_ => {
tracing::error!(?err, "unknown sqlx error");
AppError::Internal(anyhow::anyhow!("database error"))
}
}
}
}
Helper map_db_error tách logic SQLSTATE — match per-code, dễ thêm code mới sau này:
// File: crates/shop-common/src/error.rs
fn map_db_error(db_err: &dyn DatabaseError) -> AppError {
let code = db_err.code().unwrap_or_default();
let constraint = db_err.constraint().unwrap_or("");
let table = db_err.table().unwrap_or("");
match code.as_ref() {
"23505" => AppError::Conflict(format!(
"unique violation: {} on table {}",
constraint, table
)),
"23503" => AppError::ForeignKeyViolation {
constraint: constraint.to_string(),
table: table.to_string(),
},
"23514" => AppError::CheckViolation(constraint.to_string()),
"23502" => AppError::CheckViolation(format!(
"not-null violation on {}",
constraint
)),
"40001" | "40P01" => AppError::SerializationFailure,
"53300" => AppError::ServiceUnavailable,
_ => {
tracing::error!(
?code, ?constraint, ?table,
"unhandled postgres error: {}", db_err.message()
);
AppError::Internal(anyhow::anyhow!(
"database error: {}", code
))
}
}
}
Update impl IntoResponse for AppError — thêm match arm cho 4 variant mới. Pattern envelope lock từ B16 + detail object cho FK/Check (B48 continued):
// File: crates/shop-common/src/error.rs
impl IntoResponse for AppError {
fn into_response(self) -> Response {
match self {
// ... arm cũ giữ nguyên
AppError::Conflict(msg) => {
let body = json!({
"error": msg,
"code": "CONFLICT",
"request_id": null,
});
(StatusCode::CONFLICT, Json(body)).into_response()
}
AppError::ForeignKeyViolation { constraint, table } => {
let body = json!({
"error": "foreign key violation",
"code": "FOREIGN_KEY_VIOLATION",
"request_id": null,
"detail": {
"constraint": constraint,
"table": table,
}
});
(StatusCode::CONFLICT, Json(body)).into_response()
}
AppError::CheckViolation(constraint) => {
let body = json!({
"error": "check constraint violation",
"code": "CHECK_VIOLATION",
"request_id": null,
"detail": { "constraint": constraint }
});
(StatusCode::UNPROCESSABLE_ENTITY, Json(body)).into_response()
}
AppError::ServiceUnavailable => {
let body = json!({
"error": "service temporarily unavailable",
"code": "SERVICE_UNAVAILABLE",
"request_id": null,
});
let mut response = (StatusCode::SERVICE_UNAVAILABLE, Json(body)).into_response();
response.headers_mut().insert(
header::RETRY_AFTER,
HeaderValue::from_static("30"),
);
response
}
AppError::SerializationFailure => {
let body = json!({
"error": "transaction conflict, please retry",
"code": "SERIALIZATION_FAILURE",
"request_id": null,
});
(StatusCode::CONFLICT, Json(body)).into_response()
}
}
}
}
Helper internal status_code() và code() cũng phải thêm 5 arm tương ứng (compiler enforce qua exhaustive match). Sau B55, AppError có 19 variant tổng cộng (14 cũ + 5 mới).
Domain Error Wrap sqlx::Error (B54 Continued)
B54 đã lock pattern domain error OrderError với #[from] sqlx::Error:
// File: crates/shop-db/src/orders.rs (lock B54)
#[derive(Debug, thiserror::Error)]
pub enum OrderError {
#[error(transparent)]
Sqlx(#[from] sqlx::Error),
#[error("product {0} not found")]
ProductNotFound(i64),
#[error("insufficient stock for product {product_id}")]
InsufficientStock {
product_id: i64,
requested: i32,
available: i32,
},
}
Câu hỏi: handler trong shop-api nhận Result<OrderRow, OrderError> — làm sao convert sang AppError để return response qua IntoResponse? Có 2 cách.
Cách 1 — impl From<OrderError> for AppError thủ công, match từng variant:
// File: crates/shop-api/src/responses/error_map.rs (B66 sẽ tạo)
impl From<OrderError> for AppError {
fn from(err: OrderError) -> Self {
match err {
OrderError::Sqlx(e) => AppError::from(e), // tận dụng From<sqlx::Error>
OrderError::ProductNotFound(id) => {
AppError::NotFound(format!("product {} not found", id))
}
OrderError::InsufficientStock { product_id, requested, available } => {
AppError::Validation(format!(
"product {} stock {} < requested {}",
product_id, available, requested
))
}
}
}
}
Cách 2 — dùng #[from] cascade tự convert mọi variant:
// KHÔNG dùng cho Shop API
#[derive(Debug, thiserror::Error)]
pub enum AppError {
// ...
#[error(transparent)]
Order(#[from] OrderError),
}
So sánh:
- Cách 1 control rõ ràng từng business error semantic.
InsufficientStockmap 422 vì user gửi quantity vượt stock — input không hợp lệ;ProductNotFoundmap 404 vì resource đúng nghĩa không tồn tại. Verbose hơn 5-10 dòng cho mỗi domain error enum. - Cách 2 ngắn gọn nhưng AppError có 1 variant generic
Orderkhông thể map semantic — phải custom IntoResponse trong AppError match nested enum (anti-pattern, vi phạm single responsibility). Mất luôn khả năng convertUserError,CartErrorđộc lập.
Lock pattern Shop API: Cách 1 explicit match. Mọi service domain (B66 OrderError, B104 UserError, B106 CartError) đều có impl From<XxxError> for AppError riêng. Vị trí file: crates/shop-api/src/responses/error_map.rs (sẽ tạo ở B66 khi handler đầu tiên cần dùng).
Ưu điểm phụ: pattern này tách boundary rõ — shop-common::error::AppError không depend shop-db (giữ shop-common pure utility), impl From đặt ở shop-api binary là nơi có cả hai dependency.
Retry Strategy Cho 40001 Serialization Failure
SERIALIZATION_FAILURE (40001) và DEADLOCK_DETECTED (40P01) là transient error — retry thường thành công vì 2 transaction tranh chấp lần 2 thường không đụng nhau nữa. Pattern industry: retry với exponential backoff, max 3 attempts.
Tạo module mới crates/shop-common/src/retry.rs:
// File: crates/shop-common/src/retry.rs
use std::future::Future;
use std::time::Duration;
use tokio::time::sleep;
use crate::error::AppError;
pub async fn with_retry<F, Fut, T>(
mut operation: F,
max_attempts: u32,
) -> Result<T, AppError>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<T, AppError>>,
{
let mut attempt = 0;
loop {
attempt += 1;
match operation().await {
Ok(result) => return Ok(result),
Err(AppError::SerializationFailure) if attempt < max_attempts => {
let backoff_ms = 10u64 * 2u64.pow(attempt - 1); // 10, 20, 40
tracing::warn!(
attempt, max_attempts, backoff_ms,
"serialization failure, retrying"
);
sleep(Duration::from_millis(backoff_ms)).await;
}
Err(e) => return Err(e),
}
}
}
Update crates/shop-common/src/lib.rs re-export:
// File: crates/shop-common/src/lib.rs
pub mod config;
pub mod dto;
pub mod error;
pub mod headers;
pub mod pagination;
pub mod retry; // NEW B55
pub mod telemetry;
pub use retry::with_retry;
Use case ở B66 khi viết handler create order:
// Preview B66 — handler POST /api/v1/orders
pub async fn create_order_handler(
State(state): State<AppState>,
ValidatedJson(dto): ValidatedJson<CreateOrderDto>,
) -> AppResult<Created<OrderResponseDto>> {
let pool = state.db.clone();
let order = with_retry(
|| {
let pool = pool.clone();
let dto = dto.clone();
async move {
shop_db::orders::create_order_atomic(
&pool,
dto.user_id,
dto.items,
&dto.payment_type,
dto.payment_payload,
)
.await
.map_err(AppError::from)
}
},
3, // max 3 attempts
)
.await?;
Ok(Created {
location: format!("/api/v1/orders/{}", order.id),
data: order.into(),
})
}
Quyết định lock Shop API:
- Retry MAX 3 attempts cho transient error (
40001,40P01) — sau 3 lần fail thì trảSerializationFailurecho client. - Backoff exponential: 10ms → 20ms → 40ms. Tổng worst-case latency thêm 70ms — chấp nhận được cho UX.
- Fail-fast cho permanent error (
23505,23503,23514) — KHÔNG retry. Retry không giải quyết gì (slug vẫn trùng, FK vẫn sai) chỉ tốn DB cycle. - Helper
with_retryreusable cho mọi service tương lai (B66 create_order, B98 admin bulk operation).
Verify Error Response Envelope
Build + run server:
cargo build -p shop-common
cargo run -p shop-api
Test 1 — Unique violation 23505 → 409 Conflict. Tạo product 2 lần với cùng slug:
curl -X POST http://localhost:3000/api/v1/products \
-H 'Content-Type: application/json' \
-d '{"name":"iPhone 15","slug":"iphone-15","price":"25000000.00","stock":10}'
# Lần thứ 2 (slug trùng) → 409 Conflict
# {
# "error": "unique violation: products_slug_key on table products",
# "code": "CONFLICT",
# "request_id": "550e8400-e29b-41d4-a716-446655440000"
# }
Test 2 — Foreign key violation 23503 → 409 ForeignKeyViolation. Insert order_items với product_id không tồn tại (qua psql trực tiếp vì HTTP endpoint chưa có ở B55):
docker compose exec postgres psql -U shop -d shop_dev -c \
"INSERT INTO orders (user_id, total) VALUES (1, 100) RETURNING id;"
# id = 1
docker compose exec postgres psql -U shop -d shop_dev -c \
"INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES (1, 999, 1, 1000);"
# ERROR: insert or update on table "order_items" violates foreign key
# constraint "order_items_product_id_fkey"
# DETAIL: Key (product_id)=(999) is not present in table "products".
Khi gọi qua handler (B66), error này sẽ map qua map_db_error → AppError::ForeignKeyViolation → response:
# {
# "error": "foreign key violation",
# "code": "FOREIGN_KEY_VIOLATION",
# "request_id": "...",
# "detail": {
# "constraint": "order_items_product_id_fkey",
# "table": "order_items"
# }
# }
Test 3 — Check violation 23514 → 422 CheckViolation. Insert product với price âm:
docker compose exec postgres psql -U shop -d shop_dev -c \
"INSERT INTO products (name, slug, price, stock)
VALUES ('X', 'x-bad', -100, 1);"
# ERROR: new row for relation "products" violates check constraint
# "products_price_check"
Response envelope dự kiến:
# 422 Unprocessable Entity
# {
# "error": "check constraint violation",
# "code": "CHECK_VIOLATION",
# "request_id": "...",
# "detail": { "constraint": "products_price_check" }
# }
Test 4 — RowNotFound → 404 NotFound. GET product không tồn tại:
curl http://localhost:3000/api/v1/products/non-existing-slug
# 404 Not Found
# {
# "error": "product non-existing-slug not found",
# "code": "NOT_FOUND",
# "request_id": "..."
# }
Pattern lock Shop API: client thấy code rõ ràng → UI hiển thị message phù hợp + i18n key. Field detail structured cho debug (admin dashboard) hoặc client UI highlight constraint vi phạm.
Suggested commit: B55: sqlx error mapping SQLSTATE + 4 AppError variant + retry helper.
Tổng Kết
sqlx::Error8+ variant chính:Database(quaPgDatabaseError),RowNotFound,PoolTimedOut,PoolClosed,Io,Tls,Configuration,Protocol,TypeNotFound,ColumnDecode,Decode,WorkerCrashed.- SQLSTATE 5-character code Postgres chuẩn ANSI — class 2 ký tự đầu, specific 3 ký tự sau.
23505UNIQUE → 409 Conflict.23503FK → 409 ForeignKeyViolation (detail objectconstraint+table).23514CHECK → 422 CheckViolation (detail objectconstraint).23502NOT NULL → 422 (gộp dưới CheckViolation, validation tầng app phải bắt trước).40001SERIALIZATION → retry rồiSerializationFailure409.40P01DEADLOCK → retry hoặc 503.53300TOO_MANY_CONNECTIONS → 503 ServiceUnavailable.- 5 AppError variant mới:
Conflict(reuse từ B16),ForeignKeyViolation,CheckViolation,ServiceUnavailable,SerializationFailure. TổngAppErrorsau B55: 19 variant. map_db_errorhelper function lock pattern — refactorFrom<sqlx::Error>impl, tách logic SQLSTATE.- Domain error wrap
sqlx::ErrorvớiOrderError::Sqlx(#[from] sqlx::Error)+ implFrom<OrderError> for AppErrorexplicit match (Cách 1) lock pattern Shop API. - Retry strategy cho transient (
40001,40P01): max 3 attempts, exponential backoff 10/20/40ms qua helperwith_retry. - Fail-fast cho permanent (
23505,23503,23514): KHÔNG retry — retry không giải quyết được. - Logging chi tiết qua
tracing::error!với?code?constraint?tabledebug dễ. - Envelope
detailobject pattern B48 continued cho FK + Check violation. - File path lock: extend
crates/shop-common/src/error.rs+ NEWcrates/shop-common/src/retry.rs+ updatecrates/shop-common/src/lib.rsre-export. - Foundation cho B56 (connection pool tuning sâu), B57 (pool benchmark), B66 (POST /orders với
with_retry).
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
sqlx::Error8+ variant nào? Mỗi variant map sang status code nào trong Shop API?- SQLSTATE
23505vs23503— use case khác nhau ra sao? Cho ví dụ cụ thể trong Shop API. - Tại sao
23514map 422 (Validation) thay 409 (Conflict)? Semantic difference? - Retry strategy cho
40001SERIALIZATION_FAILURE — backoff bao nhiêu? Tại sao KHÔNG retry23505? - Domain error wrap
sqlx::Error— 2 cách convert sang AppError, pros/cons mỗi cách?
Đáp án
sqlx::Error8+ variant + mapping: sqlx 0.8 có hơn 12 variant chính. Database-side:Database(Box<dyn DatabaseError>)wrapPgDatabaseError→ mapping per-SQLSTATE quamap_db_error(varies status);RowNotFound→AppError::NotFound404 (handlerfetch_onekhông có row). Connection-side:PoolTimedOut→AppError::ServiceUnavailable503 (pool exhausted);PoolClosed→AppError::Internal500 + log error (pool đã shut down);Io(std::io::Error)→Internal500 + log (network/file IO error);Tls→Internal500 (TLS handshake fail);Protocol(String)→Internal500 (sai protocol Postgres);WorkerCrashed→Internal500. Codec-side:TypeNotFound { type_name }→Internal500 (Rust type không match Postgres — server bug schema);ColumnDecode { index, source }→Internal500 (decode 1 column fail — schema mismatch);Decode(Box<dyn Error>)→Internal500 (generic decode fail);Configuration→Internal500 (config parse fail lúc startup). Lock Shop API: tập trung mapping chi tiếtRowNotFound+Database (SQLSTATE)+PoolTimedOut(3 case chiếm >95% production), phần còn lại defaultInternal 500+ logtracing::error!chi tiết để debug; KHÔNG leak SQL/schema chi tiết ra client.23505vs23503use case:23505UNIQUE_VIOLATION — insert/update vi phạm UNIQUE constraint, key value trùng row đã có. Use case Shop API: (a)POST /api/v1/productsvớislug = "iphone-15"đã tồn tại → constraintproducts_slug_keyfail; (b)POST /api/v1/auth/registervớiemailtrùng → constraintusers_email_key; (c) Stripe webhook retry tạo lại payment vớipayment_intent_idtrùng → constraintpayments_payment_intent_id_key. Helperdb_err.is_unique_violation()trảtrue. Map: 409 Conflict.23503FOREIGN_KEY_VIOLATION — insert FK trỏ tới row không tồn tại, hoặc delete row đang được FK reference (chỉ vớiON DELETE RESTRICThoặcNO ACTION). Use case Shop API: (a)create_order_atomicinsertorder_itemsvớiproduct_id = 999không tồn tại → constraintorder_items_product_id_fkey; (b)DELETE /api/v1/admin/products/:idmà product đang trongorder_itemscòn → constraint cùng tên doON DELETE RESTRICT(lock B54); (c) insertpaymentsvớiorder_idtrỏorders.idkhông tồn tại → constraintpayments_order_id_fkey. Helperdb_err.is_foreign_key_violation(). Map: 409 ForeignKeyViolation (lock Shop API) với detail object{constraint, table}envelope. Khác biệt: 23505 = "tài liệu này đã tồn tại" (duplicate key), 23503 = "reference tới resource không hợp lệ" (broken link); cả 2 đều 409 nhưngcodekhác để client phân biệt UI message.- 23514 → 422 thay 409 vì semantic difference: HTTP 409 Conflict nghĩa là "request không thể hoàn thành vì conflict với state hiện tại của resource" (RFC 9110 mục 15.5.10) — typical use case duplicate key, optimistic lock fail, FK conflict. HTTP 422 Unprocessable Entity (RFC 9110 mục 15.5.21) nghĩa là "server hiểu Content-Type và parse được body nhưng nội dung vi phạm semantic/business rule".
23514CHECK_VIOLATION là vi phạm CHECK constraint trên cột (vdproducts.stock CHECK (stock >= 0),orders.total CHECK (total > 0)) — input value sai semantic business rule (price âm, stock âm) chứ KHÔNG phải conflict với state hiện tại. Map 422 vì: (a) consistent vớiValidationFailedvalidator crate B41 (cùng nguyên nhân — input không hợp lệ business rule), (b) client UI handle giống nhau (highlight field sai + hiển thị message), (c) semantic chính xác — không có resource conflict, chỉ input không pass DB constraint. Lưu ý: validation tầng app (ValidatedJson<CreateProductDto>B41 với#[validate(range(min = 1))]) phải bắt trước; nếu reach DB nghĩa là frontend gửi giá trị edge case mà DTO rule miss — CHECK ở DB là lưới an toàn cuối (defense in depth). Helperdb_err.is_check_violation(). Lock Shop API:23514+23502NOT_NULL_VIOLATION đều gộp vềCheckViolation422 + detail object{constraint}envelope cho client biết constraint nào fail. - Retry
40001backoff + tại sao KHÔNG retry23505:40001SERIALIZATION_FAILURE và40P01DEADLOCK_DETECTED là transient error — nguyên nhân tạm thời (2 transaction concurrent ghi cùng row ở SERIALIZABLE isolation hoặc deadlock cycle), retry lần 2 thường thành công vì 2 transaction không còn cùng lúc tranh chấp. Pattern industry: exponential backoff tránh thundering herd — wait time tăng gấp đôi mỗi attempt cho DB thời gian giải quyết contention. Lock Shop API: max 3 attempts, backoff 10ms → 20ms → 40ms qua công thức10 * 2^(attempt-1)ms. Tổng worst-case latency thêm 70ms — chấp nhận cho UX (user thấy delay nhỏ vẫn tốt hơn fail). Sau 3 attempts trảSerializationFailure409 cho client biết "transaction conflict, please retry". Helperwith_retrymoduleshop-common::retryreusable mọi service. KHÔNG retry23505vì đây là permanent error — slug trùng vẫn trùng kể cả retry 100 lần, FK sai vẫn sai, check fail vẫn fail. Retry không giải quyết được nguyên nhân — chỉ tốn DB cycle + tăng latency vô ích. Fail-fast trả 409 ngay cho client biết "data conflict, please change input". Cùng logic áp dụng cho23503,23514,23502— mọi class 23 integrity constraint violation đều fail-fast. Lock Shop API: helperwith_retrychỉ retry khimatches!(err, AppError::SerializationFailure); mọi error khác bubble lên ngay. - Domain error → AppError 2 cách pros/cons: Cách 1 — impl
From<OrderError> for AppErrorthủ công match từng variant. Code:impl From<OrderError> for AppError { fn from(err: OrderError) -> Self { match err { OrderError::Sqlx(e) => AppError::from(e), OrderError::ProductNotFound(id) => AppError::NotFound(format!("product {} not found", id)), OrderError::InsufficientStock { product_id, requested, available } => AppError::Validation(format!("product {} stock {} < requested {}", product_id, available, requested)) } } }. Pros: (a) control rõ ràng từng business error semantic —InsufficientStockmap 422 Validation vì user gửi quantity vượt stock (input không hợp lệ),ProductNotFoundmap 404 NotFound vì resource đúng nghĩa không tồn tại; (b) message format tùy chỉnh có context (product id, stock value) — client/log đọc rõ; (c) thay đổi business logic chỉ sửa 1 file mapping, không động đến enum domain hay AppError; (d) test dễ — match exhaustive compiler enforce thêm variant mới phải update mapping. Cons: verbose 5-10 dòng cho mỗi domain enum, repeat code khi nhiều service (Order/Cart/User/Payment). Cách 2 — thiserror#[from]cascade tự convert. Code:#[derive(Debug, thiserror::Error)] enum AppError { #[error(transparent)] Order(#[from] OrderError), #[error(transparent)] User(#[from] UserError), ... }. Pros: ngắn gọn, không viết match arm thủ công,?operator tự convert. Cons: (a) AppError có nhiều variant genericOrder/User/Cartmất khả năng map semantic — phải customIntoResponsetrong AppError match nested enum (anti-pattern, vi phạm single responsibility); (b) shop-common::error phải depend mọi crate domain (shop-db) — vi phạm dependency direction (common là leaf, không nên depend module cao tầng); (c) khó debug — error message nested khó đọc, stack trace không rõ; (d) test khó — phải xây dựng OrderError → kiểm tra outer match nested. Lock pattern Shop API: Cách 1 explicit match cho mọi service domain (B66 OrderError, B104 UserError, B106 CartError, B71 PaymentError). Vị trí file:crates/shop-api/src/responses/error_map.rs(B66 tạo) — nơi có cả AppError (shop-common) và domain error (shop-db) trong dependency.
Bài Tiếp Theo
Bài 56: sqlx Pool Configuration Sâu — chi tiết PgPoolOptions: max_connections vs Postgres limit, acquire_timeout vs request timeout, idle_timeout life cycle, before_acquire/after_connect hook, statement cache, áp Shop API multi-env pool config (dev/staging/prod).
