Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 2 — Mô hình hóa quy trình thành State Machine

Đã đọc 0/10 mục · còn khoảng 4 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Mọi orchestration đáng tin cậy đều đứng trên một ý tưởng: quy trình là một state machine (máy trạng thái) — tập hợp các trạng thái hữu hạn và các phép chuyển hợp lệ giữa chúng. Thay vì hỏi "đơn này chạy tới đâu trong hàm?", bạn hỏi "đơn này đang ở trạng thái nào trong DB?". State nằm trong storage bền, không phải trong ngăn xếp lời gọi.

Vì sao state phải bền (durable)

Giả sử orchestrator của bạn đang chạy bước "thu tiền" thì container bị OOM-kill. Nếu trạng thái chỉ nằm trong RAM, bạn không biết tiền đã trừ chưa. Nếu trạng thái là một dòng trong bảng orders với cột status='INVOICED' và payment_status='PENDING', thì tiến trình khác chỉ cần đọc lên và tiếp tục đúng chỗ.

Thiết kế bảng state

CREATE TABLE order_workflow (
  id            BIGINT PRIMARY KEY,
  order_id      VARCHAR(64) NOT NULL UNIQUE,
  state         VARCHAR(32) NOT NULL,  -- PENDING, RESERVED, INVOICED, PAID, FAILED, COMPENSATED
  step_data     JSON,                  -- reservation_id, invoice_id, payment_id...
  attempt_count INT DEFAULT 0,
  updated_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

step_data cực quan trọng: nó giữ các ID trả về từ hệ thống ngoài (mã giữ hàng, số hóa đơn). Khi cần compensate, bạn dùng chính các ID này để hủy.

Định nghĩa chuyển trạng thái hợp lệ

Viết ra bảng chuyển tường minh, và từ chối mọi chuyển không nằm trong bảng:

PENDING   --reserve-->  RESERVED
RESERVED  --invoice-->  INVOICED
INVOICED  --charge-->   PAID
*         --error-->    FAILED
FAILED    --compensate->COMPENSATED

Nếu ai đó cố chuyển từ PENDING thẳng sang PAID, code phải ném lỗi. Điều này chặn cả bug lẫn hành vi lạ của AI agent.

Pseudocode một bước chuyển

def transition(order_id, from_state, to_state, apply_fn):
    row = db.select_for_update(order_id)   # khóa dòng
    if row.state != from_state:
        return  # ai đó đã chuyển rồi -> bỏ qua (an toàn)
    result = apply_fn(row.step_data)       # gọi hệ thống ngoài
    row.step_data.update(result)
    row.state = to_state
    db.save(row)

Điều kiện if row.state != from_state biến hàm này thành an toàn khi chạy lại: nếu bước đã hoàn thành, lần chạy thứ hai không làm gì.

Khung tư duy: đặt tên trạng thái

  • Trạng thái mô tả kết quả đã đạt, không mô tả hành động đang làm. Dùng RESERVED (đã giữ hàng) chứ không phải RESERVING.
  • Luôn có một trạng thái kết thúc thành công và ít nhất một trạng thái kết thúc thất bại.
  • Không trạng thái nào được phép "lửng lơ mãi" — mỗi trạng thái không-cuối phải có timeout đưa nó đi tiếp hoặc sang FAILED.

Các bước áp dụng

  • Liệt kê mọi kết quả trung gian của quy trình, biến mỗi cái thành một trạng thái.
  • Vẽ ma trận chuyển hợp lệ; đánh dấu trạng thái đầu và cuối.
  • Tạo bảng state với cột state, step_data, attempt_count.
  • Bọc mỗi bước bằng hàm transition có kiểm tra from_state.
  • Thêm một view/truy vấn liệt kê đơn "kẹt" (updated_at quá cũ mà chưa ở trạng thái cuối).

Vì sao khóa dòng (row lock) lại quan trọng

Trong production, cùng một đơn có thể bị hai worker chạm vào cùng lúc: một job trùng trong queue, một retry đến sớm, một lần đánh thức từ callback. Nếu cả hai cùng đọc state RESERVED rồi cùng gọi bước invoice, bạn tạo hai hóa đơn. SELECT ... FOR UPDATE khóa dòng đơn đó: worker thứ hai phải chờ, và khi vào được thì state đã đổi sang INVOICED, nên điều kiện from_state khiến nó bỏ qua. Khóa dòng cộng kiểm tra from_state là hai lớp bảo vệ bổ trợ nhau — lock chống chạy song song, kiểm tra state chống chạy lặp tuần tự.

Bất biến (invariant) của state machine

Mỗi state machine tốt có vài bất biến — điều luôn đúng dù chuyện gì xảy ra. Ví dụ: "một đơn không bao giờ ở PAID mà không có payment_id trong step_data"; hoặc "đơn COMPENSATED phải từng đi qua FAILED". Viết các bất biến này ra và thêm một job kiểm tra định kỳ (audit) quét DB tìm vi phạm. Khi có bug lạ, chính các bất biến bị vỡ sẽ chỉ cho bạn thấy vấn đề sớm, thay vì để dữ liệu hỏng âm thầm lan rộng.

-- Audit: đơn PAID nhưng thiếu payment_id là dấu hiệu bug
SELECT order_id FROM order_workflow
WHERE state = 'PAID'
  AND (step_data ->> 'payment_id') IS NULL;

Sai lầm thường gặp

  • Trạng thái quá thô: chỉ NEW và DONE. Khi hỏng, bạn không biết đã đi tới đâu để compensate.
  • Cho phép mọi chuyển: không kiểm from_state, dẫn tới double-charge khi message trùng.
  • Nhét thông tin runtime lớn vào step_data: chỉ lưu ID và thứ cần cho bù trừ, không lưu cả payload nặng.
  • Quên trạng thái cuối cho nhánh lỗi: đơn FAILED không bao giờ được dọn, chất đống trong hệ thống.
Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi