Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Debug có giả thuyết: giữ phép thử đủ sức bác bỏ mình

Câu hỏi: bằng chứng nào khiến ta bỏ một giả thuyết trước khi sửa code?

Cần biết trước: transaction và lab deadlock. Ví dụ tái dựng lỗi của lab đó, không báo một sự cố production. Môi trường đã chọn: MySQL 26.7.0/InnoDB, Python 3.14.4, macOS arm64, socket và dữ liệu giả riêng. Phần SQL/controller thuộc bài deadlock; bài này sở hữu nhật ký điều tra và phép thử regression. Tiêu chí hoàn tất giữ quy tắc nghiệm thu; chọn context giữ quy tắc input.

Chốt lỗi trước khi đặt giả thuyết

Hai yêu cầu A/B đều cần tăng balance hai hàng từ 100 lên 102 sau hai commit, mỗi request đúng một lần. Trong lịch lỗi, A giữ hàng 1 rồi đòi hàng 2; B làm ngược lại. Actual: một request gặp 1213/40001, transaction đó rollback; winner commit cho balance 101/request 1. Retry toàn transaction bằng cùng mã mới đưa đến 102/request 2.

Regression muốn bảo vệ lịch hai hàng của fixture sau khi đổi mọi đường ghi sang cùng thứ tự: B có thể chờ, nhưng cả hai commit mà không có 1213 trong lịch này. Đây không phải hợp đồng “hệ thống không bao giờ deadlock”. FK, unique constraint, transaction khác hoặc đường code cũ vẫn có thể tạo vòng khác.

Thử tái hiện hai lần từ reset, dùng marker để biết đã giữ khóa và performance_schema.data_lock_waits để thấy cạnh A→B trước khi B đóng vòng. Không dùng sleep tùy ý hoặc gõ nhanh hai terminal làm bằng chứng đồng thời. Deadlock handling.

Dự đoán trước khi xem kết quả

Giả thuyếtDự đoán phân biệt đượcPhép thử giữ nguyên phần còn lạiĐiều bác bỏ
H1: thứ tự khóa ngược tạo vòngCùng thứ tự 1→2 loại vòng hai hàngGiữ bảng/dữ liệu/hai session; chỉ đổi lịch lấy khóaCùng thứ tự vẫn có cùng vòng trên hai hàng
H2: thiếu index là nguyên nhân cần thiếtDeadlock này đòi truy cập không có PRIMARY indexKiểm metadata index; giữ lịch lỗi, không thêm indexPRIMARY đã có nhưng 1213 vẫn xuất hiện
H3: lock timeout quá ngắn bị nhầm thành deadlockTimeout dài sẽ làm lỗi biến mất hoặc mã lỗi khác 1213Đặt SESSION timeout 60s cho cả hai; giữ lịch ngượcDetector vẫn sinh 1213/40001, không phải 1205

H2 bị bác bỏ chỉ cho claim “thiếu index là điều kiện cần của lỗi này”; không suy index không ảnh hưởng phạm vi khóa ở query khác. Không tắt detector hoặc tăng global timeout trên máy thật để thử. Một biến thay đổi mỗi thí nghiệm.

Dựng phép thử từ file owner

Trong thư mục trống, chép bốn file cách A của lab database: lab-common.sh, lab-local.sh, seed-pg.sql, seed-mysql.sql; chép setup.sql và deadlock.py từ bài deadlock. Không sửa các file đó. Wrapper dưới dùng runpy để chạy bộ kiểm gốc trước mỗi phép thử; controller không phải thư viện ứng dụng. Fixture gốc còn chạy PostgreSQL phục vụ lab chung, dù phép debug này chỉ dùng MySQL.

set -euo pipefail
. ./lab-local.sh
lab_up
lab_seed
lab_whoami
lab_mysql < setup.sql
from __future__ import annotations

import contextlib
import io
import runpy
import sys
from typing import Any

assert sys.version_info[:3] == (3, 14, 4)
baseline = io.StringIO()
with contextlib.redirect_stdout(baseline):
    owner = runpy.run_path("deadlock.py")
query = owner["query"]
balances = owner["balances"]
assert query("SELECT @@version") == "26.7.0"
assert "timeout ERROR 1205: waiter còn thấy 107" in baseline.getvalue()
assert (
    "retry toàn transaction và gửi trùng: balance=102, requests=2"
    in baseline.getvalue()
)
assert "cùng thứ tự 1 rồi 2: có chờ, hai commit, không deadlock" in baseline.getvalue()


def capture(function: Any, *args: Any) -> str:
    output = io.StringIO()
    with contextlib.redirect_stdout(output):
        function(*args)
    return output.getvalue()


