Hướng Dẫn Thực Hành

Xử lý lỗi callback CaptchaAI: retry và hàng đợi thư chết

Callback (pingback) của CaptchaAI không đảm bảo giao đến — server của bạn có thể sập, trả về lỗi 5xx, hoặc handler crash đúng lúc CaptchaAI gửi kết quả về. Bài này đưa ra 3 mẫu xử lý cụ thể, kèm code Python và JavaScript chạy ngay:

  1. Callback + polling dự phòng — poll lại task nào chưa nhận callback sau một khoảng timeout.
  2. Hàng đợi thư chết (dead-letter queue) — không mất kết quả khi handler xử lý lỗi, lưu lại để retry sau.
  3. Trình xử lý idempotent — xử lý callback trùng lặp mà không sinh lỗi hay dữ liệu sai.

Tình huống thực tế: đội QA thuê ngoài ở TP.HCM

Một đội QA thuê ngoài ở TP.HCM chạy hàng nghìn task giải reCAPTCHA mỗi đêm cho khách hàng nước ngoài, trên một VPS giá rẻ tự động restart theo lịch lúc nửa đêm giờ Việt Nam — đúng lúc batch job đang chờ callback. Không có polling dự phòng, cả lô task đêm đó mất kết quả mà không ai biết cho tới sáng hôm sau. Ba mẫu dưới đây tồn tại vì lý do đó.

Các lỗi callback thường gặp

Server sập

CaptchaAI nhận "connection refused" khi cố gửi callback về — kết quả không được giao.

Server trả về mã 5xx

CaptchaAI nhận response lỗi; có thể không thử gửi lại tùy cách CaptchaAI triển khai retry.

Timeout mạng

Kết nối từ CaptchaAI bị treo giữa chừng — kết quả có thể bị mất mà không để lại log lỗi rõ ràng.

Handler crash

Request được nhận nhưng kết quả chưa lưu kịp trước khi handler crash — lỗi âm thầm, khó phát hiện nhất trong 4 loại trên.

Nguyên tắc: không bao giờ chỉ dựa vào callback. Luôn có một cơ chế dự phòng.

Mẫu 1: Callback kết hợp polling dự phòng

Cách đáng tin cậy nhất: chấp nhận callback ngay khi đến, đồng thời polling cho bất kỳ task nào chưa nhận được callback trong một khoảng thời gian nhất định.

Python

import os
import time
import threading
import requests
from flask import Flask, request

app = Flask(__name__)
API_KEY = os.environ["CAPTCHAAI_API_KEY"]

# Track task state
pending_tasks = {}  # task_id -> {"submitted_at": timestamp, "status": "pending"}
results = {}
lock = threading.Lock()

def submit_captcha(sitekey, pageurl, callback_url):
    """Submit with callback, but track for fallback polling."""
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": pageurl,
        "pingback": callback_url,
        "json": 1
    })
    data = resp.json()

    if data.get("status") == 1:
        task_id = data["request"]
        with lock:
            pending_tasks[task_id] = {
                "submitted_at": time.time(),
                "status": "pending"
            }
        return task_id
    return None

@app.route("/callback")
def captcha_callback():
    """Primary result delivery — CaptchaAI sends results here."""
    task_id = request.args.get("id")
    solution = request.args.get("code")

    with lock:
        results[task_id] = solution
        pending_tasks.pop(task_id, None)

    return "OK", 200

def fallback_poller():
    """Poll for any tasks that missed their callback."""
    while True:
        time.sleep(30)  # Check every 30 seconds

        with lock:
            stale_tasks = [
                tid for tid, info in pending_tasks.items()
                if time.time() - info["submitted_at"] > 120  # 2 min callback timeout
                and info["status"] == "pending"
            ]

        for task_id in stale_tasks:
            resp = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": API_KEY,
                "action": "get",
                "id": task_id,
                "json": 1
            })
            data = resp.json()

            if data.get("status") == 1:
                with lock:
                    results[task_id] = data["request"]
                    pending_tasks.pop(task_id, None)
                print(f"Fallback poll recovered: {task_id}")
            elif data.get("request") != "CAPCHA_NOT_READY":
                # Permanent error — remove from pending
                with lock:
                    pending_tasks.pop(task_id, None)
                print(f"Task failed: {task_id} — {data.get('request')}")

# Start fallback poller in background
poller_thread = threading.Thread(target=fallback_poller, daemon=True)
poller_thread.start()

JavaScript

const express = require("express");
const axios = require("axios");

const app = express();
const API_KEY = process.env.CAPTCHAAI_API_KEY;

const pendingTasks = new Map(); // taskId -> { submittedAt, status }
const results = new Map();

async function submitCaptcha(sitekey, pageurl, callbackUrl) {
  const resp = await axios.post("https://ocr.captchaai.com/in.php", null, {
    params: {
      key: API_KEY,
      method: "userrecaptcha",
      googlekey: sitekey,
      pageurl: pageurl,
      pingback: callbackUrl,
      json: 1,
    },
  });

  if (resp.data.status === 1) {
    const taskId = resp.data.request;
    pendingTasks.set(taskId, {
      submittedAt: Date.now(),
      status: "pending",
    });
    return taskId;
  }
  return null;
}

// Primary callback endpoint
app.get("/callback", (req, res) => {
  const taskId = req.query.id;
  const solution = req.query.code;

  results.set(taskId, solution);
  pendingTasks.delete(taskId);

  res.sendStatus(200);
});

// Fallback poller
setInterval(async () => {
  const now = Date.now();
  const staleTasks = [];

  for (const [taskId, info] of pendingTasks) {
    if (now - info.submittedAt > 120000 && info.status === "pending") {
      staleTasks.push(taskId);
    }
  }

  for (const taskId of staleTasks) {
    try {
      const resp = await axios.get("https://ocr.captchaai.com/res.php", {
        params: { key: API_KEY, action: "get", id: taskId, json: 1 },
      });

      if (resp.data.status === 1) {
        results.set(taskId, resp.data.request);
        pendingTasks.delete(taskId);
        console.log(`Fallback recovered: ${taskId}`);
      } else if (resp.data.request !== "CAPCHA_NOT_READY") {
        pendingTasks.delete(taskId);
        console.log(`Task failed: ${taskId} — ${resp.data.request}`);
      }
    } catch (err) {
      console.error(`Poll error for ${taskId}: ${err.message}`);
    }
  }
}, 30000);

app.listen(3000);

Mẫu 2: Hàng đợi thư chết (dead-letter queue)

Khi handler xử lý callback nhưng gặp lỗi (database sập, dữ liệu không hợp lệ), đừng để mất kết quả — chuyển task đó sang hàng đợi thư chết để xử lý lại sau, thay vì bỏ luôn.

Python

import json
import os
import time
from pathlib import Path

DEAD_LETTER_DIR = Path("dead_letter")
DEAD_LETTER_DIR.mkdir(exist_ok=True)

@app.route("/callback")
def captcha_callback_with_dlq():
    task_id = request.args.get("id")
    solution = request.args.get("code")

    try:
        # Attempt normal processing
        store_result(task_id, solution)
        return "OK", 200
    except Exception as e:
        # Processing failed — save to dead-letter queue
        dead_letter = {
            "task_id": task_id,
            "solution": solution,
            "error": str(e),
            "received_at": time.time()
        }
        dlq_path = DEAD_LETTER_DIR / f"{task_id}.json"
        dlq_path.write_text(json.dumps(dead_letter))

        print(f"DLQ: {task_id} — {e}")
        return "OK", 200  # Still return 200 to CaptchaAI

def reprocess_dead_letters():
    """Retry processing dead-letter items."""
    for dlq_file in DEAD_LETTER_DIR.glob("*.json"):
        item = json.loads(dlq_file.read_text())

        try:
            store_result(item["task_id"], item["solution"])
            dlq_file.unlink()  # Remove after successful processing
            print(f"DLQ reprocessed: {item['task_id']}")
        except Exception:
            pass  # Leave in DLQ for next retry

JavaScript

const fs = require("fs");
const path = require("path");

const DLQ_DIR = path.join(__dirname, "dead_letter");
if (!fs.existsSync(DLQ_DIR)) fs.mkdirSync(DLQ_DIR);

app.get("/callback-dlq", (req, res) => {
  const taskId = req.query.id;
  const solution = req.query.code;

  try {
    storeResult(taskId, solution);
    res.sendStatus(200);
  } catch (err) {
    // Save to dead-letter queue
    const deadLetter = {
      task_id: taskId,
      solution: solution,
      error: err.message,
      received_at: Date.now(),
    };

    fs.writeFileSync(
      path.join(DLQ_DIR, `${taskId}.json`),
      JSON.stringify(deadLetter)
    );

    console.log(`DLQ: ${taskId} — ${err.message}`);
    res.sendStatus(200); // Still acknowledge to CaptchaAI
  }
});

function reprocessDeadLetters() {
  const files = fs.readdirSync(DLQ_DIR).filter((f) => f.endsWith(".json"));

  for (const file of files) {
    const filePath = path.join(DLQ_DIR, file);
    const item = JSON.parse(fs.readFileSync(filePath, "utf8"));

    try {
      storeResult(item.task_id, item.solution);
      fs.unlinkSync(filePath);
      console.log(`DLQ reprocessed: ${item.task_id}`);
    } catch (err) {
      // Leave in DLQ
    }
  }
}

