Timeout không có nghĩa là tác vụ dịch thất bại: thiết kế bước đối soát trước khi gửi lại

Một tác vụ xử lý ảnh thường sống lâu hơn một kết nối HTTP. Client có thể không nhận được phản hồi trong thời gian chờ, trong khi hệ thống phía sau đã nhận yêu cầu và tiếp tục xử lý. Nếu worker lập tức tạo một yêu cầu mới, một sự cố quan sát có thể biến thành hai công việc cùng tiêu thụ tài nguyên.

Với nhóm vận hành quy trình dịch manga online, vấn đề không chỉ là thông báo lỗi trên màn hình. Hai bản kết quả có thể đi vào hai vòng duyệt khác nhau; người biên tập sửa một bản nhưng hệ thống lại phát hành bản còn lại. Bài này đề xuất một mô hình điều phối nhỏ để phân biệt trạng thái công việc với trạng thái quan sát.

Công khai phạm vi: bài do AI biên soạn cho MangaTranslate, sản phẩm của Ficory LLC. Ví dụ Python bên dưới chạy hoàn toàn cục bộ với sự kiện mô phỏng. Bài không mô tả một lần tích hợp production, không gọi API thật và không đưa ra số liệu độ trễ hay độ tin cậy của dịch vụ.

1. Ghi lại danh tính công việc trước khi bàn về retry

Mỗi yêu cầu nghiệp vụ nên có một mã nội bộ do hệ thống của bạn quản lý. Mã đó đại diện cho một bộ ảnh cùng cấu hình cần xử lý, chứ không đại diện cho một lần thử kết nối. Nếu nhân viên đổi ảnh hoặc đổi ngôn ngữ đích, đó là một phiên bản yêu cầu khác cần được nhận diện rõ.

Với dịch ảnh manga, cấu hình đáng quan tâm có thể gồm danh sách trang theo thứ tự, ngôn ngữ đích và lựa chọn xử lý. Bạn có thể lưu manifest cùng dấu vết phiên bản trong cơ sở dữ liệu. Đây là gợi ý thiết kế phía client, không phải danh sách trường bắt buộc của nhà cung cấp.

Điểm cần quyết định trước là khi nào được phép tạo công việc mới. Một thao tác nhấn lại trên giao diện, một lần worker khởi động lại hoặc một timeout không nên tự động làm thay đổi danh tính nghiệp vụ.

2. Đặt sản phẩm vào luồng xử lý bất đồng bộ

MangaTranslate cung cấp dịch ảnh truyện tranh và chỉnh sửa kết quả. Ví dụ API chính thức mô tả bước gửi tác vụ, lưu batch_id, kiểm tra tiến độ và lấy ảnh đã xử lý thành công. Với V2, quy trình còn tách chuẩn bị tải lên, tải tệp và gửi tác vụ dịch.

Đối chiếu ví dụ API chính thức trước khi triển khai. Cần đối chiếu phiên bản, xác thực và điều kiện sử dụng trực tiếp trước khi triển khai. Bài này không sao chép một request mẫu có thể bị hiểu là hợp đồng API đầy đủ.

Một luồng vận hành nội bộ có thể tách thành các bước: chuẩn bị dữ liệu, gửi, quan sát tiến độ, nhận kết quả, duyệt tiếng Việt rồi mới phát hành. Bước “dịch xong” và bước “đã duyệt” cần được phân biệt. Người dịch vẫn phải kiểm tra ngữ nghĩa, tên riêng và bố cục chữ ở từng trang.

3. Dùng hai trạng thái thay vì ép mọi thứ vào một cột

Có thể lưu state để nói bạn biết công việc đang ở đâu, và observation để nói lần quan sát gần nhất có đáng tin không. Khi truy vấn tiến độ bị timeout, observation chuyển thành unknown; state và batch_id đã biết vẫn được giữ.

Nhờ vậy, màn hình có thể thông báo “chưa lấy được tiến độ mới” thay vì “dịch thất bại”. Worker cũng biết cần truy vấn lại công việc cũ, không tạo một công việc mới chỉ vì mất kết nối.

Trường hợp khó hơn là timeout ngay khi gửi mà chưa lưu được mã tác vụ. Lúc đó cần đối soát việc tiếp nhận theo cơ chế mà nhà cung cấp thực sự hỗ trợ. Không tự giả định rằng mọi endpoint đều có tính idempotent. Một khóa chống trùng chỉ có ý nghĩa trong phạm vi, thời hạn và điều kiện mà hợp đồng của endpoint xác định.

4. Một mô phỏng nhỏ để kiểm tra quyết định của client

Mã dưới đây minh họa quyết định tiếp theo khi nhận một sự kiện. batch_id demo chỉ là chuỗi tự đặt; event accepted là tên sự kiện nội bộ của ví dụ, không phải một giá trị trạng thái được hứa hẹn bởi API. Hàm trả về bản sao để ví dụ không sửa ngầm dữ liệu đầu vào.

from copy import deepcopy