mode = sys.argv[1]
assert mode in ("hypotheses", "broken", "fixed", "full")
if mode == "hypotheses":
    primary = query(
        "SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema='wiki_lab' AND table_name='accounts' AND index_name='PRIMARY' AND column_name='id'"
    )
    assert primary == "1"
    assert "ERROR 1213 (40001)" in capture(owner["cycle"], 3)
    print("H2 primary_index=present reverse_order=1213 hypothesis=rejected")
    scope = owner["cycle"].__globals__
    original = scope["Session"]

    def long_timeout(inspect_error: bool = False) -> Any:
        session = original(inspect_error=inspect_error)
        session.mark("SET SESSION innodb_lock_wait_timeout=60;", "timeout_set")
        session.send("SELECT @@innodb_lock_wait_timeout;")
        assert session.line() == "60"
        return session

    scope["Session"] = long_timeout
    try:
        assert "ERROR 1213 (40001)" in capture(owner["cycle"], 4)
    finally:
        scope["Session"] = original
    print("H3 session_timeout=60 reverse_order=1213 hypothesis=rejected")
    ordered = capture(owner["ordered"])
    assert "có chờ, hai commit, không deadlock" in ordered
    print("H1 ordered=wait_then_commit balance=102 requests=2 hypothesis=supported")
elif mode in ("broken", "fixed"):
    output = (
        capture(owner["cycle"], 5) if mode == "broken" else capture(owner["ordered"])
    )
    assert "ERROR 1213" not in output, "regression: reverse order produced 1213"
    assert (
        query("SELECT GROUP_CONCAT(balance ORDER BY id) FROM wiki_lab.accounts")
        == "102,102"
    )
    assert query("SELECT COUNT(*) FROM wiki_lab.requests") == "2"
    print("regression=green two_commits=2 balances=102,102")
else:
    print(
        "adjacent owner_suite=passed wait=normal timeout=1205 rollback=explicit replay=deduplicated"
    )
owner["reset"]()
balances(100, 0)

Chạy giả thuyết trước. Wrapper tăng timeout bằng Session factory chỉ trong lab, đọc lại giá trị 60 ở từng connection rồi khôi phục factory trong finally. Không sửa server global hoặc tạo một bản controller có lịch khác với owner.

set -euo pipefail
python3 debug.py hypotheses
H2 primary_index=present reverse_order=1213 hypothesis=rejected
H3 session_timeout=60 reverse_order=1213 hypothesis=rejected
H1 ordered=wait_then_commit balance=102 requests=2 hypothesis=supported

Giữ assertion không có 1213 và chạy lịch ngược: test phải đỏ vì đúng triệu chứng, không phải vì lỗi kết nối, thiếu file hoặc cú pháp. Sau đó chọn lịch cùng thứ tự, giữ assertion và input như cũ. Đây là sửa lịch thao tác của fixture, không sửa mã ứng dụng đang vận hành. Khi áp dụng thật, sửa mọi caller có thứ tự trái nhau. Controller lịch lỗi dùng client --force để đọc trạng thái sau lỗi, lịch sửa thì không cần tiếp tục sau lỗi. Tùy chọn này không đổi thứ tự SQL lấy khóa; hai controller có khác phần quan sát/retry, nên bài không đo chi phí runtime giữa chúng.

set -euo pipefail
python3 debug.py broken
set -euo pipefail
python3 debug.py fixed
python3 debug.py full
regression=green two_commits=2 balances=102,102
adjacent owner_suite=passed wait=normal timeout=1205 rollback=explicit replay=deduplicated
set -euo pipefail
. ./lab-local.sh
lab_clean
test ! -e lab.env

Nhật ký đủ để người khác tiếp tục

MụcRecord của ví dụ
Context tối thiểuVersion, socket lab, hai bảng/PK, lịch A1/B1/A2/B2, expected/actual
EvidenceCạnh chờ trước B2, mã 1213, báo cáo InnoDB; victim không còn thay đổi đầu transaction
Đã bác bỏThiếu PRIMARY; timeout ngắn là nguyên nhân 1213 trong lịch này
Đã xác nhậnHai đường khóa ngược; cùng thứ tự cho chờ bình thường và hai commit ở fixture
Fix cụ thểThống nhất 1→2, giữ transaction ngắn; retry toàn transaction vẫn cần cho lỗi khác
RegressionLịch cũ đỏ đúng 1213, lịch mới xanh với 102/102 và 2 request; 1205/replay vẫn đúng
Chưa chứng minhKhông có mọi loại deadlock; production latency; nhiều unique/FK/API ngoài DB

Không chép SQL/định danh khách hàng thật vào journal. Báo cáo deadlock có thể chứa query, account hoặc địa chỉ vận hành; khử định danh trước khi chia sẻ. Không lấy “test xanh” thay cho expected/actual và phần chưa kiểm. InnoDB rollback semantics.

Học tiếp: batch retry, checkpoint để tiếp tục sau mất context.