Mục lục
- Mục Tiêu Bài Học
- 3 Loại Timeout HTTP API
- Cài + Wire tower-http TimeoutLayer
- AppError::Timeout Variant + 504 Response
- Wire Per-Route Timeout
- Pitfall Background Task — Tokio Cancel Behavior
- Drop Guard Pattern — Transaction Cleanup
- Verify End-To-End + Test Timeout
- 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 3 loại timeout HTTP API: request, response, idle.
- Áp dụng
tower-http::TimeoutLayerper-route thay global. - Implement timeout config 3 class: default 5s + import 30s + upload 60s + webhook 10s.
- Response 504 Gateway Timeout với
AppError::Timeoutvariant mới (variant 22, bump 21 → 22). - Hiểu pitfall background task — tokio future cancel khi timeout, partial state risk side-effect external.
- Pattern drop guard cleanup cho transaction rollback khi cancel, emit metric monitor.
- Multi-env timeout strategy (Local 30s debug, Production strict 5s).
- Quy tắc relationship server < proxy < load balancer chain timeout đúng thứ tự.
3 Loại Timeout HTTP API
Trước khi viết code, phân biệt 3 loại timeout vận hành ở 3 tầng khác nhau trong stack HTTP. Nhầm tầng đặt timeout = bug khó debug (timeout trên proxy 60s nhưng server timeout 5s sẽ trả 504 trong khi proxy vẫn chờ — client thấy lỗi kỳ lạ).
Request timeout — server chờ tối đa N giây hoàn thành handler:
- Use case: chống slow handler block tài nguyên (1 task ngốn thread 10 phút = pool DB / tokio executor cạn).
- Vị trí: ngay trong process axum, áp dụng qua
tower-http::TimeoutLayer. - Default Shop API: 5s — phù hợp 95% endpoint JSON CRUD (P95 healthy < 100ms, 5s gấp 50 lần đủ buffer outlier).
- Khi timeout: server cắt connection + response 504, tokio future bị drop (xem step 6 pitfall).
Response timeout — client/proxy chờ tối đa N giây nhận response:
- Use case: cấu hình ở NGINX
proxy_read_timeout, HAProxytimeout server, AWS ALBidle_timeout. - Vị trí: tầng proxy/load balancer phía trước axum.
- Quy tắc cross: server request timeout MUST nhỏ hơn proxy response timeout — nếu proxy timeout trước server, proxy trả 504 còn server tiếp tục xử lý lãng phí tài nguyên.
- Shop API focus B82: request timeout phía server; proxy/lb timeout config sẽ deploy ở G18.
Idle/Keep-alive timeout — TCP connection idle bao lâu trước close:
- Use case: cấu hình ở hyper backend qua
http2_keep_alive_interval/http2_keep_alive_timeout. - Vị trí: tầng HTTP server (hyper) hoặc client (reqwest pool).
- Tách biệt hoàn toàn request timeout — idle timeout đo thời gian KHÔNG có data flow trên connection, request timeout đo thời gian processing 1 request cụ thể.
- Shop API B82 skip — preview G18 deploy.
Lock decision Shop API B82: focus request timeout per-route; response timeout + idle timeout deploy phase G18 khi setup NGINX reverse proxy + ALB. Quy tắc relationship lock vĩnh viễn:
server_request_timeout < proxy_read_timeout < load_balancer_timeout
5s < 30s < 60s (AWS ALB default)
Margin gấp 6x giữa mỗi tầng đủ buffer cho retry + network jitter + cold start container. Nếu đảo thứ tự (server 30s, proxy 5s) → proxy luôn timeout trước, server xử lý xong vô ích → tài nguyên lãng phí + client thấy 504 sai nguyên nhân (proxy báo timeout chứ không phải server).
Một số endpoint Shop API cần timeout dài hơn default 5s do tính chất workload — đó là lý do bài này cấu hình per-route thay vì global một con số duy nhất.
Cài + Wire tower-http TimeoutLayer
Feature timeout đã có sẵn trong tower-http workspace dep từ B10 (default feature set). Không cần thêm dep mới — chỉ tạo helper factory function để mỗi sub-router B79 wire 1 timeout layer riêng.
Tạo file mới crates/shop-api/src/middleware/timeout_layer.rs:
// File: crates/shop-api/src/middleware/timeout_layer.rs
use std::time::Duration;
use tower_http::timeout::TimeoutLayer;
/// Default timeout cho 95% endpoint JSON CRUD —
/// P95 healthy < 100ms, 5s gấp 50 lần đủ buffer outlier.
pub fn default_timeout() -> TimeoutLayer {
TimeoutLayer::new(Duration::from_secs(5))
}
/// Import NDJSON bulk processing — B49 lock continued.
/// 30s đủ xử lý 10K row insert + validate + audit log.
pub fn import_timeout() -> TimeoutLayer {
TimeoutLayer::new(Duration::from_secs(30))
}
/// Upload multipart large file — B79 lock continued.
/// 60s đủ upload 100MB qua kết nối 4G chậm (~2 MB/s).
pub fn upload_timeout() -> TimeoutLayer {
TimeoutLayer::new(Duration::from_secs(60))
}
/// Webhook Stripe — align Stripe retry policy 15s connect timeout
/// + 8s read timeout. Shop API 10s margin nhỏ để Stripe nhận 200 OK
/// trước khi Stripe đánh dấu webhook failed (retry exponential).
pub fn webhook_timeout() -> TimeoutLayer {
TimeoutLayer::new(Duration::from_secs(10))
}
/// Health + metrics endpoint — fail-fast load balancer detect.
/// 1s đủ cho query pool sqlx + format Prometheus text;
/// nếu vượt 1s = service unhealthy, LB tự cắt traffic.
pub fn health_timeout() -> TimeoutLayer {
TimeoutLayer::new(Duration::from_secs(1))
}
5 helper factory function trả TimeoutLayer với 5 mức khác nhau. Mỗi helper là factory pattern (tạo instance mới mỗi lần gọi) thay vì biến static — TimeoutLayer không phải Copy, không tái dùng được giữa nhiều .layer() call.
Lock decision Shop API per-route timeout:
- Default routes (95%): 5s — fail-fast UX, user thấy lỗi nhanh thay vì chờ loading vô vọng.
- Import NDJSON: 30s — bulk processing 10K row hợp lý dưới 1 phút.
- Upload multipart: 60s — large file qua kết nối chậm.
- Webhook: 10s — Stripe retry policy mặc định 8s read timeout, Shop API 10s margin nhỏ ưu tiên ACK Stripe trước khi Stripe đánh dấu failed.
- Health/metrics: 1s — fail-fast load balancer detect, nếu service degrade load balancer cắt traffic ngay.
Update crates/shop-api/src/middleware/mod.rs re-export:
// File: crates/shop-api/src/middleware/mod.rs
pub mod trace_layer; // B80
pub mod metrics_layer; // B81
pub mod timeout_layer; // B82 NEW
pub use trace_layer::custom_trace_layer;
pub use metrics_layer::metrics_middleware;
pub use timeout_layer::{
default_timeout, import_timeout, upload_timeout,
webhook_timeout, health_timeout,
};
Helper riêng cho từng class thay vì 1 hàm tham số fn timeout(d: Duration) — đảm bảo team mới đọc code thấy ngay timeout đang áp dụng cho route nào, không phải đào số 5/30/60 rải rác. Defensive pattern: muốn đổi timeout cho 1 class chỉ sửa 1 chỗ duy nhất.
AppError::Timeout Variant + 504 Response
Vấn đề: TimeoutLayer của tower-http trả BoxError mặc định khi timeout — không tự map sang HTTP status code. Cần convert tower::timeout::error::Elapsed → AppError::Timeout qua HandleErrorLayer wrap.
Bước 1 — extend crates/shop-common/src/error.rs thêm variant Timeout (bump 21 → 22):
// File: crates/shop-common/src/error.rs
use std::time::Duration;
#[derive(Debug, thiserror::Error)]
pub enum AppError {
// ... 21 variant cũ B78 (BadRequest, Validation, Unauthenticated,
// Forbidden, NotFound, MethodNotAllowed, Conflict, RateLimited,
// Internal, Upstream, Unavailable, ... TooManyRequests B78)
/// Variant 22 (B82 NEW) — request vượt deadline TimeoutLayer.
/// Map 504 Gateway Timeout RFC 7231 §6.6.5.
#[error("request timeout after {0:?}")]
Timeout(Duration),
}
Bước 2 — update impl IntoResponse for AppError handle variant mới (lock B16 envelope chuẩn):
// File: crates/shop-common/src/error.rs (impl IntoResponse)
use axum::{http::StatusCode, response::IntoResponse, Json};
use serde_json::json;
impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
match self {
// ... 21 nhánh cũ B16 + B78
AppError::Timeout(duration) => {
let body = json!({
"error": "request timeout",
"code": "REQUEST_TIMEOUT",
"request_id": null,
"detail": {
"timeout_seconds": duration.as_secs_f64(),
}
});
(StatusCode::GATEWAY_TIMEOUT, Json(body)).into_response()
}
}
}
}
Body envelope giữ format chuẩn B16 + thêm field detail.timeout_seconds để client biết deadline cụ thể vi phạm — hữu ích cho client retry strategy (client tự điều chỉnh request size nếu hay vượt deadline).
Bước 3 — tạo helper convert BoxError → AppError::Timeout trong crates/shop-api/src/error_map.rs:
// File: crates/shop-api/src/error_map.rs
use std::time::Duration;
use axum::{response::Response, BoxError};
use tower::timeout::error::Elapsed;
use shop_common::error::AppError;
/// Convert BoxError từ TimeoutLayer → AppError::Timeout (504),
/// fallback Internal cho mọi error khác (KHÔNG bao giờ panic).
pub async fn handle_timeout(err: BoxError) -> Response {
if err.is::<Elapsed> () {
// tower::timeout::error::Elapsed KHÔNG carry Duration value
// → đọc default 5s; mỗi sub-router gắn layer khác nhau,
// muốn phân biệt thì truyền duration qua closure capture
// (xem step 5 — wire per-route truyền timeout đúng).
AppError::Timeout(Duration::from_secs(5)).into_response()
} else {
// Fallback cho mọi error khác từ stack tower —
// không leak detail ra client, log internal đầy đủ.
tracing::error!(error = %err, "unexpected service error");
AppError::Internal(err.to_string()).into_response()
}
}
Alternative pattern: closure capture duration cho từng sub-router để 504 response phản ánh đúng timeout class (default 5s vs import 30s vs upload 60s):
// File: crates/shop-api/src/error_map.rs (improved version)
use std::time::Duration;
use axum::{response::Response, BoxError};
pub fn make_timeout_handler(
timeout: Duration,
) -> impl Fn(BoxError) -> futures::future::Ready<Response> + Clone {
move |err: BoxError| {
let resp = if err.is::<tower::timeout::error::Elapsed> () {
AppError::Timeout(timeout).into_response()
} else {
tracing::error!(error = %err, "unexpected service error");
AppError::Internal(err.to_string()).into_response()
};
futures::future::ready(resp)
}
}
Lock decision Shop API: dùng HandleErrorLayer::new(make_timeout_handler(duration)) wrap TimeoutLayer trong ServiceBuilder chain để mỗi sub-router truyền đúng Duration tương ứng vào response. Order matter: HandleErrorLayer phải đặt outer hơn TimeoutLayer để bắt error trồi lên từ layer trong.
Wire Per-Route Timeout
Cập nhật crates/shop-api/src/router.rs wire HandleErrorLayer + TimeoutLayer cho từng sub-router theo pattern lock B79 (default + import + upload sub-router):
// File: crates/shop-api/src/router.rs
use std::time::Duration;
use axum::{error_handling::HandleErrorLayer, Router, routing::post};
use tower::ServiceBuilder;
use tower_http::limit::RequestBodyLimitLayer;
use axum::extract::DefaultBodyLimit;
use crate::middleware::{
custom_trace_layer, metrics_middleware,
default_timeout, import_timeout, upload_timeout,
webhook_timeout, health_timeout,
};
use crate::error_map::make_timeout_handler;
use crate::state::AppState;
pub fn build_router(state: AppState) -> Router {
// Default routes: 5s timeout — 95% endpoint JSON CRUD.
let default_routes = Router::new()
.merge(routes::products::default_routes())
.merge(routes::categories::routes())
.merge(routes::brands::routes())
.merge(routes::cart::routes())
.merge(routes::users::routes())
.layer(DefaultBodyLimit::max(2 * 1024 * 1024))
.layer(
ServiceBuilder::new()
// OUTER: HandleErrorLayer convert BoxError → AppError.
.layer(HandleErrorLayer::new(
make_timeout_handler(Duration::from_secs(5)),
))
// INNER: TimeoutLayer cắt future quá deadline.
.layer(default_timeout()),
);
// Import routes: 30s timeout — bulk NDJSON processing B49.
let import_routes = Router::new()
.route("/products/import.ndjson", post(import_products_ndjson))
.layer(DefaultBodyLimit::max(10 * 1024 * 1024))
.layer(
ServiceBuilder::new()
.layer(HandleErrorLayer::new(
make_timeout_handler(Duration::from_secs(30)),
))
.layer(import_timeout()),
);
// Upload routes: 60s timeout — multipart large file B79.
let upload_routes = Router::new()
.route("/products/{slug}/upload", post(upload_product_image))
.layer(DefaultBodyLimit::max(100 * 1024 * 1024))
.layer(
ServiceBuilder::new()
.layer(HandleErrorLayer::new(
make_timeout_handler(Duration::from_secs(60)),
))
.layer(upload_timeout()),
);
// Webhook: 10s timeout — Stripe retry policy align.
let webhook_routes = Router::new()
.route("/webhooks/stripe", post(stripe_webhook))
.layer(DefaultBodyLimit::max(1024 * 1024))
.layer(
ServiceBuilder::new()
.layer(HandleErrorLayer::new(
make_timeout_handler(Duration::from_secs(10)),
))
.layer(webhook_timeout()),
);
// Health/metrics: 1s timeout — fail-fast LB detect.
let infra_routes = Router::new()
.merge(routes::health::routes())
.route("/metrics", axum::routing::get(routes::metrics::metrics))
.layer(
ServiceBuilder::new()
.layer(HandleErrorLayer::new(
make_timeout_handler(Duration::from_secs(1)),
))
.layer(health_timeout()),
);
Router::new()
.merge(infra_routes)
.nest("/api/v1", default_routes
.merge(import_routes)
.merge(upload_routes)
.merge(webhook_routes))
// INNER N — metrics_layer (B81)
.layer(axum::middleware::from_fn(metrics_middleware))
// OUTER N — trace_layer (B80)
.layer(custom_trace_layer())
// OUTERMOST — global hard cap raw bytes (B79)
.layer(RequestBodyLimitLayer::new(100 * 1024 * 1024))
.with_state(state)
}
Pattern lock B82: ServiceBuilder chain HandleErrorLayer + TimeoutLayer — order matters. HandleErrorLayer phải đặt outer (gọi .layer() trước trong builder chain) để bắt được error trồi lên từ TimeoutLayer inner.
Stack giờ 10 layer (9 cũ B81 + per-route timeout). Lưu ý timeout layer KHÔNG nằm trong global stack mà nằm trong từng sub-router — đó là lý do middleware stack count tăng "logical" 10 lớp dù physically có 5 timeout layer khác nhau cho 5 sub-router.
Pitfall Background Task — Tokio Cancel Behavior
Khi handler timeout, tokio cancel future tương ứng — nghĩa là drop future ngay tại điểm await đang chờ. Hệ quả: nếu handler đang ở giữa chuỗi await nhiều bước, các bước sau KHÔNG bao giờ chạy.
Example scenario tạo đơn hàng:
pub async fn create_order(
State(state): State<AppState>,
Json(dto): Json<CreateOrderDto>,
) -> Result<Json<OrderResponseDto>, AppError> {
let mut tx = state.db.begin().await?;
insert_order(&mut tx, &dto).await?;
decrement_stock(&mut tx, &dto).await?; // ← timeout giữa đây
tx.commit().await?; // ← KHÔNG bao giờ chạy
Ok(Json(...))
}
Khi timeout xảy ra tại decrement_stock:
txbị drop → sqlx tự ROLLBACK best-effort thông qua async drop (B54 lock continued).- Nếu
tx.commit()chưa kịp chạy → DB ở trạng thái trước transaction (an toàn). - Nếu
tx.commit()đã chạy xong và return Ok → tokio cancel sau commit thì transaction đã COMMIT thành công, response 504 trả về client nhưng DB đã thay đổi.
Tin tốt: PostgreSQL transaction atomic — rollback hoặc commit hoàn toàn, KHÔNG bao giờ partial. Dù tokio cancel giữa chừng, DB state nhất quán (ACID guarantee).
Tin xấu: side-effect bên ngoài DB KHÔNG có atomic guarantee. Scenario rủi ro:
- Stripe API call đã thành công nhưng tokio cancel trước khi save
payment_idvào DB → mất tiền không có record. - File write đã ghi xong nhưng timeout trước khi commit row metadata → file rác không có entry tham chiếu.
- Queue publish đã enqueue message background worker nhưng cancel trước khi mark
queued_at→ message duplicate publish lần sau.
Pattern phòng vệ Shop API:
- Idempotency-Key (B66 lock) cho mọi POST mutation external side-effect — client retry safe, server detect duplicate qua key trả response cũ.
- Background task tách qua queue (G21 worker preview) — handler chỉ enqueue rồi return 202 Accepted nhanh, không chờ side-effect external; worker idempotent xử lý có retry.
- Saga pattern cho multi-service workflow (G18 deploy) — mỗi bước có compensation transaction rollback nếu bước sau fail.
- Outbox pattern (G21 preview) — write event vào table
outboxcùng business transaction, worker poll table publish queue → atomic guarantee.
Quy tắc lock B82: handler critical mutation (create_order, checkout, payment_capture) KHÔNG gọi side-effect external trực tiếp trong handler — luôn enqueue qua outbox + worker xử lý async với idempotent guarantee.
Drop Guard Pattern — Transaction Cleanup
Drop guard là pattern Rust idiomatic — struct với impl Drop tự động chạy cleanup code khi struct ra khỏi scope (kể cả khi future bị cancel). Áp dụng cho transaction: emit metric mỗi khi transaction bị drop chưa commit để monitor phát hiện anomaly.
// File: crates/shop-core/src/orders.rs (preview)
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
/// Drop guard cho transaction lifecycle — emit metric +
/// log warn khi transaction bị drop chưa commit (timeout / panic / early return).
struct TransactionGuard {
committed: Arc<AtomicBool>,
operation: &'static str,
}
impl TransactionGuard {
fn new(operation: &'static str) -> (Self, Arc<AtomicBool>) {
let committed = Arc::new(AtomicBool::new(false));
let guard = TransactionGuard {
committed: committed.clone(),
operation,
};
(guard, committed)
}
}
impl Drop for TransactionGuard {
fn drop(&mut self) {
let committed = self.committed.load(Ordering::Acquire);
if !committed {
tracing::warn!(
operation = self.operation,
"transaction dropped without commit — rolled back"
);
metrics::counter!(
"transaction_rolled_back_total",
"operation" => self.operation,
"reason" => "drop",
)
.increment(1);
}
}
}
Áp dụng vào handler create_order:
pub async fn create_order(
State(state): State<AppState>,
Json(dto): Json<CreateOrderDto>,
) -> Result<Json<OrderResponseDto>, AppError> {
let (_guard, committed_flag) = TransactionGuard::new("create_order");
let mut tx = state.db.begin().await?;
insert_order(&mut tx, &dto).await?;
decrement_stock(&mut tx, &dto).await?;
tx.commit().await?;
// Mark committed TRƯỚC khi _guard ra khỏi scope —
// drop chạy sau cùng tự đọc committed=true bỏ qua warn.
committed_flag.store(true, Ordering::Release);
Ok(Json(...))
}
Lock pattern Shop API:
- Mọi mutation transaction critical (create_order, checkout, payment_capture) wrap
TransactionGuard. - Emit metric
transaction_rolled_back_total{operation, reason}cho mỗi rollback unexpected. - Logging WARN nếu drop chưa commit — alert engineer review log Loki theo
request_id(B80 lock continued). - Monitor qua Grafana panel với PromQL
rate(transaction_rolled_back_total{reason="drop"}[5m]) > 0.01alert Slack non-paging — bình thường rate = 0, vượt = anomaly đáng debug.
Note quan trọng: B54 đã lock tx.commit() / tx.rollback() explicit match pattern — drop guard chỉ thêm tầng phòng vệ quan sát (observability), không thay thế explicit commit/rollback. Drop guard giúp phát hiện sớm code path nào quên explicit (B54 best practice), không phải workaround cho code sai.
Atomic flag committed tách khỏi struct guard để compiler không complain ownership move khi struct chứa generic Transaction không Clone — pattern Arc<AtomicBool> lock vĩnh viễn cho mọi drop guard tương lai Shop API.
Verify End-To-End + Test Timeout
Verify pipeline end-to-end qua 4 scenario kiểm tra mọi class timeout đã wire đúng + 504 response đúng format envelope.
Test 1 — endpoint nhanh dưới deadline default 5s:
time curl -s http://localhost:3000/api/v1/products | head -c 100
# real 0m0.052s
# user 0m0.005s
# sys 0m0.005s
# {"items":[{"id":1,"slug":"iphone-15",...
# → OK, response trong 52ms << 5s
Test 2 — simulate slow handler vượt deadline 5s:
// File: crates/shop-api/src/routes/test.rs (chỉ enable feature "test-handlers")
#[cfg(feature = "test-handlers")]
pub async fn slow_handler() -> Result<&'static str, AppError> {
tokio::time::sleep(Duration::from_secs(10)).await;
Ok("done")
}
time curl -i http://localhost:3000/api/v1/test/slow
# real 0m5.012s # ← cắt đúng tại 5s deadline
# HTTP/1.1 504 Gateway Timeout
# content-type: application/json
# content-length: 113
#
# {"error":"request timeout","code":"REQUEST_TIMEOUT",
# "request_id":null,"detail":{"timeout_seconds":5.0}}
Test 3 — import endpoint 30s timeout:
# File 5K row — process ~25s under 30s limit:
time curl -s -X POST http://localhost:3000/api/v1/products/import.ndjson \
-H "Idempotency-Key: $(uuidgen)" \
--data-binary @small-5k.ndjson
# real 0m25.4s
# {"imported":5000,"failed":0,...}
# → OK
# File 50K row — process ~31s vượt 30s limit:
time curl -i -X POST http://localhost:3000/api/v1/products/import.ndjson \
-H "Idempotency-Key: $(uuidgen)" \
--data-binary @large-50k.ndjson
# real 0m30.018s
# HTTP/1.1 504 Gateway Timeout
# {"error":"request timeout","code":"REQUEST_TIMEOUT",
# "request_id":null,"detail":{"timeout_seconds":30.0}}
Test 4 — upload endpoint 60s timeout:
# Upload 20MB qua kết nối 1MB/s — ~20s under 60s:
time curl -s -X POST http://localhost:3000/api/v1/products/iphone-15/upload \
-F "[email protected]" \
--limit-rate 1M
# real 0m20.3s
# {"image_url":"https://cdn.shop.vn/products/iphone-15/abc.jpg"}
# Upload 100MB qua kết nối 1MB/s — ~100s vượt 60s:
time curl -i -X POST http://localhost:3000/api/v1/products/iphone-15/upload \
-F "[email protected]" \
--limit-rate 1M
# real 1m0.024s
# HTTP/1.1 504 Gateway Timeout
# {"error":"request timeout","code":"REQUEST_TIMEOUT",
# "request_id":null,"detail":{"timeout_seconds":60.0}}
Verify metric Prometheus:
curl -s http://localhost:3000/metrics | grep -E "timeout|transaction_rolled"
# transaction_rolled_back_total{operation="create_order",reason="drop"} 0
# http_request_duration_seconds_bucket{method="GET",path="/api/v1/test/slow",le="5.0"} 0
# http_request_duration_seconds_bucket{method="GET",path="/api/v1/test/slow",le="10.0"} 1
# http_request_duration_seconds_count{method="GET",path="/api/v1/test/slow"} 1
# http_request_duration_seconds_sum{method="GET",path="/api/v1/test/slow"} 5.012
# Lưu ý: request /test/slow KHÔNG record full 10s duration —
# timeout cắt tại 5s, sum = 5.012s (deadline + margin nhỏ overhead);
# bucket le="5.0" = 0 (chưa đạt mốc bị cắt), le="10.0" = 1 (rơi vào bucket cao hơn).
# Anomaly detect: sum >> sum_normal cho cùng endpoint = signal timeout đang xảy ra.
Pattern verify lock B82: chạy 4 scenario sau mỗi deploy production để confirm timeout config đúng + 504 response format giữ envelope chuẩn cross-service. Tự động hóa qua suite e2e test (B70 lock testcontainers continued) — mock slow handler + assert 504 + check metric counter.
Tổng Kết
- 3 loại timeout: request (B82 focus), response (G18 preview), idle (G18 preview).
tower-http::TimeoutLayerlock per-route — KHÔNG global.- 4 timeout class lock: default 5s, import 30s, upload 60s, webhook 10s.
- Health/metrics 1s fail-fast cho load balancer detect service degrade.
- Relationship:
server_timeout < proxy_timeout < lb_timeout(5s < 30s < 60s). AppError::Timeout(Duration)variant 22 — bump 21 → 22.- 504 Gateway Timeout response chuẩn HTTP RFC 7231 §6.6.5.
HandleErrorLayer + TimeoutLayerServiceBuilderchain lock pattern — HandleError outer.- Pitfall background task cancel: tokio future drop → partial state risk.
- PostgreSQL atomic — DB rollback hoặc commit hoàn toàn, KHÔNG partial.
- Side-effect external (Stripe / file / queue) có thể partial — mitigate qua Idempotency-Key + outbox.
- Drop guard pattern + metric
transaction_rolled_back_total{operation,reason}cho monitor Grafana. - Saga + queue cho multi-service workflow (G18 + G21 deploy).
- File path lock: NEW
middleware/timeout_layer.rs5 helper factory + extenderror_map.rshandle_timeout/make_timeout_handler+ extenderror.rsTimeout variant. - Stack giờ 10 layer (9 cũ B81 +
timeout_per_routeB82). - Foundation cho B83 (request validation), G18 (proxy timeout config), G21 (queue worker + saga + outbox).
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- 3 loại timeout HTTP — phân biệt request vs response vs idle timeout. Quy tắc relationship server < proxy < lb tại sao bắt buộc theo thứ tự này? Cho scenario cụ thể nếu đảo thứ tự gì xảy ra.
- 4 timeout class Shop API — phân tích pros/cons mỗi config (default 5s / import 30s / upload 60s / webhook 10s). Tại sao webhook 10s thay 5s default? Stripe retry policy ảnh hưởng quyết định ra sao?
HandleErrorLayer + TimeoutLayerServiceBuilderchain — tại saoHandleErrorLayerphải đặt outer (gọi.layer()trước trong builder)? Cho scenario order matters: nếu đặt sai thứ tự (TimeoutLayer outer + HandleErrorLayer inner) gì xảy ra?- Pitfall background task cancel — Tokio future drop behavior khi timeout. PostgreSQL atomic vs side-effect external scenario: cho ví dụ cụ thể Shop API (Stripe payment + DB save) — gì có thể partial, gì atomic. Mitigate strategy 3 tầng (Idempotency-Key + outbox + saga) áp dụng ra sao?
- Drop guard pattern — explicit cleanup khi future drop. Cho ví dụ scenario uncommitted transaction + monitor metric. Tại sao tách
AtomicBoolra khỏi struct guard (pattern Arc<AtomicBool>)? Drop guard có thay thế explicit commit/rollback B54 không?
Đáp án
- 3 loại timeout HTTP phân biệt: (a) Request timeout — server (axum) chờ tối đa N giây cho handler hoàn thành 1 request cụ thể; vị trí trong process axum áp dụng qua
tower-http::TimeoutLayer; default Shop API 5s; khi vượt server cắt future + response 504. (b) Response timeout — proxy/load balancer (NGINXproxy_read_timeout, HAProxytimeout server, AWS ALBidle_timeout) chờ tối đa N giây nhận response từ upstream; vị trí ngoài process axum; cấu hình ở tầng infrastructure G18; thường lớn hơn server request timeout để tránh proxy timeout trước. (c) Idle/Keep-alive timeout — đo thời gian TCP connection KHÔNG có data flow trước close (hyperhttp2_keep_alive_interval); tách biệt request timeout vì đo connection state chứ không phải request processing time; phục vụ tối ưu connection pool client (reqwest pool) + server (hyper). Quy tắc relationship:server_timeout < proxy_timeout < lb_timeout(5s < 30s < 60s AWS ALB default). Tại sao bắt buộc thứ tự này: (i) margin gấp 6x giữa mỗi tầng đủ buffer cho retry network jitter + cold start container + GC pause; (ii) tầng dưới timeout trước tầng trên → root cause rõ ràng (server log 504 = handler thật sự chậm, không phải proxy/lb nhầm); (iii) client retry không bị nhân đôi attempt khi proxy timeout trước server (proxy đã trả 504 nhưng server vẫn xử lý → client retry lần 2 gây duplicate request). Scenario đảo thứ tự: nếu cấu hình server 30s + proxy 5s + lb 10s, request handler chạy 15s thực tế — proxy timeout tại 5s trả 504, nhưng server vẫn tiếp tục xử lý đến hết 15s; tài nguyên server lãng phí (DB connection + thread tokio); client thấy lỗi 504 từ proxy nhưng nguyên nhân thực là proxy_read_timeout sai cấu hình chứ KHÔNG phải server chậm; client retry lần 2 đến server đang còn chạy lần 1 → double-charge nếu là payment endpoint; debug khó vì log server không có error (handler chạy thành công 15s sau nhưng client đã disconnect). Lock Shop API: review timeout chain 3 tầng mỗi quý deploy G18 với checklist 4 mức (server < proxy < lb < client_browser default 60s). - 4 timeout class pros/cons: Default 5s — pros: fail-fast UX user thấy lỗi nhanh thay vì chờ vô vọng, tài nguyên server giải phóng sớm, alert nhanh khi service degrade; cons: outlier query phức tạp (báo cáo aggregate join nhiều bảng) có thể chạm deadline → cần optimize query hoặc tách endpoint riêng class 30s; áp dụng 95% endpoint Shop API CRUD JSON (P95 healthy < 100ms gấp 50 lần deadline đủ buffer outlier). Import 30s — pros: đủ xử lý 10K row NDJSON bulk insert + validate + audit log; cons: nếu user import file 100K row sẽ luôn timeout → cần chunk client-side trước hoặc chuyển sang async job queue 202 Accepted (G16 deploy); use case:
/products/import.ndjsonB49 lock. Upload 60s — pros: cover large file 100MB qua kết nối 4G chậm 2 MB/s; cons: 60s là threshold tối đa user kiên nhẫn chờ progress bar → khuyến cáo client chia chunked upload (multipart resumable) cho file > 100MB; use case:/products/{slug}/uploadB79 lock. Webhook 10s — pros: align Stripe retry policy (Stripe gửi POST webhook với 15s connect timeout + 8s read timeout, gấp đôi 5s default an toàn margin); cons: webhook handler MUST trả 200 OK trong 10s nếu không Stripe đánh dấu failed + retry exponential (5s/30s/1h/6h/...) 3 ngày; pattern lock: webhook handler chỉ verify signature + enqueue background job → return 200 OK ngay trong < 100ms, worker xử lý async (B71 lock outbox preview). Tại sao webhook 10s thay 5s default: Stripe retry policy mặc định 8s read timeout — nếu Shop API 5s default sẽ ép Shop API cắt response sớm hơn Stripe expect; với 10s margin, webhook có thời gian (a) HMAC verify signature (1ms), (b) parse JSON event (1ms), (c) enqueue background worker (10ms), (d) return 200 OK; tổng << 1s nhưng để 10s deadline buffer cho cold start container + DB connection pool lazy init lần đầu (B56 acquire_timeout 5s + 5s margin xử lý). Stripe retry policy ảnh hưởng: nếu webhook timeout Stripe sẽ retry tối đa 3 ngày exponential — risk duplicate event nếu Shop API xử lý partial (Idempotency-Key bằngevent.idStripe mitigate B66 lock). Health/metrics 1s — pros: fail-fast LB detect service unhealthy ngay trong 1 scrape interval; cons: cold start lần đầu container mất 2-3s init pool sqlx → health check fail trong 2-3 health check đầu (cấu hình LBhealthy_threshold2 health check sai → pod bị kill khởi động lại loop, cầnhealthy_threshold5+ vàstart_period30s grace). HandleErrorLayer + TimeoutLayer ServiceBuilderchain order matters:ServiceBuilderbuilder pattern tower —.layer(L1).layer(L2).layer(L3)tạo stackL1(L2(L3(service))), layer thêm trước là outer bọc ngoài layer sau. Tại sao HandleErrorLayer outer:TimeoutLayerwrap inner service → khi inner service timeout,TimeoutLayertrảErr(BoxError)trồi lên outer;HandleErrorLayerở outer nhậnErr(BoxError)+ chạy closure handler convert sangResponse(axum yêu cầu service Final phảiService<Request, Response = Response, Error = Infallible>để mount vào router); nếu KHÔNG cóHandleErrorLayerouter,BoxErrortrồi lên axum router → axum panic vì error type không match (router yêu cầuInfallible). Pattern lock B82:ServiceBuilder::new().layer(HandleErrorLayer::new(closure)).layer(TimeoutLayer::new(duration))đúng order — HandleErrorLayer outer, TimeoutLayer inner. Scenario đặt sai thứ tự: nếu viếtServiceBuilder::new().layer(TimeoutLayer::new(duration)).layer(HandleErrorLayer::new(closure))— TimeoutLayer outer, HandleErrorLayer inner; khi inner service trảOk(Response)bình thường, HandleErrorLayer pass-through; nhưng khi inner service trảErr(BoxError), HandleErrorLayer ở inner convert sangResponse; sau đó TimeoutLayer outer wrap response — nhưng TimeoutLayer KHÔNG biết phân biệt "response từ handler thành công" vs "response từ HandleError convert error", nên timeout có thể cắt response stream khi đang ghi error 504 ra wire → client nhận response cụt + connection reset thay vì 504 đầy đủ. Hơn nữa, nếu TimeoutLayer outer mà timeout trước khi inner service kịp trả error, BoxError sẽ trồi lên router (vì HandleErrorLayer inner đã chạy xong stage convert) → router panic Infallible mismatch. Tóm lại: HandleErrorLayer MUST outer hơn mọi layer có thể sinh BoxError. Lock Shop API: comment// OUTER: HandleErrorLayer+// INNER: TimeoutLayerMANDATORY mỗi sub-router build.- Pitfall background task cancel + PostgreSQL atomic: khi tokio cancel future tại điểm
awaitđang chờ, mọi statement sau điểm đó KHÔNG chạy + struct local destruct theoDrop. PostgreSQL atomic guarantee: transactionBEGIN ... COMMIThoặcBEGIN ... ROLLBACKhoàn toàn — không có trạng thái partial; nếutxbị drop chưa commit → sqlx tự ROLLBACK best-effort qua async drop (B54 lock); nếutx.commit()đã chạy xong return Ok rồi tokio cancel sau → COMMIT đã succeed DB nhất quán. Scenario Shop API:create_orderhandler 3 step: (a)insert_ordertx, (b)decrement_stocktx, (c)tx.commit()— nếu timeout giữa (a) và (b): tx drop, ROLLBACK, DB không có order + stock không đổi (atomic OK); nếu timeout giữa (b) và (c): tx drop, ROLLBACK, DB không có order + stock không đổi (atomic OK); nếu timeout SAU (c) commit xong: DB có order + stock đã decrement (atomic OK), nhưng client nhận 504 không biết order đã tạo → risk duplicate khi retry. Side-effect external scenario:create_order+ Stripe API call + DB save payment_id: (a)insert_ordertx, (b)stripe.charge(...)API call (external, không trong tx), (c)insert_payment(payment_id from stripe)tx, (d)tx.commit()— nếu timeout giữa (b) và (c): Stripe đã charge tiền user thành công nhưng Shop API tx rollback không lưu payment_id → mất tiền không có record DB; user thấy 504 retry → Stripe charge lần 2 (nếu không có Idempotency-Key Stripe-side); duplicate charge thật. Mitigate 3 tầng: (i) Idempotency-Key B66 — client gửi UUID per request, server cache response 24h, retry với cùng key trả response cũ; mitigate duplicate request từ client side. (ii) Outbox pattern G21 — handler chỉ write event vào tableoutboxtrong cùng transaction business; worker polloutboxpublish queue + gọi Stripe API; atomic guarantee giữa business state + outbox event (cùng transaction); worker idempotent với Idempotency-Key Stripe-side; mitigate side-effect external partial. (iii) Saga pattern G18 — multi-step workflow có compensation transaction rollback ngược nếu step sau fail (vd: reserve_stock → charge_payment → confirm_order, nếu charge_payment fail thì compensation = release_stock); cho complex flow cross-service. Lock Shop API: critical mutation (create_order/checkout/payment_capture/refund) KHÔNG gọi Stripe trực tiếp handler — luôn enqueue outbox + worker xử lý async (G21 deploy). - Drop guard pattern + Arc<AtomicBool> design rationale: drop guard là struct với
impl Droptự động chạy cleanup code khi struct ra khỏi scope (kể cả khi future bị cancel giữa chừng) — guarantee bởi Rust language spec (drop chạy cho mọi local variable khi stack unwind hoặc scope exit). Scenario uncommitted transaction:create_orderhandler timeout giữadecrement_stockvàtx.commit()— tokio cancel future, local variabletx+_guardra khỏi scope: (a)_guardDrop chạy → đọccommitted.load(Acquire) == false→ emit metrictransaction_rolled_back_total{operation="create_order",reason="drop"}+ log WARN "transaction dropped without commit"; (b)txDrop chạy sau → sqlx ROLLBACK best-effort. Monitor Grafana panel: PromQLrate(transaction_rolled_back_total[5m])normal = 0, anomaly > 0.01 = signal có timeout liên tục cho operation cụ thể → engineer debug log Loki query{operation="create_order"} | level="warn"trace nguyên nhân. Tại sao tách AtomicBool ra khỏi struct guard (Arc<AtomicBool> pattern): alternative naive design embed flag trực tiếp struct guardstruct Guard { committed: bool, ... }— vấn đề: Rust ownership rule, sau khi tạo guard nếu muốn setguard.committed = trueở cuối handler thì cần&mut guard, nhưng_guardvới prefix underscore convention = unused intentionally + lifetime hết scope handler không lấy được&mut; phải redesign signaturelet mut guard = ...; ... guard.commit_marked(); drop(guard);verbose + dễ quên explicit drop. Pattern Arc<AtomicBool> tách flag ra Arc share: guard giữcommitted: Arc<AtomicBool>clone, handler giữcommitted_flag: Arc<AtomicBool>clone — setcommitted_flag.store(true, Release)không cần mutable borrow guard; Drop của guard đọc qua immutableself.committed.load(Acquire); memory ordering Acquire/Release đảm bảo happen-before nếu set xảy ra trước Drop chạy. Drop guard KHÔNG thay thế explicit commit/rollback B54: B54 lock pattern explicitmatch tx.commit().await { Ok(_) => ..., Err(e) => ... }là MANDATORY cho mọi transaction — đảm bảo handle commit failure rõ ràng (transient network error, serialization failure, deadlock victim). Drop guard chỉ là tầng observability bổ sung — phát hiện code path nào quên explicit commit/rollback (anti-pattern); KHÔNG là cleanup logic thay thế. Lock Shop API: critical mutation handler wrap drop guard + giữ B54 explicit commit/rollback đầy đủ; pattern combine 2 best practice không loại trừ.
Bài Tiếp Theo
Bài 83: Request Validation + Input Sanitize — middleware validate query string + path param + header trước khi reach handler, sanitize input (HTML escape, SQL injection prevention recap), pattern strict-mode validation với validator crate global rule.
