Xử lý lỗi 503 và Retry-After khi gọi API tạo hình ảnh, kèm hàng đợi
Phân biệt lỗi nên thử lại và lỗi phải dừng, vòng thử lại có giãn cách theo Retry-After, và một hàng đợi SQLite nhỏ giúp chạy hàng trăm lượt vẽ không mất việc nào.

Lần đầu gọi API AI Hình Ảnh trong vòng lặp hai trăm dòng, nhiều người gặp cùng một cảnh: chạy êm được ba mươi lượt, rồi một tràng mã 503, rồi đoạn mã dừng hẳn vì không ai bắt lỗi. Hoặc tệ hơn: đoạn mã thử lại liên tục không nghỉ, càng làm máy vẽ bận thêm. Bài này đưa ra cách xử lý lỗi đúng với API này, kèm mã Python chạy được và một hàng đợi nhỏ bằng SQLite để việc chạy hàng loạt không bao giờ mất dòng nào.
Vì sao có 503
Máy vẽ của AI Hình Ảnh đặt tại Việt Nam và dùng chung với các dịch vụ khác của hệ AI Đỉnh. Mỗi lượt gọi API là đồng bộ: máy chủ nhận yêu cầu, vẽ, rồi trả hình ảnh trong cùng một kết nối, chờ tối đa khoảng 90 giây. Vì vậy số lượt đang xử lý cùng lúc có giới hạn. Khi vượt giới hạn, hoặc máy vẽ đang nhận việc từ nơi khác, API trả 503 kèm header Retry-After cho biết nên chờ bao nhiêu giây.
503 là lỗi tạm thời và bình thường. Hệ thống của bạn phải coi nó như "đèn đỏ", không phải "đường cụt".
Phân loại lỗi: thử lại hay dừng
| Mã | Nghĩa | Thử lại? | Việc cần làm |
|---|---|---|---|
| 200 | Thành công (có thể ít hình ảnh hơn n) | — | Lưu những hình ảnh nhận được |
| 400 | Tham số sai, thiếu prompt | Không | Ghi lỗi vào dòng đó, sửa dữ liệu |
| 401 | Thiếu khoá, sai khoá, khoá đã tắt | Không | Dừng toàn bộ hàng đợi, kiểm tra khoá |
| 402 | Không đủ điểm | Không | Dừng toàn bộ hàng đợi, nạp điểm |
| 503 | Máy vẽ bận | Có | Chờ đúng Retry-After, cộng thêm vài giây ngẫu nhiên |
| Lỗi 5xx khác | Lỗi phía máy chủ | Có, ít lần | Chờ lâu hơn, quá số lần thì đánh dấu lỗi |
| Hết giờ, mất mạng phía bạn | Kết nối đứt | Có, cẩn thận | Ghi lại để đối chiếu điểm (xem dưới) |
Hai mã 401 và 402 đặc biệt: chúng không phải lỗi của một dòng mà của cả tài khoản. Thử lại dòng tiếp theo cũng sẽ lỗi y hệt. Dừng ngay, báo người phụ trách.
Trường hợp 200 nhưng thiếu hình ảnh
Khi gửi n lớn với hạng chậm, máy vẽ từng hình ảnh lần lượt trong khoảng 90 giây. Không kịp thì trả về số hình ảnh đã vẽ được, phần còn lại hoàn điểm. Đây không phải lỗi, nhưng mã của bạn phải đếm len(data) thực nhận và, nếu cần đủ số, xếp thêm một lượt cho phần thiếu. usage.images và usage.credits trong kết quả cho biết chính xác số hình ảnh và số điểm đã tính.
Vòng thử lại đúng cách
Ba nguyên tắc:
- Tôn trọng
Retry-After. Máy chủ biết rõ hơn bạn khi nào rảnh. - Thêm độ lệch ngẫu nhiên. Nếu bạn chạy vài tiến trình, chúng không được cùng thức dậy một giây rồi lại cùng nhận 503.
- Giới hạn số lần. Sau năm lần, đánh dấu dòng là lỗi tạm thời và đi tiếp, để chạy lại sau.
```python
import os
import random
import time
import requests
URL = "https://aihinhanh.com/v1/images/generations"
KEY = os.environ["AHN_KEY"] # ahn_XXXX
class LoiDungHan(Exception):
"""401, 402: lỗi của cả tài khoản, phải dừng hàng đợi."""
class LoiDuLieu(Exception):
"""400: lỗi của riêng dòng này, sửa dữ liệu rồi chạy lại."""
class LoiTamThoi(Exception):
"""Thử đủ số lần mà vẫn bận: để lượt sau."""
def ve(payload: dict, so_lan: int = 5) -> dict:
cho_mac_dinh = 15
loi_cuoi = ""
for lan in range(1, so_lan + 1):
retry_after = None
try:
r = requests.post(
URL,
json=payload,
headers={"Authorization": f"Bearer {KEY}"},
timeout=120,
)
except requests.RequestException as e:
loi_cuoi = f"mạng: {e}"
else:
if r.status_code == 200:
return r.json()
try:
thong_bao = r.json().get("error", {}).get("message", "")
except ValueError:
thong_bao = r.text[:200]
if r.status_code in (401, 402):
raise LoiDungHan(f"{r.status_code}: {thong_bao}")
if r.status_code == 400:
raise LoiDuLieu(thong_bao)
loi_cuoi = f"{r.status_code}: {thong_bao}"
retry_after = r.headers.get("Retry-After")
if retry_after and retry_after.isdigit():
cho = int(retry_after)
else:
cho = cho_mac_dinh * (2 ** (lan - 1)) # 15, 30, 60, 120...
cho = min(cho, 300) + random.uniform(0, 6)
print(f" lần {lan} lỗi ({loi_cuoi}), chờ {cho:.0f}s")
time.sleep(cho)
raise LoiTamThoi(loi_cuoi)
```
Hàm trả về nguyên kết quả JSON khi thành công, và ném ba loại ngoại lệ khác nhau để phía gọi xử lý đúng: dừng hẳn, bỏ qua dòng, hay để lượt sau.
Một chi tiết về hết giờ phía bạn
Nếu kết nối đứt hoặc hết 120 giây phía bạn, bạn không biết lượt đó đã được xử lý tới đâu. Điểm được trừ khi máy chủ nhận yêu cầu, và hình ảnh không vẽ được thì hoàn điểm. Nhưng nếu đứt mạng đúng lúc hình ảnh đã vẽ xong và đang trả về, lượt đó có thể đã được tính. Đây là lý do không nên đặt thời gian chờ quá ngắn (dưới 100 giây), và nên ghi lại thời điểm những lượt đứt mạng để đối chiếu với lịch sử điểm nếu thấy chênh.
Hàng đợi: đừng giữ việc trong bộ nhớ
Vòng for trên danh sách hai trăm dòng có một điểm yếu: tắt máy, mất điện, hay một lỗi lạ ở dòng 137 là phải chạy lại từ đầu, hoặc tệ hơn, chạy lại cả những dòng đã trừ điểm. Hàng đợi bằng một tệp SQLite giải quyết chuyện này với chưa tới năm mươi dòng mã.
```python
import base64
import json
import sqlite3
import time
from pathlib import Path
DB = sqlite3.connect("hang_doi.sqlite")
DB.execute("""CREATE TABLE IF NOT EXISTS viec(
id INTEGER PRIMARY KEY,
ten TEXT UNIQUE,
payload TEXT NOT NULL,
trang_thai TEXT NOT NULL DEFAULT 'cho',
so_lan INTEGER NOT NULL DEFAULT 0,
lan_sau REAL NOT NULL DEFAULT 0,
loi TEXT,
diem INTEGER NOT NULL DEFAULT 0)""")
DB.commit()
THU_MUC = Path("ra")
THU_MUC.mkdir(exist_ok=True)
def them(ten: str, payload: dict) -> None:
DB.execute("INSERT OR IGNORE INTO viec(ten, payload) VALUES (?, ?)",
(ten, json.dumps(payload, ensure_ascii=False)))
DB.commit()
def chay() -> None:
while True:
row = DB.execute(
"SELECT id, ten, payload, so_lan FROM viec "
"WHERE trang_thai IN ('cho', 'tam') AND lan_sau <= ? "
"ORDER BY id LIMIT 1", (time.time(),)).fetchone()
if not row:
con = DB.execute("SELECT COUNT(*) FROM viec WHERE trang_thai='tam'").fetchone()[0]
if con == 0:
print("Xong hàng đợi.")
return
time.sleep(30)
continue
vid, ten, payload, so_lan = row
try:
kq = ve(json.loads(payload))
except LoiDungHan as e:
print("DỪNG:", e)
return
except LoiDuLieu as e:
DB.execute("UPDATE viec SET trang_thai='hong', loi=? WHERE id=?", (str(e), vid))
except LoiTamThoi as e:
moi = 'tam' if so_lan + 1 < 3 else 'hong' # quá 3 lượt thì để người xem
DB.execute("UPDATE viec SET trang_thai=?, so_lan=so_lan+1, loi=?, "
"lan_sau=? WHERE id=?", (moi, str(e), time.time() + 600, vid))
else:
for i, d in enumerate(kq["data"], 1):
(THU_MUC / f"{ten}-{i}.webp").write_bytes(base64.b64decode(d["b64_json"]))
DB.execute("UPDATE viec SET trang_thai='xong', diem=? WHERE id=?",
(kq["usage"]["credits"], vid))
DB.commit()
```
Dùng:
```python
them("bia-bai-pho-ha-noi", {"model": "plus", "size": "1536x1024", "n": 1,
"output_format": "webp",
"prompt": "A steaming bowl of pho on a street stall table in Hanoi, morning light, realistic photo"})
chay()
```
Những gì hàng đợi này cho bạn:
- Không chạy lại dòng đã xong. Cột
tenlà duy nhất; thêm lại cùng tên bị bỏ qua, dòngxongkhông bao giờ được chọn lại. - Dòng lỗi tạm thời được để lượt sau, mười phút sau mới thử lại, trong khi các dòng khác vẫn chạy; quá ba lượt vẫn bận thì chuyển sang
hongđể người xem. - Dòng lỗi dữ liệu đứng yên với trạng thái
hongvà lý do trong cộtloi, chờ người sửa. - Dừng sạch khi hết điểm hay sai khoá, chạy lại sau khi xử lý thì tiếp tục đúng chỗ.
- Cột
diemcộng lại là tổng chi phí thật của cả đợt.
Một tiến trình hay nhiều tiến trình
Bản năng của lập trình viên là chạy song song cho nhanh. Với API này, chạy song song nhiều hơn hai luồng thường chậm hơn tổng thể: các luồng tranh nhau chỗ, nhận 503, ngủ, rồi lại tranh. Mỗi tài khoản trên trang web cũng chỉ vẽ một lượt một lúc, và API đi chung máy vẽ đó.
| Cách chạy | Ưu | Nhược |
|---|---|---|
| Một tiến trình tuần tự | Đơn giản, ít 503, dễ đoán thời gian | Không tận dụng lúc máy rảnh |
| Hai tiến trình | Nhanh hơn chút khi máy rảnh | Phải khoá dòng khi chọn việc |
| Nhiều hơn hai | Không nhanh hơn | 503 dày, khó theo dõi |
Nếu chạy hai tiến trình với hàng đợi trên, cần đánh dấu dòng là dang trong cùng một giao dịch trước khi gọi ve(), để hai tiến trình không chọn cùng một dòng.
Ước lượng thời gian một đợt
Một cửa hàng gốm sứ Bát Tràng cần 180 hình ảnh hạng Plus khổ vuông cho gian hàng mới. Mỗi hình ảnh Plus mất khoảng 6–10 giây; gửi n: 2 mỗi lượt thì mỗi lượt khoảng 15–20 giây cộng thời gian truyền. 90 lượt tuần tự là khoảng 25–35 phút nếu máy rảnh, và có thể lâu gấp đôi vào giờ bận. Hãy cho đợt chạy bắt đầu từ tối hôm trước, đừng chờ sáng hôm cần đăng.
Chi phí thì tính trước được chính xác: 180 × 500 = 90.000 điểm. Cách đặt trần chi phí cho cả đợt có ở bài tạo hàng loạt hình ảnh sản phẩm từ CSV.
Tóm lại
- 503 là đèn đỏ: chờ đúng
Retry-After, cộng độ lệch ngẫu nhiên, giới hạn số lần. - 400 là lỗi của dòng; 401, 402 là lỗi của tài khoản, phải dừng cả hàng đợi.
- 200 có thể ít hình ảnh hơn
n, phần thiếu đã được hoàn điểm. - Giữ việc trong hàng đợi SQLite, chạy một hoặc hai tiến trình.
Tạo khoá ở trang khoá API; nếu đang chuyển từ OpenAI, xem trước bài chuyển mã OpenAI Images sang AI Hình Ảnh.



