Ngiêm cấm sử đụng PROXY và VPS vào mục đích trái pháp luật bạn sẽ phải chịu toàn bộ trách nhiệm trước pháp luật khi sử dụng dịch vụ của chúng tôi tks!
WireMock mô phỏng origin lỗi: delay, fault và ma trận retry

WireMock mô phỏng origin lỗi: delay, fault và ma trận retry

Chuyên viên Kỹ thuật ProxyVN 05-07-2026 Đang tính... phút đọc

WireMock mô phỏng origin lỗi là cách dựng máy chủ giả có độ trễ và phản hồi hỏng để kiểm downloader trước khi gặp lỗi thật. Bài này viết cho người viết crawler cần timeout, retry đúng. ProxyVN chạy WireMock 2.35.2 thật trên Windows ngày 05/11/2026 với 14 stub, phát hiện gzip làm mất hiệu ứng dribble, và thấy CONNECTION_RESET_BY_PEER không treo như tài liệu cảnh báo.

WireMock mô phỏng origin lỗi: mục tiêu và phạm vi không lặp

Phần này chốt bài toán, đầu ra và ranh giới với các bài sửa lỗi timeout.

Bài toán cần giải

Một downloader chạy tốt khi thử, rồi kẹt cả đêm trên một trang trả dữ liệu rất chậm. Hôm khác, nó thử lại liên tục trên lỗi không thể tự hết, làm origin càng quá tải. Nhóm không có cách nào tái hiện các ca này theo yêu cầu, vì website thật không hỏng đúng lúc mình cần kiểm.

Đầu ra nghiệm thu

Đầu ra là bộ stub lỗi có kiểm soát cùng ma trận kết quả: mỗi lỗi, mỗi client, ra exception gì, sau bao lâu, retry bao nhiêu lần. Ma trận này là tài liệu sống của downloader. Khi đổi thư viện HTTP, anh em chạy lại và so bảng.

Ranh giới với các bài khác

Bài VCR.py trong cùng cụm phát lại response thật đã ghi, hợp cho parser. Bài này tạo ra lỗi không có sẵn. Bài lỗi 408 Request Timeout và bài 504 Gateway Timeout giải thích lỗi thực tế và cách xử lý ở phía vận hành. Bài proxy load testing nói về kiểm tải. Ở đây không có checklist sửa proxy chậm.

Phạm vi lab

Lab chạy WireMock standalone bản jre8 2.35.2, vì máy lab chỉ có Java 8. Tệp jar tải từ Maven Central, SHA1 khớp với tệp .sha1 đi kèm. Mọi thứ chạy trên 127.0.0.1 cổng 9591. Client là requests 2.34.2 và httpx 0.28.1. Không có website bên ngoài nào bị gọi.

Về phiên bản

Tài liệu hiện tại của WireMock nói về bản mới hơn, có bản 3 và 4 beta. Bản 2.35.2 lab dùng là bản cũ, chọn theo Java có trên máy. Tên fault và JSON của stub trong bài chạy được trên bản này. Anh em dùng bản mới nên đối chiếu lại, nhất là các tham số dòng lệnh.

Ai nên đọc

Bài hợp với kỹ sư viết crawler, API client, người phụ trách độ tin cậy của pipeline thu thập. Người đang gặp lỗi trên production nên đọc bài sửa lỗi cụ thể trước. Bài này giúp ngăn lỗi lặp lại bằng test.

Thuật ngữ dùng trong bài

Stub là một luật nối request với response giả. Fault là lỗi mức socket hoặc giao thức. Dribble là gửi thân response nhỏ giọt theo từng chunk. Scenario là stub có trạng thái, đổi theo số lần gọi. Read timeout là thời gian tối đa chờ giữa hai lần nhận dữ liệu, không phải tổng thời gian tải.

Dựng origin giả có delay và malformed response bằng WireMock mô phỏng origin lỗi

Phần này đi từ chạy jar tới nạp stub đầu tiên qua API quản trị.

Nguồn tài liệu đã đọc

Lab đọc trang Simulating Faults của WireMock cho delay, dribble và fault. Trang Stateful Behaviour cho scenario, và trang Standalone cho cách chạy jar. Phần retry của client dựa vào tài liệu urllib3 Retry.

Chạy standalone

WireMock chạy bằng một lệnh java. Lab tắt banner cho log gọn. Sau vài giây, đường dẫn quản trị /__admin/mappings trả danh sách rỗng, nghĩa là máy chủ sẵn sàng.

import subprocess, time, requests
 
p = subprocess.Popen(["java", "-jar", "wiremock.jar", "--port", "9591", "--disable-banner"],
                     stdout=open("wm.log", "w"), stderr=subprocess.STDOUT)
A = "http://127.0.0.1:9591"
for _ in range(30):
    try:
        if requests.get(A + "/__admin/mappings", timeout=1).ok:
            break
    except requests.RequestException:
        time.sleep(0.5)

Vòng chờ giúp test không bắt đầu trước khi máy chủ lên. Máy có Java mới thì dùng bản wiremock-standalone mới hơn. Trong CI, Docker là lựa chọn gọn nếu môi trường cho phép.

Nạp stub qua API quản trị

Thay vì tệp, lab POST từng stub JSON lên /__admin/mappings và nhận 201. Trước đó gọi /__admin/mappings/reset để không bị stub cũ ảnh hưởng. Cách này giúp mỗi bộ test tự dựng trạng thái mình cần.

stubs = [
    {"request": {"urlPath": "/ok"},
     "response": {"status": 200, "body": "ok body",
                  "headers": {"Content-Type": "text/plain"}}},
    {"request": {"urlPath": "/s503"},
     "response": {"status": 503, "headers": {"Retry-After": "1"}, "body": "busy"}},
]
requests.post(A + "/__admin/mappings/reset")
for x in stubs:
    r = requests.post(A + "/__admin/mappings", json=x)
    assert r.status_code == 201, r.text
print(requests.get(A + "/__admin/mappings").json()["meta"])

Bộ đầy đủ của lab có 14 stub, API báo total 14. Lab xuất toàn bộ ra stubs.json để đưa vào kho mã. Tệp này chính là tài sản nghiệm thu.

Cách đặt tên đường dẫn

Lab đặt tên theo lỗi: /fixed, /rand, /dribble, /empty, /garbage, /malformed, /reset, /badjson, /wrongtype, /flap. Đọc ma trận là biết ngay ca nào. Dùng urlPath để không phụ thuộc query. Mỗi ca một đường dẫn giúp đếm số lần gọi chính xác.

Không có stub thì sao

Gọi đường dẫn chưa khai báo nhận 404 với thân 1023 byte, và log ghi Request was not matched kèm so sánh với stub gần nhất. Khi test thất bại vì 404 lạ, xem log này trước. WireMock cho biết lý do khá rõ, giúp tiết kiệm thời gian dò.

Đếm số lần gọi

API /__admin/requests/count nhận bộ lọc và trả số lần gọi. Đây là cách đo retry chính xác nhất, vì đo từ phía máy chủ. Trước mỗi phép thử, lab gọi /__admin/requests/reset. Con số này không phụ thuộc lời khai của client.

def hits(path):
    return requests.post(A + "/__admin/requests/count", json={"url": path}).json()["count"]
 
requests.post(A + "/__admin/requests/reset")

Hàm hits được dùng ở mọi phép thử retry bên dưới.

Giữ tách biệt với mạng thật

Downloader trong test chỉ cần đổi base URL thành 127.0.0.1:9591. Cấu hình base URL phải đổi được từ môi trường. Nếu code viết chết tên miền, phần lớn lợi ích của origin giả biến mất. Đây là thay đổi nhỏ trong code mà đem lại nhiều giá trị nhất.

Khai báo stub origin trả status hoặc body fixture trong WireMock mô phỏng origin lỗi

Phần này nói về các lỗi không nằm ở socket, tức là lỗi trông giống thành công.

Trạng thái lỗi rõ ràng

Lỗi trông như thành công

Lỗi trông như thành công

Stub /s503 trả 503 kèm Retry-After. Requests và httpx đều không ném exception với 503, chỉ trả response. Vì vậy downloader phải tự kiểm mã. Một script không có raise_for_status sẽ lưu thân busy như nội dung trang. Lỗi này rất phổ biến trong crawler viết nhanh.

JSON hỏng

