Mã thoát của job: để bộ lập lịch biết khi nào dừng và khi nào thử lại
Câu hỏi bài này trả lời: một job chạy dưới Kubernetes hay AWS Batch chỉ báo kết quả cho bộ lập lịch bằng một con số; những cách viết script nào khiến con số đó thành 0 dù việc đã hỏng; shell tự đặt những mã nào; và một hợp đồng mã thoát tối thiểu trông ra sao để lỗi tạm thời được thử lại có giới hạn còn lỗi xác thực thì dừng ngay?
Cần biết trước: shell cơ bản ($?, pipeline, ||), mã trạng thái HTTP, đọc Python ngắn. Lab dùng bash 3.2.57 (bản đi kèm macOS), curl 8.7.1 và Python 3.14.4 (thư viện chuẩn) trên macOS arm64, với một API thử chạy cục bộ ở cổng do hệ điều hành cấp; không cần mạng ngoài. Lab chưa chạy trên Linux, trên bash hay curl bản khác, trên Kubernetes hay AWS Batch: phần nền tảng chỉ trích tài liệu đọc ngày 2026-10-04 và ghi rõ là chưa chạy.
Bộ lập lịch chỉ đọc một con số
Khi tiến trình của job kết thúc, thứ bộ lập lịch nhận được là mã thoát. Ví dụ podFailurePolicy trong tài liệu Job của Kubernetes viết: “an exit code of 0 means that the container succeeded”, còn “any other exit code represents that the container failed, and hence the entire Pod. The Pod will be re-created if the total number of restarts is below backoffLimit”. AWS Batch liệt kê trong các tình huống thất bại có thể thử lại: “Any non-zero exit code from a container job”.
Có hai hướng sai, và cả hai đều tốn kém:
- Báo 0 khi việc đã hỏng. Không tầng nào phía sau thấy lỗi: không thử lại, không cảnh báo, và job kế tiếp đọc dữ liệu hỏng như dữ liệu tốt.
- Báo cùng một mã khác 0 cho mọi lỗi. Bộ lập lịch không phân biệt được lỗi nên thử lại (mạng chập chờn) với lỗi thử lại vô ích (thiếu token), nên chỉ còn hai lựa chọn: thử lại tất cả hoặc không thử lại gì.
Bài đi từ hướng sai thứ nhất sang cách giải hướng thứ hai bằng một hợp đồng mã thoát.
Ba cách làm mã thoát thành 0 dù việc đã hỏng
Mỗi cách có một bản sai và một bản sửa trong lab, số liệu ở phần kết quả bên dưới:
| Cách viết | Vì sao thoát 0 | Dấu vết sai còn lại | Bản sửa |
|---|---|---|---|
lệnh || true | lệnh lỗi nằm trong danh sách ||; trạng thái cả danh sách là true | dòng “xong” vẫn in, bước sau chạy tiếp | bỏ || true; lab thoát 1 (mã của cp) |
pipeline không pipefail | trạng thái pipeline là của lệnh cuối | dữ liệu đầu vào cụt, bước sau nhận thiếu | set -o pipefail; lab thoát 3 (mã lệnh đầu) |
curl không --fail | HTTP 401 vẫn là một phản hồi nhận được nên curl thoát 0 | tệp đích chứa JSON báo lỗi như thể là dữ liệu | --fail: thoát 22 và không tạo tệp |
Căn cứ trong tài liệu: “The exit status of a pipeline is the exit status of the last command in the pipeline, unless the pipefail option is enabled”; với pipefail, trạng thái là mã của “the last (rightmost) command to exit with a non-zero status”. Về set -e, tài liệu Bash nói shell không thoát khi lệnh lỗi là một phần của any command executed in a && or || list except the command following the final && or || hoặc của any command in a pipeline but the last (subject to the state of the pipefail shell option). Với curl, mã 22 là “HTTP page not retrieved” và “This return code only appears if –fail is used”.
--fail chưa đủ để phân loại
Hai ca sau cần cách xử lý khác hẳn nhau: HTTP 401 là sai cấu hình, thử lại vô ích; HTTP 503 là sự cố tạm thời, thử lại có thể qua. Nhưng --fail cho cả hai cùng mã 22 (dòng curl --fail: HTTP 401 thoát 22, HTTP 503 thoát 22 trong kết quả), nên bộ lập lịch không có gì để phân biệt. Tài liệu curl còn ghi --fail “is not fail-safe and there are occasions where non-successful response codes slip through, especially when authentication is involved (response codes 401 and 407)”. Vì hai lý do đó, fetch.sh trong lab không dựa vào --fail: nó đọc %{http_code} (theo tài liệu curl là “The numerical response code that was found in the last retrieved HTTP(S) or FTP(s) transfer”) rồi tự đổi sang mã thoát của hợp đồng. Lab không tái hiện được trường hợp “slip through” vì nó cần cơ chế xác thực nhiều bước; câu đó chỉ là trích tài liệu.
Bỏ qua lỗi: đúng một mã đã biết
Có lúc một lệnh thất bại là chấp nhận được, ví dụ một bước trả mã 4 để nói “không có gì để làm”. Cách an toàn là bỏ qua đúng mã đó và để mọi mã khác đi qua: step "$1" || { rc=$?; [ "$rc" -eq 4 ] || exit "$rc"; }. Trong lab, mã 4 cho chạy tiếp (thoát 0) còn mã 9 thoát 9 chứ không bị nuốt. || true là bản không phân biệt mã nào.
Mã do shell đặt
Một số con số đã có nghĩa cố định trước khi job của bạn viết dòng nào:
| Mã | Khi nào | Ca trong lab | Căn cứ |
|---|---|---|---|
| 126 | tìm thấy lệnh nhưng không thực thi được | ./noexec.sh thiếu quyền thực thi | Bash: “If a command is found but is not executable, the return status is 126”; POSIX: “the exit status shall be 126” |
| 127 | không tìm thấy lệnh | khong-co-lenh | Bash: “a status of 127”; POSIX: “the exit status shall be 127” |
| 128+N | tiến trình chết vì tín hiệu số N | SIGTERM (N là 15) cho 143 | Bash: “Bash uses the value 128+N as the exit status”; POSIX: “an exit status greater than 128” và “in an implementation-defined manner, which signal” |
Mã của ứng dụng nên tránh 126, 127 và từ 128 trở lên, để mỗi con số trong log chỉ có một nghĩa. Mục POSIX nói trên là “2.8.2 Exit Status for Commands” trong bản POSIX.1-2024.
Hợp đồng mã thoát của bài
| Mã | Nghĩa | Bộ lập lịch nên | Ca trong lab |
|---|---|---|---|
| 0 | xong, tệp đích đã ghi | đi tiếp | HTTP 200 |
| 10 | lỗi tạm thời | thử lại có giới hạn | HTTP 408, 429, 500, 503; quá thời gian (curl 28); cổng đóng (curl 7) |
| 11 | xác thực hoặc ủy quyền | dừng và báo người sửa cấu hình | HTTP 401, 403 |
| 1 | lỗi khác | dừng và để người đọc log | HTTP 404; HTTP 302 vì fetch.sh không tự theo chuyển hướng |
Mã 10 và 11 là quy ước của bài, không phải chuẩn: chúng nhỏ hơn 126 và khác 1, mã chung chung nhất. Việc xếp HTTP 408, 429 và 5xx vào “tạm thời” cũng là lựa chọn của bài; 500 có thể là lỗi lặp lại mãi, nên thử lại luôn phải có giới hạn. Theo tài liệu curl, mã 7 là “Failed to connect to host” và mã 28 là “Operation timeout”; --max-time đặt thời gian tối đa cho mỗi lần truyền và “Prevents your batch jobs from hanging for hours due to slow networks or links going down”, nên fetch.sh luôn đặt nó.
Lab: tải có phân loại và thử lại có giới hạn
Tạo thư mục trống rồi lưu bốn tệp:
api.pylà API thử bằng thư viện chuẩn:/seedđòi headerAuthorizationđúng,/flakytrả 503 hai lần rồi 200,/slowchậm 3 giây,/status/NNNtrả đúng mã NNN.fetch.shtải một URL và đổi kết quả thành mã thoát của hợp đồng. Nó không dùngset -evì cần đọc$?của curl rồi tự quyết; đọc%{http_code}thay vì dựa vào--fail; ghi vào tệp tạm rồimvđể chỉ lần thành công mới tạo hoặc thay tệp đích; cótrapdọn tệp tạm.jobwrap.pychạy một lệnh tối đa ba lần, chỉ thử lại khi mã nằm trongRETRYABLE(chỉ có 10), nghỉ tăng dần giữa các lần, và trả mã của lần cuối. Python báo cái chết vì tín hiệu bằng mã âm (“A negative value -N indicates that the child was terminated by signal N”), nên wrapper đổi sang 128+N như shell để một con số trong log có cùng nghĩa ở mọi chỗ.cases.pychạy mọi ca vàasserttừng kết quả: lệch một mã là script thoát khác 0.
import json
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
TOKEN = "demo-token"
hits = {"flaky": 0}
class Handler(BaseHTTPRequestHandler):
def log_message(self, *args):
pass # giữ stderr sạch cho phần đo
def reply(self, status, body):
payload = json.dumps(body).encode()
try:
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
except (BrokenPipeError, ConnectionResetError):
pass # client đã bỏ đi, như ca quá thời gian
def do_GET(self):
if self.path == "/seed":
if self.headers.get("Authorization") == f"Bearer {TOKEN}":
return self.reply(200, {"rows": 3})
return self.reply(401, {"error": "unauthorized"})
if self.path == "/flaky":
hits["flaky"] += 1
return self.reply(503 if hits["flaky"] <= 2 else 200, {"attempt": hits["flaky"]})
if self.path == "/slow":
time.sleep(3)
return self.reply(200, {"slow": True})
if self.path.startswith("/status/"):
return self.reply(int(self.path.rsplit("/", 1)[1]), {"path": self.path})
return self.reply(404, {"error": "not found"})
def start():
"""Mở server ở cổng do hệ điều hành cấp; trả về (server, địa chỉ gốc)."""
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
threading.Thread(target=server.serve_forever, daemon=True).start()
port = server.server_address[1]
return server, f"http://127.0.0.1:{port}"
#!/usr/bin/env bash
# Tải $1 về tệp $2 và đổi kết quả thành mã thoát theo hợp đồng của job:
# 0 thành công | 10 lỗi tạm thời, thử lại được | 11 xác thực hoặc ủy quyền, không thử lại | 1 lỗi khác
# Không dùng `set -e`: script tự đọc mã của curl rồi tự quyết mã thoát.
set -uo pipefail
url=$1
out=$2
tmp=$(mktemp "$out.XXXXXX") || exit 1
trap 'rm -f "$tmp"' EXIT
status=$(curl --silent --show-error --max-time "${MAX_TIME:-30}" --output "$tmp" \
--write-out '%{http_code}' --header "Authorization: Bearer ${TOKEN:-}" "$url")
rc=$?
if [ "$rc" -ne 0 ]; then
case $rc in
7 | 28) exit 10 ;; # không kết nối được hoặc quá thời gian: thường là tạm thời
*) exit 1 ;;
esac
fi
case $status in
2??) mv "$tmp" "$out" ;; # chỉ thành công mới tạo hoặc thay tệp đích
401 | 403) exit 11 ;;
408 | 429 | 5??) exit 10 ;;
*) exit 1 ;;
esac
import argparse
import subprocess
import sys
import time
RETRYABLE = {10} # chỉ mã "lỗi tạm thời" của hợp đồng mới được thử lại
def run(command, attempts, backoff):
code = 1
for attempt in range(1, attempts + 1):
code = subprocess.run(command).returncode
if code < 0:
code = 128 - code # bị tín hiệu N giết: báo 128+N như shell
print(f"lần {attempt}: mã {code}", file=sys.stderr, flush=True)
if code not in RETRYABLE or attempt == attempts:
return code
time.sleep(backoff * attempt)
return code
parser = argparse.ArgumentParser()
parser.add_argument("--attempts", type=int, default=3)
parser.add_argument("--backoff", type=float, default=0.01)
parser.add_argument("command", nargs=argparse.REMAINDER)
args = parser.parse_args()
command = args.command[1:] if args.command[:1] == ["--"] else args.command
sys.exit(run(command, args.attempts, args.backoff))
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
import api
LAB = Path.cwd()
server, base = api.start()
work = Path(tempfile.mkdtemp(prefix="joblab-"))
def run(command, token, extra):
env = {**os.environ, "API": base, **extra}
env.pop("TOKEN", None)
if token:
env["TOKEN"] = api.TOKEN
return subprocess.run(command, cwd=work, env=env, capture_output=True, text=True, timeout=60)
def sh(script, *args, token=False, **extra):
return run(["bash", "-c", script, "_", *args], token, extra)
def fetch_once(url, out="probe.json", token=False, **extra):
return run(["bash", str(LAB / "fetch.sh"), url, out], token, extra).returncode
def wrapped(*command, token=False):
"""Chạy lệnh qua jobwrap.py; trả về (mã thoát, mã của từng lần thử)."""
wrapper = [sys.executable, "-B", str(LAB / "jobwrap.py"), "--attempts", "3", "--backoff", "0.01", "--"]
done = run([*wrapper, *command], token, {})
attempts = [int(line.rsplit(" ", 1)[1]) for line in done.stderr.splitlines() if line.startswith("lần ")]
return done.returncode, attempts
def fetch(url, out="seed.json", token=False):
return wrapped("bash", str(LAB / "fetch.sh"), url, out, token=token)
def seed_text():
path = work / "seed.json"
return path.read_text() if path.exists() else "(không có tệp)"
try:
curl_version = subprocess.run(["curl", "--version"], capture_output=True, text=True).stdout.split()[1]
bash_version = sh("echo $BASH_VERSION").stdout.strip()
print(f"công cụ: curl {curl_version}, bash {bash_version}, python {sys.version.split()[0]}")
print("== Ba cách nuốt lỗi và bản sửa")
bad = sh("cp missing.txt out.txt || true; echo xong")
good = sh("set -e; cp missing.txt out.txt; echo xong")
print(
f"`|| true` sau lệnh lỗi: bản sai thoát {bad.returncode} và vẫn in {bad.stdout.strip()!r}; "
f"bản sửa thoát {good.returncode}"
)
assert (bad.returncode, good.returncode) == (0, 1) and "xong" not in good.stdout
producer = "producer() { echo dong1; return 3; }; "
bad = sh(producer + "producer | wc -l > /dev/null")
good = sh("set -o pipefail; " + producer + "producer | wc -l > /dev/null")
print(f"pipeline không pipefail: bản sai thoát {bad.returncode}; bản sửa thoát {good.returncode}")
assert (bad.returncode, good.returncode) == (0, 3)
bad = sh('curl -s "$API/seed" -o seed.json')
body = seed_text()
(work / "seed.json").unlink()
good = sh('curl -s --fail "$API/seed" -o seed.json')
print(
f"curl không --fail gặp HTTP 401: bản sai thoát {bad.returncode} và để lại {body}; "
f"bản sửa thoát {good.returncode}, tệp: {seed_text()}"
)
assert bad.returncode == 0 and "unauthorized" in body
assert good.returncode == 22 and not (work / "seed.json").exists()
codes = [sh(f'curl -s --fail "$API/status/{n}" -o /dev/null').returncode for n in (401, 503)]
print(f"curl --fail: HTTP 401 thoát {codes[0]}, HTTP 503 thoát {codes[1]}: cùng một mã")
assert codes == [22, 22]
rule = 'step() { return "$1"; }; step "$1" || { rc=$?; [ "$rc" -eq 4 ] || exit "$rc"; }; echo tiếp'
known, unknown = sh(rule, "4"), sh(rule, "9")
print(f"bỏ qua đúng một mã đã biết (4): mã 4 thoát {known.returncode}, mã 9 thoát {unknown.returncode}")
assert (known.returncode, unknown.returncode) == (0, 9)
print("== Mã do shell đặt")
(work / "noexec.sh").write_text("echo hi\n")
for label, script, expected in (
("thành công", "true", 0),
("lệnh không tồn tại", "khong-co-lenh", 127),
("tệp không có quyền thực thi", "./noexec.sh", 126),
("tiến trình con bị SIGTERM", "sleep 30 & kill -TERM $!; wait $!", 143),
):
code = sh(script).returncode
print(f"{label} -> {code}")
assert code == expected
print("== Hợp đồng: từ kết quả HTTP hoặc mạng sang mã thoát")
for label, path, extra, expected in (
("HTTP 200", "/status/200", {}, 0),
("HTTP 302 (không theo chuyển hướng)", "/status/302", {}, 1),
("HTTP 401", "/status/401", {}, 11),
("HTTP 403", "/status/403", {}, 11),
("HTTP 404", "/status/404", {}, 1),
("HTTP 408", "/status/408", {}, 10),
("HTTP 429", "/status/429", {}, 10),
("HTTP 500", "/status/500", {}, 10),
("HTTP 503", "/status/503", {}, 10),
("quá thời gian (curl 28)", "/slow", {"MAX_TIME": "1"}, 10),
):
code = fetch_once(base + path, **extra)
print(f"{label} -> {code}")
assert code == expected
dead, dead_base = api.start()
dead.shutdown()
dead.server_close()
code = fetch_once(dead_base + "/seed")
print(f"cổng đóng (curl 7) -> {code}")
assert code == 10
leftovers = sorted(p.name for p in work.glob("probe.json.*"))
print(f"tệp tạm còn sót lại sau các lần gọi ở trên: {leftovers}")
assert leftovers == []
print("== Khởi tạo gọi API có xác thực")
naive = sh('curl -s "$API/seed" -o seed.json; echo "khởi tạo xong"')
print(f"init cũ, thiếu token: thoát {naive.returncode}, seed.json = {seed_text()}")
assert naive.returncode == 0 and "unauthorized" in seed_text()
(work / "seed.json").unlink()
code, attempts = fetch(base + "/seed")
print(f"fetch.sh thiếu token: các lần {attempts} -> thoát {code}, tệp: {seed_text()}")
assert (code, attempts) == (11, [11]) and not (work / "seed.json").exists()
code, attempts = fetch(base + "/seed", token=True)
print(f"fetch.sh có token: các lần {attempts} -> thoát {code}, seed.json = {seed_text()}")
assert (code, attempts) == (0, [0]) and seed_text() == '{"rows": 3}'
code, attempts = fetch(base + "/seed")
print(f"lần sau thiếu token: các lần {attempts} -> thoát {code}, seed.json vẫn là {seed_text()}")
assert (code, attempts) == (11, [11]) and seed_text() == '{"rows": 3}'
print("== Wrapper thử lại có giới hạn")
for label, path, expected in (
("/flaky trả 503 hai lần rồi 200", "/flaky", (0, [10, 10, 0])),
("503 mãi", "/status/503", (10, [10, 10, 10])),
("HTTP 404 (lỗi khác)", "/status/404", (1, [1])),
):
result = fetch(base + path, out="out.json", token=True)
print(f"{label}: các lần {result[1]} -> thoát {result[0]}")
assert result == expected
result = wrapped("bash", "-c", "kill -TERM $$")
print(f"job bị SIGTERM: các lần {result[1]} -> thoát {result[0]}")
assert result == (143, [143])
finally:
server.shutdown()
shutil.rmtree(work, ignore_errors=True)
python3 -B cases.py
công cụ: curl 8.7.1, bash 3.2.57(1)-release, python 3.14.4
== Ba cách nuốt lỗi và bản sửa
`|| true` sau lệnh lỗi: bản sai thoát 0 và vẫn in 'xong'; bản sửa thoát 1
pipeline không pipefail: bản sai thoát 0; bản sửa thoát 3
curl không --fail gặp HTTP 401: bản sai thoát 0 và để lại {"error": "unauthorized"}; bản sửa thoát 22, tệp: (không có tệp)
curl --fail: HTTP 401 thoát 22, HTTP 503 thoát 22: cùng một mã
bỏ qua đúng một mã đã biết (4): mã 4 thoát 0, mã 9 thoát 9
== Mã do shell đặt
thành công -> 0
lệnh không tồn tại -> 127
tệp không có quyền thực thi -> 126
tiến trình con bị SIGTERM -> 143
== Hợp đồng: từ kết quả HTTP hoặc mạng sang mã thoát
HTTP 200 -> 0
HTTP 302 (không theo chuyển hướng) -> 1
HTTP 401 -> 11
HTTP 403 -> 11
HTTP 404 -> 1
HTTP 408 -> 10
HTTP 429 -> 10
HTTP 500 -> 10
HTTP 503 -> 10
quá thời gian (curl 28) -> 10
cổng đóng (curl 7) -> 10
tệp tạm còn sót lại sau các lần gọi ở trên: []
== Khởi tạo gọi API có xác thực
init cũ, thiếu token: thoát 0, seed.json = {"error": "unauthorized"}
fetch.sh thiếu token: các lần [11] -> thoát 11, tệp: (không có tệp)
fetch.sh có token: các lần [0] -> thoát 0, seed.json = {"rows": 3}
lần sau thiếu token: các lần [11] -> thoát 11, seed.json vẫn là {"rows": 3}
== Wrapper thử lại có giới hạn
/flaky trả 503 hai lần rồi 200: các lần [10, 10, 0] -> thoát 0
503 mãi: các lần [10, 10, 10] -> thoát 10
HTTP 404 (lỗi khác): các lần [1] -> thoát 1
job bị SIGTERM: các lần [143] -> thoát 143
Đọc kết quả:
- Ba cách nuốt lỗi đều thoát 0 và để lại dấu vết sai.
|| truevẫn in “xong”; pipeline khôngpipefailthoát 0 dù lệnh đầu trả 3; curl không--failghi JSON báo lỗi vàoseed.json. Bản sửa thoát 1, 3 và 22 và không để lại tệp. --failcho mã 22 với cả 401 lẫn 503. Một mã không đủ để bộ lập lịch chọn giữa dừng và thử lại, nên hợp đồng phải do script tự đặt.- Shell tự đặt 127, 126 và 143. Lệnh không tồn tại, tệp không thực thi được và
SIGTERM(128 + 15) cho ba con số khác nhau, không cần job làm gì. - Bảng hợp đồng ra đúng ở cả hai nhánh mạng. Cổng đóng (curl 7) và quá thời gian (curl 28) đều thành 10, 401 và 403 thành 11, 404 và 302 thành 1. Sau chuỗi ca lỗi, thư mục không còn tệp tạm.
- Khởi tạo thiếu token. Bản cũ thoát 0 và để
seed.jsonchứa JSON báo lỗi, bước sau sẽ đọc nó như dữ liệu.fetch.shthoát 11, không tạo tệp, và wrapper chỉ chạy một lần. Sau một lần thành công, lần chạy thiếu token tiếp theo vẫn thoát 11 và giữ nguyên{"rows": 3}vì tệp đích chỉ bị thay khi nhận 2xx. - Wrapper chỉ thử lại mã 10.
/flakyra các lần 10, 10, 0 rồi thoát 0;503mãi ra 10, 10, 10 và thoát 10 (hết lượt vẫn báo lỗi, không đổi thành 0); 404 (mã 1) vàSIGTERM(mã 143) chỉ chạy một lần.
Tác giả đã phá thử 13 chỗ của fetch.sh và jobwrap.py trong bản sao ngoài lab (đổi 401 hoặc 403 thành lỗi chung, bỏ 5xx hay 408 khỏi “tạm thời”, bỏ mã 7 hoặc 28, bỏ trap, ghi đè tệp đích khi 401, coi 3xx là thành công, thử lại cả mã 11 hoặc mã 1, bỏ đổi tín hiệu thành 128+N, chỉ thử một lần) và cases.py đỏ ở cả 13 lần. Hai chỗ đầu tiên chưa bị bắt (3xx và mã 1) dẫn tới hai ca HTTP 302 và HTTP 404 trong lab.
Thử lại có giới hạn: wrapper làm và không làm gì
- Chỉ mã 10 mới thử lại. Lỗi xác thực (11), lỗi khác (1) và cái chết vì tín hiệu (143) đều dừng ngay. Việc không thử lại tín hiệu là lựa chọn của bài: job bị dừng từ ngoài thường là quyết định của bộ lập lịch hay người vận hành.
- Hết lượt vẫn là thất bại. Sau ba lần thoát 10, wrapper trả 10: bộ lập lịch thấy lỗi và có thể cảnh báo. Wrapper không bao giờ đổi lỗi thành 0.
- Khoảng nghỉ của lab chỉ để chạy nhanh. 0,01 giây nhân số lần là quá ngắn cho dịch vụ thật; thực tế cần khoảng nghỉ dài hơn, có jitter và tổng thời hạn, như bài Batch job: chạy lại an toàn sau lỗi giữa chừng đã nêu.
- Chạy lại chỉ an toàn khi job không gây hiệu ứng kép. Wrapper không biết job có idempotent hay không; bài batch job nói cách chọn identity và checkpoint để chạy lại không nhân đôi kết quả.
- Hai tầng thử lại nhân nhau. Wrapper trong container và số lần thử lại của nền tảng (
backoffLimit,attempts) cho số lần chạy thật bằng tích của hai tầng. Hãy để một tầng quyết định, hoặc tính tích đó vào ngân sách.
Ánh xạ sang Kubernetes và AWS Batch (chưa chạy)
Cả hai nền tảng đều có cách đọc mã thoát, và cả hai mặc định thử lại khi không có luật nào khớp:
| Nền tảng | Mã không khớp luật nào | Căn cứ |
|---|---|---|
Kubernetes podFailurePolicy | xử lý mặc định: Pod được tạo lại nếu số lần khởi động lại còn dưới backoffLimit | “When no rule matches the Pod failure, the default handling applies” |
AWS Batch evaluateOnExit | job được thử lại | “If evaluateOnExit is specified, but none of the retry strategies match, then the job is retried” |
Vì vậy “chỉ thử lại mã 10” cần một luật dừng cho phần còn lại.
Kubernetes. Tài liệu Job nói các luật trong spec.podFailurePolicy.rules “are evaluated in order. Once a rule matches a Pod failure, the remaining rules are ignored”. Hành động FailJob đánh dấu Job thất bại và dừng mọi Pod đang chạy; Count xử lý theo cách mặc định và tăng bộ đếm backoffLimit. Ví dụ của tài liệu dùng onExitCodes với operator: In và values: [42] cho FailJob, trong Pod có restartPolicy: Never, và đây là điều kiện bắt buộc: “you must also define that Job’s pod template with .spec.restartPolicy set to Never”. Với hợp đồng của bài, một luật NotIn là đủ:
spec:
backoffLimit: 6
podFailurePolicy:
rules:
- action: FailJob
onExitCodes:
containerName: main
operator: NotIn
values: [10]
Tài liệu tham chiếu API Job nói mã thoát 0 “are excluded from the requirement check” nên luật chỉ xét container đã thất bại, và NotIn thỏa khi “at least one container exit code (…) is not in the set of specified values”. Mã 10 không khớp luật nào nên rơi vào xử lý mặc định (thử lại đến backoffLimit); mọi mã khác làm Job thất bại ngay.
AWS Batch. Tài liệu cho phép attempts “between 1 and 10”, và khi dùng evaluateOnExit thì phải đặt cả attempts. Mẫu tài liệu đề nghị là các mục RETRY riêng rồi một mục cuối thoát với mọi lý do (“add a final entry that exits for any reason”):
"retryStrategy": {
"attempts": 3,
"evaluateOnExit": [
{ "action": "RETRY", "onExitCode": "10" },
{ "action": "EXIT", "onReason": "*" }
]
}
onExitCode là “a glob pattern to match against the decimal representation of the ExitCode”, chỉ gồm chữ số và có thể kết thúc bằng * “so that only the start of the string needs to be an exact match”. Hệ quả cho hợp đồng này: 10 khớp đúng mã 10, còn 1* khớp cả 1, 10, 11 và 143, một cái bẫy khi chọn mã gần nhau. Các trang đã đọc không nêu thứ tự đánh giá các mục; ví dụ của tài liệu đặt mục EXIT ở cuối cùng.
Cả hai đoạn cấu hình chỉ theo tài liệu, chưa chạy. Trước khi tin, hãy chạy một job cố ý thoát 10, một job thoát 11 và một job thoát 1 trên cụm hay tài khoản của bạn, rồi đếm số lần Pod hay attempt thật.
Giới hạn
- Lab chỉ chạy với bash 3.2.57, curl 8.7.1 và Python 3.14.4 trên macOS arm64. Chưa chạy trên Linux, bash 5, dash, zsh hay BusyBox, và chưa thử curl bản khác; tài liệu curl đọc là bản hiện hành, không phải riêng bản 8.7.1.
- Kubernetes và AWS Batch chỉ được trích tài liệu, không chạy. Tài liệu AWS tự nhắc tối đa 6 mục
evaluateOnExitở trang hướng dẫn nhưng 5 điều kiện ở trang API, nên bài không nêu con số nào. - Mã 10 và 11, danh sách mã HTTP “tạm thời” và việc không thử lại tín hiệu đều là quy ước của bài, không phải chuẩn. Một dịch vụ khác có thể cần danh sách khác.
- API thử là giả lập: không TLS, không chuyển hướng, không
Retry-After, không giới hạn tốc độ, không xác thực nhiều bước. Trường hợp--failđể lọt 401 hoặc 407 mà tài liệu curl nêu không được tái hiện. - Ba cách nuốt lỗi là ba cách hay gặp trong lab, không phải danh sách đầy đủ; bài không xét
trap,exittrong subshell hay các cách khác. - Lab truyền token qua tham số
--headercho ngắn; bài không bàn cách giữ bí mật token trong job. - Lab không đo thời gian thật của thử lại (khoảng nghỉ 0,01 giây) và không mô phỏng hiệu ứng ngoài của job nên không chứng minh chạy lại an toàn.
- Lab không ghi tệp ngoài thư mục bạn đã tạo và thư mục tạm; mọi server tự tắt khi
cases.pykết thúc, kể cả khiasserthỏng.
Học tiếp và nguồn
- Batch job: chạy lại an toàn sau lỗi giữa chừng: identity, checkpoint và retry ở tầng giao dịch, điều kiện để thử lại không nhân đôi kết quả.
- Kiểm kê giấy phép phần mềm: một ví dụ khác dùng mã thoát khác 0 để chặn pipeline khi báo cáo không đạt.
- GNU, Bash Reference Manual: Pipelines, Exit Status và The Set Builtin: trạng thái pipeline, mã 126, 127, 128+N và hành vi của
set -e. - The Open Group, POSIX.1-2024: Shell Command Language: mục 2.8.2, mã thoát của lệnh.
- curl, manpage:
--fail,--fail-with-body,--max-time, biếnhttp_codecủa--write-outvà bảng mã thoát. - Python, subprocess:
returncodeâm khi tiến trình con chết vì tín hiệu. - Kubernetes, Jobs và Job API:
podFailurePolicy,onExitCodes, các hành động. - AWS Batch, Automated job retries, EvaluateOnExit và RetryStrategy:
attempts,evaluateOnExit,onExitCode.
Nguồn trực tuyến đọc ngày 2026-10-04; số đo thuộc bash 3.2.57, curl 8.7.1 và Python 3.14.4 trên macOS arm64.