// Retry DLQ every 5 minutes
setInterval(reprocessDeadLetters, 300000);

Mẫu 3: Trình xử lý callback dạng idempotent

CaptchaAI có thể gửi cùng một callback nhiều lần (retry phía CaptchaAI, mạng chập chờn…). Trình xử lý cần idempotent — chạy lại bao nhiêu lần cũng chỉ ra đúng một kết quả:

@app.route("/callback")
def idempotent_callback():
    task_id = request.args.get("id")
    solution = request.args.get("code")

    with lock:
        # Only process if not already handled
        if task_id in results:
            return "OK", 200  # Already processed — skip silently

        results[task_id] = solution
        pending_tasks.pop(task_id, None)

    return "OK", 200

Khắc phục sự cố

Các lỗi thường gặp khi vận hành ba mẫu trên và cách xử lý:

Polling dự phòng tìm thấy task đã được giao trước đó

Nguyên nhân: race condition giữa callback và polling. Cách xử lý: thêm kiểm tra idempotency, bỏ qua nếu đã có kết quả.

Dead-letter queue phình to mà không được xử lý lại

Nguyên nhân: reprocessor không chạy hoặc bị lỗi. Cách xử lý: kiểm tra log reprocessor; xử lý dứt điểm nguyên nhân gốc (thường là DB).

Callback trả về 200 nhưng kết quả vẫn mất

Nguyên nhân: handler crash sau khi đã phản hồi. Cách xử lý: xử lý xong dữ liệu rồi mới phản hồi, hoặc dùng mẫu DLQ.

Quá nhiều request polling dự phòng

Nguyên nhân: quá nhiều task bị coi là "cũ". Cách xử lý: tăng ngưỡng timeout callback; kiểm tra uptime server.

Chọn mẫu nào cho hệ thống của bạn

Ba mẫu trên không loại trừ nhau — chọn theo khối lượng task và mức độ khắt khe của SLA:

  • Khối lượng thấp, thỉnh thoảng downtime → callback + polling dự phòng.
  • Khối lượng lớn, có nguy cơ mất kết nối database → hàng đợi thư chết.
  • Nhiều consumer có thể cùng xử lý một kết quả → trình xử lý idempotent.
  • Hệ thống production có SLA → kết hợp cả ba.

Câu hỏi thường gặp

Có nên luôn trả về 200 cho callback CaptchaAI, kể cả khi xử lý nội bộ bị lỗi?

Có. Trả mã lỗi (4xx/5xx) không giúp ích gì — CaptchaAI có thể không thử gửi lại callback. Luôn trả 200 ngay khi nhận được, còn lỗi xử lý nội bộ thì xử lý bằng DLQ hoặc polling dự phòng.

Nên đợi bao lâu trước khi kích hoạt polling dự phòng?

Tối thiểu 120 giây kể từ lúc gửi task. Phần lớn CAPTCHA giải xong trong 10–60 giây; khoảng thời gian còn lại là để trừ hao độ trễ mạng khi CaptchaAI gửi callback về.

Nên lưu trạng thái pending_tasks và dead-letter queue ở đâu khi chạy nhiều instance?

Không nên dùng biến trong bộ nhớ (dict/Map) như ví dụ trên nếu chạy nhiều instance song song — mỗi instance có bản trạng thái riêng, dễ trùng hoặc sót task. Chuyển sang Redis hoặc một bảng database dùng chung để tất cả instance đọc/ghi cùng trạng thái.

Làm sao biết endpoint /callback của mình có nhận đúng request từ CaptchaAI không?

Ghi log toàn bộ request đến trước khi xử lý (task_id, code, thời điểm) và đối chiếu với task_id đã submit. Nếu log trống dù task đã gửi từ lâu, khả năng cao là firewall/NAT đang chặn IP của CaptchaAI hoặc server chưa mở public port cho route callback.

Dead-letter queue nên lưu bằng file JSON, Redis hay database?

File JSON đủ dùng cho khối lượng nhỏ, dễ kiểm tra thủ công. Ở quy mô lớn hơn, chuyển sang Redis (TTL tự động) hoặc một bảng database riêng để dễ giám sát và dọn dẹp theo lịch.

Bài viết liên quan

Bước tiếp theo

Triển khai callback handler chịu lỗi tốt cho hệ thống của bạn — lấy API key CaptchaAI và áp dụng các mẫu polling dự phòng, dead-letter queue, idempotent handler ở trên.

Hướng dẫn liên quan:

Os comentários estão desativados para este artigo.