Stub /badjson trả 200, Content-Type application/json, thân {"items": [1, 2, bị cắt giữa chừng. Cả hai client đều báo thành công 200 với 16 byte. Chỉ đến lúc gọi r.json(), lab mới nhận JSONDecodeError. Một origin trả thân cụt mà vẫn đóng kết nối đúng cách thì mã HTTP không cho biết gì.

r = requests.get(A + "/badjson", timeout=(1, 1.5))
try:
    r.json()
except ValueError as e:
    print(type(e).__name__, str(e)[:60])

Output là JSONDecodeError với vị trí ký tự 16. Downloader nên tách lỗi này khỏi lỗi mạng, vì retry có thể hữu ích hoặc không tùy nguyên nhân.

Trả sai loại nội dung

Stub /wrongtype trả 200 text/html với thân Access denied. Đây là mẫu rất hay gặp: một lớp bảo vệ trả trang HTML khi anh em chờ JSON. HTTP nói thành công, nội dung nói thất bại. Downloader phải kiểm Content-Type trước khi parse.

Hàm phân loại response

Lab gom các kiểm tra vào một hàm trả nhãn, để pipeline có thể đếm từng loại. Nhãn này là ngôn ngữ chung giữa test và log sản xuất.

def classify(r, want="application/json"):
    if r.status_code >= 500:
        return "server_loi"
    if r.status_code >= 400:
        return "client_loi"
    if want not in r.headers.get("content-type", ""):
        return "sai_loai"
    try:
        r.json()
    except ValueError:
        return "json_hong"
    return "ok"
 
for p in ("/s503", "/badjson", "/wrongtype", "/khong-co-stub"):
    print(p, classify(requests.get(A + p, timeout=(1, 1.5))))

Output thật lần lượt là server_loi, json_hong, sai_loai, client_loi. Tên nhãn là tự đặt, anh em đổi theo quy ước log của dự án.

Fixture thân response

Stub có thể đọc thân từ tệp trong thư mục __files bằng bodyFileName. Với trang HTML lớn, đặt fixture ra tệp cho dễ xem. Lab dùng body nội tuyến vì thân rất ngắn. WireMock tự tạo hai thư mục __files và mappings ngay nơi chạy jar.

Khớp request theo điều kiện

Stub khớp theo phương thức, đường dẫn, query, header hoặc thân. Ví dụ chỉ trả 429 khi thiếu một header. Lab không dùng hết các điều kiện này. Khớp đơn giản theo urlPath là đủ cho ma trận lỗi và làm test dễ đọc.

Trạng thái 429

Chưa có stub 429 trong lab, nhưng cách làm giống /s503, thêm Retry-After. Downloader nên tôn trọng header này. Cách đặt nhịp có ở bài rate limit API khi dùng proxy. Mô phỏng 429 giúp kiểm hàng đợi có lùi đúng không, trước khi chạm website thật.

Ghi nhớ về mã thành công

Bốn ca ở trên cho thấy một điều: lỗi quan trọng nhất của downloader thường không ném exception. Test chỉ kiểm timeout mà bỏ các ca 200 hỏng thì bỏ sót nửa số sự cố thật. Ma trận phải có cả cột "kết quả nghiệp vụ", không chỉ cột exception.

Thêm fixed, random delay và chunked dribble theo lab WireMock mô phỏng origin lỗi

Phần này có phát hiện lớn nhất của bài: nén làm thay đổi kết quả.

Fixed delay

Gzip nuốt dribble

Gzip nuốt dribble

Stub /fixed đặt fixedDelayMilliseconds 3000. Với read timeout 1.5 giây, requests ném ReadTimeout sau 1.52 giây, httpx ném ReadTimeout sau 1.71 giây. Phần 0.2 giây chênh của httpx đến từ việc lab tạo client mới mỗi lần, không phải do httpx chậm. Với ca này, read timeout hoạt động đúng như tên gọi.

{"request": {"urlPath": "/fixed"},
 "response": {"status": 200, "fixedDelayMilliseconds": 3000, "body": "muon"}}

Stub này mô phỏng origin bận trước khi gửi byte đầu tiên. Đây là ca dễ nhất của timeout.

Random delay

Stub /rand dùng delayDistribution loại uniform từ 200 tới 800 mili giây, lượt của lab mất 0.41 giây. Tài liệu còn có lognormal với median và sigma, mô tả tốt độ trễ đuôi dài của hệ thống thật. Random delay hợp cho kiểm tải và hàng đợi, ít hợp cho test đơn vị vì kết quả thay đổi. Test cần lặp lại ổn định nên dùng fixed delay.

Chunked dribble là gì

Stub /dribble gửi thân 2000 byte chia 5 chunk trong tổng 4000 mili giây. Mỗi chunk cách nhau khoảng 0.8 giây. Mục đích là mô phỏng origin nhỏ giọt: luôn có dữ liệu, nên read timeout không kích hoạt, nhưng tổng thời gian kéo dài.

Kết quả đầu tiên khó hiểu

Theo lý thuyết, read timeout 1.5 giây không nên kích hoạt vì khoảng trống chỉ 0.8 giây. Nhưng cả hai client đều lỗi: requests ConnectionError sau 2.34 giây, httpx ReadTimeout sau 2.53 giây. Con số 2.33 đúng bằng 0.83 giây chờ header cộng 1.5 giây. Dữ liệu dường như không về từng phần.

Đo bằng socket thô

Lab gửi request bằng socket trực tiếp, không có Accept-Encoding. Từng chunk 400 byte về đúng các mốc 0.81, 1.62, 2.43, 3.24, 4.05 giây. Vậy WireMock có nhỏ giọt thật. Khác biệt nằm ở header client gửi đi.

Gzip nuốt hiệu ứng dribble

Requests và httpx mặc định gửi Accept-Encoding gzip. WireMock lúc đó nén response và trả Content-Encoding gzip, khối nén gom lại nên client chỉ thấy dữ liệu ở cuối. Lab đo hai cấu hình.

Bảng: Gzip nuốt hiệu ứng dribble

Accept-Encoding

Content-Encoding

Mốc nhận dữ liệu

Read timeout 1.5 s

gzip, deflate

gzip

4.06 s, một khối 2000 byte

Lỗi ở 2.35 s

identity

không có

1.62, 2.43, 3.23, 4.04, 4.04 s, 400 byte mỗi khối

Xong ở 4.06 s

Ý nghĩa của phát hiện

Muốn test dribble đúng, anh em phải tắt nén ở client hoặc ở WireMock. Nếu không, test đang đo một hành vi khác hẳn. Cũng từ đó rút ra bài học thực tế: origin hoặc proxy nén có thể đổi cách dữ liệu đến, và cùng một trang có thể gây timeout hay không tùy header. Bản 2.35.2 có cờ --disable-gzip ở phía máy chủ, lab dùng cách đổi header client vì không phải khởi động lại.

import time
 
t = time.time()
seen = []
with requests.get(A + "/dribble", stream=True, timeout=(1, 6),
                  headers={"Accept-Encoding": "identity"}) as r:
    for c in r.iter_content(None):
        seen.append((round(time.time() - t, 2), len(c)))
print(seen)

Output thật là các cặp 1.62 với 400, 2.43 với 400, 3.23 với 400, 4.04 với 400, 4.04 với 400. Mốc nhận của client trễ hơn socket thô một nhịp, vì hàm đọc chunk chờ dấu kết thúc chunk đi cùng khối sau.

Read timeout không chặn nhỏ giọt

Với identity và read timeout 1.5 giây, tải xong trong 4.06 giây mà không có lỗi. Giả sử origin nhỏ giọt một giờ, read timeout vẫn không kích hoạt. Đây chính là tình huống downloader kẹt cả đêm ở đầu bài. Timeout theo tên thư viện không phải deadline tổng.

Deadline tổng phải tự làm

Lab đặt deadline tổng 2 giây bằng cách kiểm trong vòng lặp. Với identity, nó dừng ở 3.23 giây sau 1200 byte, chứ không phải đúng 2 giây, vì chỉ kiểm được khi chunk mới về. Với gzip, nó chỉ dừng ở 4.05 giây sau khi có đủ dữ liệu. Muốn chặt hơn, chạy trong tiến trình hoặc tác vụ có thể hủy từ bên ngoài.

def fetch(url, deadline=2.0):
    t0 = time.time()
    buf = bytearray()
    with requests.get(url, stream=True, timeout=(1, 1.5),
                      headers={"Accept-Encoding": "identity"}) as r:
        for c in r.iter_content(None):
            buf += c
            if time.time() - t0 > deadline:
                raise TimeoutError(f"qua deadline, da nhan {len(buf)} byte")
    return bytes(buf)

Hàm này báo được số byte đã nhận, có ích khi ghi log. Giới hạn kích thước tải cũng nên kiểm cùng chỗ.

Httpx với identity

Lab gọi httpx.get với identity và read timeout 1.5, nhận 200 với 2000 byte sau 4.37 giây. Httpx có timeout riêng cho connect, read, write, pool, và cũng không có tổng deadline theo nghĩa này. Kết luận giống requests.

Tạo malformed response trên origin giả riêng với WireMock mô phỏng origin lỗi

Các fault làm hỏng giao thức, chỉ công cụ kiểu này mới tái hiện dễ dàng.

Bốn fault lab dùng

Ma trận fault

Ma trận fault

Tài liệu mô tả EMPTY_RESPONSE là không gửi gì rồi đóng kết nối. MALFORMED_RESPONSE_CHUNK gửi header OK rồi rác rồi đóng. RANDOM_DATA_THEN_CLOSE gửi rác rồi đóng. CONNECTION_RESET_BY_PEER đóng với SO_LINGER 0, thường gây lỗi connection reset. Stub chỉ cần khai trường fault.

{"request": {"urlPath": "/malformed"},
 "response": {"fault": "MALFORMED_RESPONSE_CHUNK"}}

Các fault này chỉ dùng trên máy chủ giả. Đừng thử gửi dữ liệu kiểu này vào hệ thống của người khác.

Ma trận thật

Bảng dưới là output của lab, connect timeout 1 giây, read timeout 1.5 giây.

Bảng: Ma trận thật

Đường dẫn

requests

Thời gian

httpx

Thời gian

/ok

200, 7 byte

0.00

200, 7 byte

0.27

/s503

503, 4 byte

0.01

503, 4 byte

0.19

/fixed

ReadTimeout

1.51

ReadTimeout

1.71

/rand

200, 1 byte

0.41

200, 1 byte

0.98

/dribble

ConnectionError

2.34

ReadTimeout

2.53

/empty

ConnectionError

0.02

RemoteProtocolError

0.21

/garbage

ConnectionError

0.01

RemoteProtocolError

0.20

/malformed

ChunkedEncodingError

0.02

RemoteProtocolError

0.20

/reset

ConnectionError

0.02

ReadError

0.22

/badjson

200, 16 byte

0.00

200, 16 byte

0.22

/wrongtype

200, 26 byte

0.01

200, 26 byte

0.20

không stub

404, 1023 byte

0.02

404, 1023 byte

0.20

Cùng lỗi, khác exception

Hai thư viện đặt tên lỗi khác nhau. Với requests, /empty, /garbage, /reset đều là ConnectionError với ProtocolError bên trong, còn /malformed là ChunkedEncodingError. Với httpx, ba ca là RemoteProtocolError, /reset là ReadError. Code chuyển thư viện mà bắt exception theo tên cũ sẽ để lọt lỗi.

Dribble dưới requests là ConnectionError

Dòng /dribble cần chú ý: timeout xảy ra lúc đọc thân nên requests báo ConnectionError bọc ReadTimeoutError, không phải requests.ReadTimeout như /fixed. Code chỉ bắt requests.Timeout sẽ không bắt được ca này. Lab thấy rõ trong traceback của urllib3. Khi đọc stream, phải bắt cả ConnectionError và ChunkedEncodingError.

RETRYABLE = (requests.ConnectionError, requests.Timeout,
             requests.exceptions.ChunkedEncodingError)
 
def safe_get(url):
    try:
        r = requests.get(url, timeout=(1, 1.5))
        return r, None
    except RETRYABLE as e:
        return None, type(e).__name__

Tuple này gom các lỗi lab thấy ở cột requests. Danh sách là theo ma trận, không phải đầy đủ mọi lỗi của thư viện.

Khác biệt theo hệ điều hành

Tài liệu cảnh báo CONNECTION_RESET_BY_PEER chỉ hoạt động tốt trên hệ Unix, và trên Windows nhiều khả năng làm kết nối treo. Lab chạy trên Windows 10, Java 8, WireMock 2.35.2 và không thấy treo: requests lỗi sau 0.02 giây, httpx sau 0.22 giây. Kết quả có thể khác theo bản JVM. Ma trận nên chạy cả trên hệ điều hành dùng cho CI, không suy từ tài liệu. Thông báo lỗi cụ thể cũng có thể khác giữa Windows và Linux.

Lỗi thân response giữa chừng

MALFORMED_RESPONSE_CHUNK gửi header 200 rồi hỏng. Với stream, anh em có thể đã ghi một phần vào tệp trước khi lỗi xảy ra. Vì vậy downloader nên ghi vào tệp tạm, chỉ đổi tên khi tải xong. Lab không kiểm phần ghi tệp, nhưng đây là hệ quả trực tiếp của ca này.

Origin giả riêng cho lỗi lạ hơn

Fault của WireMock có danh sách cố định. Với lỗi lạ hơn như header sai cú pháp hay Content-Length nói dối, anh em cần máy chủ socket tự viết. Lab đã làm cách đó ở bài RFC 9112 trong cụm. Dùng WireMock cho phần phổ biến, socket tự viết cho phần đặc thù.

Thời gian httpx trong ma trận

Các số httpx trong bảng cao hơn khoảng 0.2 giây do httpx.get tạo client mới mỗi lần. Dùng một httpx.Client chung sẽ khác. Lab giữ con số thật và ghi lý do, để không ai kết luận httpx chậm hơn từ bảng này.

Đối soát timeout, retry của downloader và ghi khác biệt theo hệ điều hành với WireMock mô phỏng origin lỗi

Phần này đo retry từ phía máy chủ, vì đó là con số đáng tin.

Cấu hình retry trong lab

Lab dùng HTTPAdapter với Retry của urllib3: status_forcelist 502, 503, 504, allowed_methods GET, backoff_factor 0.2, respect_retry_after_header True, raise_on_status False.

from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
 
def sess(total):
    s = requests.Session()
    s.mount("http://", HTTPAdapter(max_retries=Retry(
        total=total, backoff_factor=0.2, status_forcelist=[502, 503, 504],
        allowed_methods=["GET"], respect_retry_after_header=True,
        raise_on_status=False)))
    return s

Chỉ cho phép GET là chủ ý. Thử lại POST có thể tạo đơn trùng nếu lần đầu thực ra đã thành công.

Scenario lỗi rồi hồi phục

Stub /flap dùng scenario có ba trạng thái: lần đầu 503, lần hai reset kết nối, lần ba 200 với thân hoi phuc. Scenario mô phỏng origin hồi phục dần, điều không làm được với stub tĩnh. Trước mỗi phép thử, lab gọi /__admin/scenarios/reset.

flap = [
    {"scenarioName": "flap", "requiredScenarioState": "Started", "newScenarioState": "s2",
     "request": {"urlPath": "/flap"}, "response": {"status": 503}},
    {"scenarioName": "flap", "requiredScenarioState": "s2", "newScenarioState": "s3",
     "request": {"urlPath": "/flap"}, "response": {"fault": "CONNECTION_RESET_BY_PEER"}},
    {"scenarioName": "flap", "requiredScenarioState": "s3",
     "request": {"urlPath": "/flap"}, "response": {"status": 200, "body": "hoi phuc"}},
]

Trạng thái khởi đầu của mọi scenario có tên Started. Thứ tự được quyết định bằng newScenarioState, không phải thứ tự khai báo.

Kết quả retry

Bảng: Kết quả retry

Phép thử

Kết quả client

Số lần máy chủ nhận

Thời gian

/flap, total 3

200, hoi phuc

3

0.43 s

/flap, total 1

ConnectionError

2

không đo

/s503, total 2, Retry-After 1

503 trả về

3

2.03 s

/fixed, total 2, read 1 s

ConnectionError

3

không đo

Đọc bảng

Với total 3, client vượt qua cả 503 lẫn reset và thành công ở lần ba. Với total 1, client dừng ở lần hai, máy chủ nhận đúng 2 lượt. Với /s503, thời gian 2.03 giây cho thấy Retry-After 1 được tôn trọng ở hai lần chờ. Hết lượt thử, raise_on_status False làm client trả response 503, nên code vẫn phải kiểm mã.

Retry nhân tải lên origin

Dòng /fixed đáng chú ý: mỗi request timeout phía client vẫn tới được máy chủ, nên origin nhận 3 request cho một lần gọi. Origin đang chậm lại bị nhân ba tải. Trong đợt sự cố, nhiều client cùng làm vậy thì origin càng chết. Vì vậy cần backoff và giới hạn tổng, tức retry có trách nhiệm.

Thời gian tổng của một lời gọi

Với read timeout 1 giây và total 2, một lời gọi có thể chiếm hơn 3 giây cộng backoff. Công thức thô là số lượt nhân timeout cộng thời gian chờ. Hàng đợi phải tính theo con số này, không phải theo timeout một lượt. Nhiều nhóm đặt timeout 30 giây và quên rằng retry biến nó thành nhiều phút.

Lỗi nào nên retry

Theo ma trận của lab, 503, reset, timeout đáng thử lại. Thân JSON hỏng thì tùy, vì có thể là lỗi tạm hoặc lỗi nội dung. 404 và 403 không nên thử lại nhiều. Trang lỗi Access denied trả 200 không nên retry ngay, vì thử lại dồn dập làm tình hình xấu đi. Bài Prefect retry task tải web nói về retry ở tầng workflow.

Proxy nằm ở đâu trong ma trận

Khi crawler đi qua proxy, lỗi có thể do proxy, không phải origin, và mã có thể là 502 hay 504 do proxy sinh. Anh em có thể đặt WireMock làm origin rồi cho downloader đi qua proxy thật để so bảng. Lab không dùng proxy, nên bảng trên chỉ là đường đi thẳng. Chuỗi cấu hình theo định dạng proxy, và vai trò proxy có ở bài Web Scraping proxy là gì.

Khi test đi qua proxy thật

Nếu cần so với proxy thật, anh em dựng WireMock trên máy có địa chỉ công khai riêng, chỉ dùng cho test. Không trỏ phép thử lỗi vào website của người khác.

Nếu downloader cần IP ổn định, anh em có thể mua proxy tốc độ cao tại trang chủ ProxyVN, rồi chạy ma trận trên đường đi đó.

Đưa ma trận vào pytest

Ma trận nên chạy tự động, không phải script chạy tay. Pytest có parametrize để biến mỗi dòng bảng thành một test. Mỗi test khẳng định loại exception và khoảng thời gian rộng, ví dụ dưới hai giây. Không khẳng định thời gian chính xác, vì máy CI có thể chậm hơn. Test đỏ nghĩa là hành vi downloader đã đổi, không phải máy chậm.

import pytest
 
CASES = [("/fixed", requests.Timeout), ("/empty", requests.ConnectionError),
         ("/malformed", requests.exceptions.ChunkedEncodingError),
         ("/reset", requests.ConnectionError)]
 
@pytest.mark.parametrize("path,exc", CASES)
def test_fault(path, exc):
    with pytest.raises(exc):
        requests.get(A + path, timeout=(1, 1.5))

Bốn ca này khớp cột requests trong ma trận của lab. Lab chạy đoạn này và cả bốn test đạt trong 1.70 giây. Trên môi trường khác, tên exception có thể đổi, nên anh em chạy lại trước khi dựa vào nó. Bản ghi của từng lần chạy cũng nên lưu cùng phiên bản thư viện đã dùng trong lần đó.

Dọn dẹp sau test

Fixture nên khởi động WireMock một lần cho cả phiên test, reset mappings và requests giữa các test, rồi tắt tiến trình ở cuối. Quên tắt thì lần chạy sau gặp lỗi cổng đã dùng. Với song song, mỗi worker dùng một cổng riêng. Đây là chi tiết nhỏ nhưng hay làm CI thất bại ngẫu nhiên.

Log phía máy chủ là tài liệu

Tệp wm.log ghi các request không khớp cùng so sánh với stub gần nhất. Khi test đỏ trên CI, đính kèm tệp này vào kết quả. Thông tin trong đó đủ để biết client đã gửi gì. Đây là thứ rất khó có khi test trên website thật.

Khóa phiên bản

Lab chạy Windows 10, Java 1.8.0_401, wiremock-jre8-standalone 2.35.2, Python 3.12.10, requests 2.34.2, httpx 0.28.1. Tên exception và thời gian có thể đổi theo các bản này. Ma trận phải ghi rõ môi trường, vì nó là phép đo, không phải định lý.

Đầu ra nghiệm thu stub lỗi có kiểm soát cùng ma trận kết quả downloader timeout, retry của WireMock mô phỏng origin lỗi

Mục này chốt tài sản bàn giao và danh sách kiểm.

Tài sản bàn giao

Retry đo từ máy chủ

Retry đo từ máy chủ

Bộ bàn giao gồm tệp stubs.json 14 stub, script chạy ma trận, bảng kết quả theo môi trường. Khi đổi thư viện HTTP hay nâng phiên bản, chạy lại và so với bảng cũ. Khác biệt nào cũng phải được giải thích trước khi merge.

Danh sách kiểm

  • Timeout: có connect, read và deadline tổng riêng.
  • Nén: test dribble với Accept-Encoding identity.
  • Exception: bắt theo ma trận, gồm ConnectionError khi đọc thân.
  • Nội dung: kiểm mã, Content-Type và thân, không chỉ exception.
  • Retry: đo số lần từ máy chủ, chỉ cho GET, có backoff.
  • Môi trường: ma trận chạy trên hệ điều hành của CI.

Câu kết luận được phép viết

Từ lab có thể viết rằng gzip làm dribble về một khối với requests và httpx. Read timeout không chặn được origin nhỏ giọt. Mỗi lần retry timeout vẫn tới origin. CONNECTION_RESET_BY_PEER không treo trong môi trường lab. Không nên viết rằng điều đó đúng mọi Windows.

Những gì lab chưa chạy

Lab chưa chạy Linux, chưa chạy bản WireMock 3, chưa thử HTTPS, chưa đi qua proxy, chưa thử client bất đồng bộ, chưa kiểm ghi tệp tạm. Lab chưa thử lognormal. Hàm classify và safe_get viết theo ma trận, cần test riêng trong dự án thật.

Lỗi nguy hiểm nhất của downloader thường không ném exception. Read timeout không phải deadline tổng, và mỗi lần retry vẫn là thêm một request tới origin.

WireMock mô phỏng origin lỗi: câu hỏi thường gặp

Các câu dưới đây gom những điểm hay bị hỏi.

WireMock có cần Java mới không?

Bản mới cần Java mới hơn. Máy chỉ có Java 8 thì dùng bản jre8 2.35.2 như lab.

Vì sao dribble của tôi không nhỏ giọt?

Client gửi Accept-Encoding gzip nên response bị nén. Đặt identity hoặc tắt gzip ở WireMock.

Read timeout có giới hạn tổng thời gian không?

Không. Nó tính khoảng chờ giữa các lần nhận. Deadline tổng phải tự làm.

Vì sao requests báo ConnectionError thay vì Timeout?

Khi timeout xảy ra lúc đọc thân, requests bọc thành ConnectionError trong lab.

Reset trên Windows có treo không?

Tài liệu cảnh báo có thể treo. Lab không thấy treo trên Windows 10, Java 8.

Nên retry POST không?

Mặc định không, trừ khi thao tác có tính idempotent rõ ràng.

Khác gì VCR.py?

VCR.py phát lại phản hồi thật đã ghi. WireMock tạo lỗi có kiểm soát.

Có nên đưa WireMock vào CI không?

Có, với bộ stub ổn định. Dùng fixed delay để test lặp lại được.

WireMock mô phỏng origin lỗi và những điều cần nhớ

WireMock mô phỏng origin lỗi giúp downloader được kiểm trước sự cố khi anh em dựng đủ delay, dribble, fault, tắt nén lúc đo, đếm retry từ phía máy chủ và lưu ma trận theo môi trường. Proxy.vn - Nhà cung cấp dịch vụ proxy chất lượng hàng đầu Việt Nam khuyên anh em đặt deadline tổng khi dùng WireMock mô phỏng origin lỗi.

TIN TỨC LIÊN QUAN