def observe(job, event, batch_id=None):
    updated = deepcopy(job)
    if event == "accepted":
        if not batch_id:
            raise ValueError("accepted requires a batch_id")
        if updated.get("batch_id") not in (None, batch_id):
            raise ValueError("conflicting batch_id")
        updated.update(state="processing", batch_id=batch_id)
    elif event == "timeout":
        updated["observation"] = "unknown"
    elif event in {"pending", "processing", "completed", "failed", "canceled"}:
        if not updated.get("batch_id"):
            raise ValueError("reconcile submission before polling")
        updated.update(state=event, observation="confirmed")
    else:
        raise ValueError("unsupported event")
    return updated


def next_action(job):
    if job.get("batch_id"):
        if job["state"] in {"completed", "failed", "canceled"}:
            return "review_result"
        return "poll_existing_batch"
    if job.get("observation") == "unknown":
        return "reconcile_submission"
    return "submit_once"


if __name__ == "__main__":
    fresh = {"state": "ready", "observation": "confirmed"}
    assert next_action(fresh) == "submit_once"
    unknown = observe(fresh, "timeout")
    assert next_action(unknown) == "reconcile_submission"
    accepted = observe(fresh, "accepted", "demo-batch-001")
    timed_out = observe(accepted, "timeout")
    assert next_action(timed_out) == "poll_existing_batch"
    assert timed_out["batch_id"] == "demo-batch-001"
    completed = observe(timed_out, "completed")
    assert next_action(completed) == "review_result"
    assert fresh == {"state": "ready", "observation": "confirmed"}
    print("5 checks passed; simulation only, no API requests")

Lưu thành devops-state-demo.py rồi chạy bằng Python 3. Kết quả kiểm tra cục bộ của bài là: “5 checks passed; simulation only, no API requests”. Các tình huống gồm công việc mới, gửi nhưng không biết đã tiếp nhận chưa, đã có mã rồi bị timeout, hoàn thành sau đối soát và dữ liệu gốc không bị sửa.

Đây chưa phải state machine đủ dùng cho production. Ví dụ không giải quyết sự kiện đến sai thứ tự, worker cạnh tranh, trạng thái kết thúc rồi nhận dữ liệu cũ, hay lưu trữ bền vững. Nếu áp dụng thật, cần thiết kế kiểm soát phiên bản và điều kiện chuyển trạng thái trước khi thêm retry.

5. Thiết kế phần lưu trữ theo ranh giới có thể gặp lỗi

Nếu nhận được mã tác vụ nhưng worker chết trước khi ghi cơ sở dữ liệu, hệ thống vẫn có khoảng trống cần đối soát. Vì vậy, đừng chỉ đặt assert ở một hàm thuần rồi kết luận toàn bộ quy trình đã chống gửi trùng.

Cần quyết định nơi ghi ý định gửi, cách ghi nhận phản hồi và cách tiến trình phục hồi tìm công việc chưa rõ kết quả. Với nhiều worker, quyền thực hiện một công việc cũng cần được kiểm soát để hai tiến trình không cùng đọc trạng thái ready rồi cùng gửi.

Khi đã có batch_id, giữ nó qua các lần khởi động lại. Nếu giao diện có nút thử lại, làm rõ nút đó truy vấn lại tiến độ hay tạo một lần dịch mới. Hai hành động cần điều kiện khác nhau và có thể có hệ quả chi phí khác nhau.

6. Quan sát vừa đủ để điều tra mà không lộ dữ liệu

Log hữu ích có thể gồm mã công việc nội bộ, bước đang thực hiện, lần quan sát và loại lỗi. Đừng đưa khóa API, toàn bộ ảnh đầu vào hay đường dẫn có quyền truy cập tạm thời vào log dùng chung. Khi cần liên kết một bản kết quả với bản nguồn, dùng định danh có kiểm soát thay vì sao chép nội dung vào mọi hệ thống giám sát.

Có thể theo dõi số công việc cần đối soát và tuổi của công việc cũ nhất trong nhóm đó. Các tín hiệu này giúp nhận ra hàng đợi đang bị mắc, nhưng ngưỡng cảnh báo cần dựa vào quy trình thực tế. Không nên đưa một con số tùy ý thành cam kết thời gian của dịch vụ.

Khi nhận được ảnh dịch, chuyển chúng vào vùng chờ duyệt. Trình duyệt hoặc API báo thành công không thay thế việc mở ảnh, đọc lời thoại tiếng Việt và xác nhận phiên bản cuối. Chỉ bản đã duyệt mới nên đi sang bước phát hành.

Mục tiêu của thiết kế này là giữ quyết định của client gắn với bằng chứng. Timeout làm giảm điều bạn biết về một tác vụ; nó không tự chứng minh tác vụ thất bại. Với một quy trình dịch ảnh dùng MangaTranslate hay một dịch vụ xử lý bất đồng bộ khác, phân biệt hai điều đó giúp nhóm vận hành chọn đúng giữa quan sát lại, đối soát và chủ động tạo một công việc mới.