Easy Prompt
写作生成文字进阶

Retry and Backoff Policy Calculator

来自 prompts.chat 的提示词:Retry and Backoff Policy Calculator

提示词正文

复制后可直接粘贴到模型或内部评测工具。


name: retry-backoff-policy-calculator description: Checks retry, backoff and timeout settings across a call chain to prevent retry storms. Computes exponential delay schedules with full, equal or decorrelated jitter, worst-case latency per layer, expected tries and request amplification. Flags inner timeouts that outlive the caller, retries of non-idempotent calls, retries on 4xx, ignored Retry-After and missing caps or deadlines. Includes a stdlib Python calculator with --self-test.

Retry and Backoff Policy Calculator

You help engineers set retry, backoff and timeout policies that make services more reliable instead of turning a small outage into a big one. You collect the settings of every hop in a call chain (browser, gateway, services, databases, third-party APIs), compute what they really do under failure, and propose a safer policy with numbers.

Files in this skill

  • scripts/retry_policy.py - reads a call-chain JSON and prints delay schedules, worst-case latency, expected tries, amplification and flags (Python 3 standard library only)
  • references/backoff-math.md - formulas for backoff, jitter, worst-case time, expected tries, amplification and timeout nesting
  • references/retry-safety-guide.md - what is safe to retry, idempotency keys, circuit breakers, retry budgets, common library settings
  • templates/retry-policy-review.md - review report with current chain, findings and proposed policy
  • examples/checkout-chain.json - a three-layer checkout chain with typical mistakes

Workflow

1. Map the chain

Ask for each hop, outermost first: attempts (total tries), base delay, multiplier, max delay, jitter type, per-try timeout, overall deadline, what it retries on (status codes, "timeout", "network"), whether the call is idempotent, whether an idempotency key is sent, and whether Retry-After is honored. Read them from code or config (SDK clients, HTTP client retry adapters, Envoy/Istio routes, gRPC service config, job queues). Unknown values: use the library default and mark them as assumed.

2. Run the calculator

python3 scripts/retry_policy.py examples/checkout-chain.json
python3 scripts/retry_policy.py chain.json --failure-rate 0.5 --max-amplification 8
python3 scripts/retry_policy.py chain.json --json > retry-report.json
python3 scripts/retry_policy.py --self-test

Exit code 1 means high-severity findings, 2 means bad input.

3. Explain the findings

Order: RETRY_UNSAFE and AMPLIFICATION first (they cause duplicate side effects and outages), then INNER_OUTLIVES_CALLER, HOT_LOOP, NO_PER_TRY_TIMEOUT, then medium and low. Explain each with the numbers from the report, for example "one checkout can become 36 calls to the payments provider".

4. Propose a fixed policy

Use references/backoff-math.md and references/retry-safety-guide.md: retry at one layer, full jitter with a cap, inner worst case below the caller's per-try timeout, deadlines passed down, idempotency keys for writes, no retries on 4xx except 408/429, Retry-After honored, a retry budget or circuit breaker. Re-run the script on the proposed JSON and show before and after.

5. Write it up

Fill templates/retry-policy-review.md and give config snippets for the user's library.

Example result

For examples/checkout-chain.json at 30% failure rate: worst-case amplification 36 calls per checkout (expected 2.74); the gateway layer's 9.3 s worst case outlives the web app's 8 s per-try timeout; the payments layer retries a non-idempotent charge 4 times without an idempotency key and retries 409; no jitter at the gateway; Retry-After ignored on 429/503.

Rules

  • Never recommend retrying a non-idempotent call without an idempotency key.
  • Keep numbers honest: label library defaults and guesses as assumptions.
  • Prefer fewer retries with good timeouts over many retries.
  • Recommend load tests or fault injection to confirm the policy, not just the math. FILE:references/backoff-math.md

Backoff and Timeout Math

Terms

  • attempts: total tries including the first. attempts 3 = 1 try + 2 retries.
  • base: first retry delay. multiplier: growth factor (usually 2). cap: max single delay.
  • per-try timeout: how long one try may take before it is abandoned.
  • deadline: total time budget for the call including all tries and delays.

Delay before retry k (k = 1 .. attempts-1)

d(k) = min(cap, base x multiplier^(k-1))

JitterDelayRangeMean
noneddd
fullrandom(0, d)0 .. dd/2
equald/2 + random(0, d/2)d/2 .. d0.75 d
decorrelatedmin(cap, random(base, 3 x previous))base .. min(cap, base x 3^k)simulated

Full jitter spreads load best and is the default recommendation. Equal jitter keeps a minimum wait. Decorrelated jitter adapts to the previous delay and works well for long-running clients.

Worst-case duration of one call

worst = attempts x per_try_timeout + sum of max delays, then cut at the deadline if one is set. Example: 3 tries x 1 s + 100 ms + 200 ms = 3.3 s.

Expected tries

With an independent failure probability p per try and n attempts: E[tries] = 1 + p + p^2 + ... + p^(n-1) p = 0.3, n = 3 -> 1.39. During a full outage p = 1 and E = n.

Amplification across layers

If layer A retries 3 times and calls B, which retries 3 times and calls C, which retries 4 times, one user request can become 3 x 3 x 4 = 36 calls to C during an outage. This is a retry storm: the retries keep the failing service overloaded, so it cannot recover.

Rules of thumb:

  • Retry at one layer only, usually the one closest to the user or the one that owns the idempotency key.
  • Inner layers: attempts 1 or 2 with a short deadline.
  • Keep worst-case amplification at or below about 10.
  • Add a retry budget: retries may be at most 10-20% of requests over a sliding window; past that, fail fast.

Timeout nesting

Each layer's worst case must fit inside its caller's per-try timeout, minus a margin for network and work: inner_worst <= outer_per_try_timeout - margin Otherwise the caller abandons the try and retries while the inner layer is still working, which doubles load. Pass a deadline down the chain (for example a grpc-timeout or X-Request-Deadline header) and stop when it is used up.

Retry-After

For 429 and 503, servers may send Retry-After: <seconds> or an HTTP date. Wait at least that long (use max(Retry-After, computed backoff)), and give up if it is longer than the remaining deadline. FILE:references/retry-safety-guide.md

Retry Safety Guide

What to retry

OutcomeRetry?Notes
Connection refused / reset before the request was sentYesSafe for any method
Timeout after the request was sentOnly if idempotent or with an idempotency keyThe server may have done the work
408 Request TimeoutYes, with backoff
429 Too Many RequestsYes, after Retry-AfterNever faster than the server asks
500 Internal Server ErrorSometimesOnly if the error is known to be transient
502 / 503 / 504Yes, with backoff and jitterHonor Retry-After on 503
400 / 401 / 403 / 404 / 405 / 410 / 422NoSame request fails the same way; fix the request, refresh auth once at most
409 ConflictUsually noException: an idempotency key that is still in progress, retry after a short wait

Idempotency

  • GET, HEAD, OPTIONS, PUT and DELETE are idempotent by HTTP semantics, if the server implements them correctly.
  • POST and PATCH are not. To retry them, send an idempotency key (a UUID per logical operation, e.g. Idempotency-Key header) that the server stores with the result for 24 hours or more. Generate the key once, before the first try, and reuse it for every retry.
  • Payments, emails, SMS, order creation and stock reservation must never be retried without a key.

Client checklist

  1. Per-try timeout set and shorter than the caller's.
  2. Overall deadline set and passed down the chain.
  3. Exponential backoff with a cap and full jitter.
  4. attempts <= 3 at the edge, 1-2 inside.
  5. Retry only on the table's "Yes" rows.
  6. Idempotency key on every retried non-idempotent call.
  7. Retry budget or circuit breaker so retries stop during an outage.
  8. Metrics: tries per request, retry rate, give-up count, by dependency.
  9. Log the final error with the attempt count, not every attempt at error level.

Circuit breaker basics

  • Closed: calls flow; count failures in a rolling window.
  • Open: after for example 50% failures over at least 20 calls, fail fast for 30 s.
  • Half-open: let a few trial calls through; close if they succeed. A breaker and a retry budget together stop retry storms from deepening an outage.

Common library settings

  • AWS SDKs: standard mode = 3 attempts, full jitter, base 1 s for throttling; adaptive mode adds client-side rate limiting.
  • gRPC retry policy: maxAttempts, initialBackoff, maxBackoff, backoffMultiplier, retryableStatusCodes (UNAVAILABLE is the usual one); retries need hedging/transparent retry rules for non-idempotent methods.
  • Envoy / Istio: retry_on: 5xx,reset,connect-failure; num_retries; per_try_timeout; set route timeout >= per_try_timeout x (num_retries + 1).
  • Python urllib3.Retry(total=3, backoff_factor=0.5, status_forcelist=[502,503,504], respect_retry_after_header=True, allowed_methods=[...]). Check the library docs for the version you use; defaults change. FILE:templates/retry-policy-review.md

Retry Policy Review - <service / call chain> - <date>

Reviewer: <name> Input: <chain JSON / config files> Failure rate assumed: <30%>

Chain

#Caller -> calleeAttemptsBackoff (base / x / cap)JitterPer-try timeoutDeadlineWorst case
1

Worst-case amplification: <36> calls per request (target <= 10) Expected amplification at <p>: <2.7>

Findings

SeverityLayerCodeProblemFix
high

Proposed policy

#Caller -> calleeAttemptsBackoffJitterPer-try timeoutDeadlineRetry on
1full

New worst-case amplification: <n> New worst-case latency: <s>

Follow-ups

  • Idempotency keys for: <calls>
  • Retry budget / circuit breaker for: <dependency>
  • Dashboards: tries per request, retry rate, give-ups FILE:examples/checkout-chain.json { "name": "checkout: browser -> api gateway -> orders service -> payments provider", "failure_rate": 0.3, "max_amplification": 10, "layers": [ { "name": "web app -> api gateway", "attempts": 3, "base_delay_ms": 200, "multiplier": 2, "max_delay_ms": 2000, "jitter": "full", "per_try_timeout_ms": 8000, "deadline_ms": 20000, "retry_on": ["network", 502, 503, 504], "idempotent": true, "respect_retry_after": true }, { "name": "api gateway -> orders service", "attempts": 3, "base_delay_ms": 100, "multiplier": 2, "max_delay_ms": 1000, "jitter": "none", "per_try_timeout_ms": 3000, "retry_on": [500, 502, 503, 504, "timeout"], "idempotent": true }, { "name": "orders service -> payments provider (create charge)", "attempts": 4, "base_delay_ms": 250, "multiplier": 2, "jitter": "equal", "per_try_timeout_ms": 2500, "deadline_ms": 9000, "retry_on": [409, 429, 500, 503, "timeout"], "idempotent": false, "idempotency_key": false, "respect_retry_after": false } ] } FILE:scripts/retry_policy.py #!/usr/bin/env python3 """Retry and backoff policy calculator (Python 3 standard library only).

Input: a JSON file describing a call chain, outermost caller first: {"name": "checkout", "failure_rate": 0.3, "max_amplification": 10, "layers": [ {"name": "web -> api", "attempts": 3, "base_delay_ms": 100, "multiplier": 2, "max_delay_ms": 2000, "jitter": "full", "per_try_timeout_ms": 5000, "deadline_ms": 12000, "retry_on": [502, 503, 504, "timeout"], "idempotent": true, "respect_retry_after": true}, ...]} attempts = total tries including the first one (attempts 1 = no retries). jitter: none | full | equal | decorrelated. Delay before retry k (k = 1..attempts-1): none d = min(cap, base * multiplier^(k-1)) full U(0, d) equal d/2 + U(0, d/2) decorrelated min(cap, U(base, 3 * previous)) (AWS style; expected value simulated, seed 7)

Reports per layer: delay schedule (min / expected / max), worst-case time for one call (attempts x per-try timeout + max delays, cut at deadline), expected tries at the given failure rate, and for the chain: worst-case and expected request amplification at the innermost dependency. Flags: NO_JITTER, NO_CAP, HOT_LOOP (base 0), TOO_MANY_ATTEMPTS (> 5), NO_PER_TRY_TIMEOUT, NO_DEADLINE, DEADLINE_TOO_SHORT (deadline < one per-try timeout), INNER_OUTLIVES_CALLER (worst case > the outer layer's per-try timeout), RETRY_UNSAFE (non-idempotent with retries and no idempotency key), RETRY_ON_4XX (retrying 400/401/403/404/409/422), IGNORES_RETRY_AFTER (retries 429/503 without honoring Retry-After), AMPLIFICATION. Usage: python3 retry_policy.py chain.json python3 retry_policy.py chain.json --failure-rate 0.5 --json python3 retry_policy.py --self-test Exit code: 0 OK, 1 high-severity flags found, 2 bad input. """ import argparse import json import random import sys

JITTERS = ("none", "full", "equal", "decorrelated") BAD_4XX = {400, 401, 403, 404, 405, 409, 410, 422} SEVERITY = {"INNER_OUTLIVES_CALLER": "high", "RETRY_UNSAFE": "high", "AMPLIFICATION": "high", "HOT_LOOP": "high", "NO_PER_TRY_TIMEOUT": "high", "RETRY_ON_4XX": "medium", "NO_JITTER": "medium", "NO_CAP": "medium", "IGNORES_RETRY_AFTER": "medium", "NO_DEADLINE": "medium", "DEADLINE_TOO_SHORT": "medium", "TOO_MANY_ATTEMPTS": "low"}

class PolicyError(ValueError): pass

def num(layer, key, default=None, minimum=0): v = layer.get(key, default) if v is None: return None if isinstance(v, bool) or not isinstance(v, (int, float)): raise PolicyError(f"layer {layer.get('name', '?')!r}: {key} must be a number") if v < minimum: raise PolicyError(f"layer {layer.get('name', '?')!r}: {key} must be >= {minimum}") return v

def delays(layer): """Return [(min, expected, max)] in ms for each retry.""" n = int(layer["attempts"]) base, mult, cap = layer["base_delay_ms"], layer["multiplier"], layer["max_delay_ms"] capf = (lambda x: x) if cap is None else (lambda x: min(cap, x)) j = layer["jitter"] out = [] if j == "decorrelated": rng = random.Random(7) runs = 20000 sums = [0.0] * (n - 1) for _ in range(runs): prev = base for k in range(n - 1): prev = capf(rng.uniform(base, prev * 3)) if base > 0 else 0.0 sums[k] += prev hi = base for k in range(n - 1): hi = capf(hi * 3) out.append((capf(base), sums[k] / runs, hi)) return out for k in range(n - 1): d = capf(base * mult ** k) if j == "none": out.append((d, d, d)) elif j == "full": out.append((0.0, d / 2, d)) else: out.append((d / 2, d * 0.75, d)) return out

def normalize(raw): layers = raw.get("layers") if isinstance(raw, dict) else None if not layers or not isinstance(layers, list): raise PolicyError("JSON must have a non-empty 'layers' list (outermost caller first)") out = [] for i, l in enumerate(layers): if not isinstance(l, dict): raise PolicyError(f"layer {i} must be an object") l = dict(l) l.setdefault("name", f"layer {i + 1}") a = num(l, "attempts", 1, 1) if int(a) != a: raise PolicyError(f"layer {l['name']!r}: attempts must be a whole number") l["attempts"] = int(a) l["base_delay_ms"] = num(l, "base_delay_ms", 0) l["multiplier"] = num(l, "multiplier", 2, 1) l["max_delay_ms"] = num(l, "max_delay_ms", None) l["per_try_timeout_ms"] = num(l, "per_try_timeout_ms", None, 1) l["deadline_ms"] = num(l, "deadline_ms", None, 1) j = str(l.get("jitter", "none")).lower() if j not in JITTERS: raise PolicyError(f"layer {l['name']!r}: jitter must be one of {', '.join(JITTERS)}") l["jitter"] = j l["retry_on"] = list(l.get("retry_on", [])) l["idempotent"] = bool(l.get("idempotent", True)) l["idempotency_key"] = bool(l.get("idempotency_key", False)) l["respect_retry_after"] = bool(l.get("respect_retry_after", False)) out.append(l) return out

def expected_tries(n, p): if p >= 1: return float(n) return sum(p ** k for k in range(n))

def analyze(raw, failure_rate=None, max_amp=None): layers = normalize(raw) p = failure_rate if failure_rate is not None else raw.get("failure_rate", 0.2) if not 0 <= p <= 1: raise PolicyError("failure rate must be between 0 and 1") max_amp = max_amp if max_amp is not None else raw.get("max_amplification", 10) res = {"name": raw.get("name", "retry policy"), "failure_rate": p, "max_amplification": max_amp, "layers": []} worst_amp, exp_amp = 1, 1.0 for i, l in enumerate(layers): n = l["attempts"] ds = delays(l) flags = [] retries = n > 1 if retries and l["jitter"] == "none": flags.append(("NO_JITTER", "all clients retry at the same moments; use full or decorrelated jitter")) if retries and l["max_delay_ms"] is None and n > 3: flags.append(("NO_CAP", "no max_delay_ms; exponential delays grow without bound")) if retries and l["base_delay_ms"] == 0: flags.append(("HOT_LOOP", "base_delay_ms is 0; retries fire immediately and hammer the dependency")) if n > 5: flags.append(("TOO_MANY_ATTEMPTS", f"{n} attempts; 3 (2 retries) is usually enough")) if l["per_try_timeout_ms"] is None: flags.append(("NO_PER_TRY_TIMEOUT", "no per-try timeout; one hung call can block every retry")) if l["deadline_ms"] is None and retries: flags.append(("NO_DEADLINE", "no overall deadline across attempts")) if l["deadline_ms"] is not None and l["per_try_timeout_ms"] and l["deadline_ms"] < l["per_try_timeout_ms"]: flags.append(("DEADLINE_TOO_SHORT", "deadline is shorter than a single try")) if retries and not l["idempotent"] and not l["idempotency_key"]: flags.append(("RETRY_UNSAFE", "retries a non-idempotent call without an idempotency key (double charge / duplicate write risk)")) bad = sorted(c for c in l["retry_on"] if isinstance(c, int) and c in BAD_4XX) if retries and bad: flags.append(("RETRY_ON_4XX", f"retries client errors {bad}; they will fail the same way again")) if retries and any(c in (429, 503) for c in l["retry_on"]) and not l["respect_retry_after"]: flags.append(("IGNORES_RETRY_AFTER", "retries 429/503 but ignores the Retry-After header")) max_delay_total = sum(d[2] for d in ds) exp_delay_total = sum(d[1] for d in ds) worst = None if l["per_try_timeout_ms"] is not None: worst = n * l["per_try_timeout_ms"] + max_delay_total if l["deadline_ms"] is not None: worst = min(worst, l["deadline_ms"]) elif l["deadline_ms"] is not None: worst = l["deadline_ms"] if i > 0: parent = layers[i - 1] pt = parent["per_try_timeout_ms"] if pt is not None and (worst is None or worst > pt): flags.append(("INNER_OUTLIVES_CALLER", f"worst case {fmt_ms(worst)} > caller per-try timeout {fmt_ms(pt)}; the caller gives up and retries while this layer is still working")) et = expected_tries(n, p) worst_amp *= n exp_amp *= et res["layers"].append({ "name": l["name"], "attempts": n, "jitter": l["jitter"], "delays_ms": [[round(a, 1), round(b, 1), round(c, 1)] for a, b, c in ds], "max_delay_total_ms": round(max_delay_total, 1), "expected_delay_total_ms": round(exp_delay_total, 1), "per_try_timeout_ms": l["per_try_timeout_ms"], "deadline_ms": l["deadline_ms"], "worst_case_ms": None if worst is None else round(worst, 1), "expected_tries": round(et, 3), "cumulative_worst_amplification": worst_amp, "cumulative_expected_amplification": round(exp_amp, 3), "flags": [{"code": c, "severity": SEVERITY[c], "detail": d} for c, d in flags]}) if worst_amp > max_amp: res["layers"][-1]["flags"].append({"code": "AMPLIFICATION", "severity": "high", "detail": f"one top-level request can become {worst_amp} calls to the innermost dependency during an outage (limit {max_amp}); retry at one layer only or add a retry budget"}) res["worst_amplification"] = worst_amp res["expected_amplification"] = round(exp_amp, 3) res["high_flags"] = sum(1 for l in res["layers"] for f in l["flags"] if f["severity"] == "high") return res

def fmt_ms(v): if v is None: return "unbounded" if v >= 1000: s = f"{v / 1000:.2f}".rstrip("0").rstrip(".") return s + " s" return f"{v:g} ms"

def to_markdown(r): out = [f"# Retry policy report: {r['name']}", "", f"Failure rate used for expected values: {r['failure_rate']:.0%}", "", "| Layer | Attempts | Jitter | Max total delay | Per-try timeout | Deadline | Worst case | Expected tries |", "|---|---|---|---|---|---|---|---|"] for l in r["layers"]: out.append(f"| {l['name']} | {l['attempts']} | {l['jitter']} | {fmt_ms(l['max_delay_total_ms'])} | " f"{fmt_ms(l['per_try_timeout_ms'])} | {fmt_ms(l['deadline_ms'])} | {fmt_ms(l['worst_case_ms'])} | {l['expected_tries']:g} |") out += ["", "## Delay schedule (min / expected / max)"] for l in r["layers"]: if not l["delays_ms"]: out.append(f"- {l['name']}: no retries") continue parts = [f"retry {k + 1}: {fmt_ms(a)} / {fmt_ms(b)} / {fmt_ms(c)}" for k, (a, b, c) in enumerate(l["delays_ms"])] out.append(f"- {l['name']}: " + "; ".join(parts)) out += ["", "## Amplification at the innermost dependency", f"- Worst case (every try fails): {r['worst_amplification']} calls per top-level request", f"- Expected at {r['failure_rate']:.0%} failure rate: {r['expected_amplification']:g} calls per top-level request", "", "## Findings"] n = 0 for sev in ("high", "medium", "low"): for l in r["layers"]: for f in l["flags"]: if f["severity"] == sev: n += 1 out.append(f"- {sev.upper()} {f['code']} ({l['name']}): {f['detail']}") if not n: out.append("- No findings.") return "\n".join(out)

def self_test(): cases = []

def ok(name, cond):
    cases.append((name, bool(cond)))

def one(**kw):
    base = {"name": "x", "attempts": 3, "base_delay_ms": 100, "multiplier": 2, "max_delay_ms": 1000,
            "jitter": "full", "per_try_timeout_ms": 1000, "deadline_ms": 5000, "retry_on": [503],
            "respect_retry_after": True}
    base.update(kw)
    return base

def codes(r, i=0):
    return {f["code"] for f in r["layers"][i]["flags"]}

d = delays(normalize({"layers": [one(jitter="none", attempts=5)]})[0])
ok("exponential no jitter 100,200,400,800", [x[2] for x in d] == [100, 200, 400, 800])
d = delays(normalize({"layers": [one(jitter="none", attempts=6)]})[0])
ok("cap applied at 1000", d[-1][2] == 1000 and d[-2][2] == 800)
d = delays(normalize({"layers": [one(jitter="full")]})[0])
ok("full jitter mean half", d[0] == (0.0, 50.0, 100))
d = delays(normalize({"layers": [one(jitter="equal")]})[0])
ok("equal jitter 50..100 mean 75", d[0] == (50.0, 75.0, 100))
d = delays(normalize({"layers": [one(jitter="decorrelated", attempts=4)]})[0])
ok("decorrelated within bounds", all(a <= b <= c for a, b, c in d) and d[0][2] == 300 and d[2][2] == 1000)
r = analyze({"layers": [one()]})
ok("clean policy has no flags", not codes(r))
ok("worst case = 3x1000 + 100 + 200", r["layers"][0]["worst_case_ms"] == 3300)
r = analyze({"layers": [one(deadline_ms=2000)]})
ok("deadline caps worst case", r["layers"][0]["worst_case_ms"] == 2000)
ok("expected tries p=0.5 n=3 is 1.75", expected_tries(3, 0.5) == 1.75 and expected_tries(3, 1) == 3)
ok("no jitter flagged", "NO_JITTER" in codes(analyze({"layers": [one(jitter="none")]})))
ok("hot loop flagged", "HOT_LOOP" in codes(analyze({"layers": [one(base_delay_ms=0)]})))
ok("no cap flagged with 4+ attempts", "NO_CAP" in codes(analyze({"layers": [one(max_delay_ms=None, attempts=4)]})))
ok("unsafe retry flagged", "RETRY_UNSAFE" in codes(analyze({"layers": [one(idempotent=False)]})))
ok("idempotency key makes it safe", "RETRY_UNSAFE" not in codes(analyze({"layers": [one(idempotent=False, idempotency_key=True)]})))
ok("4xx retry flagged", "RETRY_ON_4XX" in codes(analyze({"layers": [one(retry_on=[400, 503])]})))
ok("Retry-After ignored flagged", "IGNORES_RETRY_AFTER" in codes(analyze({"layers": [one(retry_on=[429], respect_retry_after=False)]})))
ok("single attempt is not a retry policy", not codes(analyze({"layers": [one(attempts=1, jitter="none", deadline_ms=None)]})))
r = analyze({"layers": [one(per_try_timeout_ms=2000, deadline_ms=None), one(per_try_timeout_ms=1000, deadline_ms=None)]})
ok("inner outlives caller flagged", "INNER_OUTLIVES_CALLER" in codes(r, 1))
r = analyze({"layers": [one(per_try_timeout_ms=4000), one(per_try_timeout_ms=1000, deadline_ms=3500)]})
ok("inner within caller timeout ok", "INNER_OUTLIVES_CALLER" not in codes(r, 1))
r = analyze({"layers": [one(), one(), one()], "max_amplification": 10})
ok("3x3x3 amplification 27 flagged", r["worst_amplification"] == 27 and "AMPLIFICATION" in codes(r, 2))
r = analyze({"layers": [one(), one()]}, failure_rate=0.5)
ok("expected amplification 1.75^2", r["expected_amplification"] == 3.062)
for bad, label in ((({"layers": []}), "empty layers rejected"), (({"layers": [one(jitter="random")]}), "bad jitter rejected"),
                   (({"layers": [one(attempts=0)]}), "zero attempts rejected"), (({"layers": [one(attempts=2.5)]}), "fractional attempts rejected")):
    try:
        analyze(bad)
        ok(label, False)
    except PolicyError:
        ok(label, True)
ok("fmt_ms", fmt_ms(1500) == "1.5 s" and fmt_ms(250) == "250 ms" and fmt_ms(None) == "unbounded")
passed = sum(c for _, c in cases)
for name, c in cases:
    print(("PASS " if c else "FAIL ") + name)
print(f"{passed}/{len(cases)} passed")
return 0 if passed == len(cases) else 1

def main(argv=None): ap = argparse.ArgumentParser(description="Check retry/backoff/timeout policies across a call chain.") ap.add_argument("file", nargs="?", help="chain JSON ('-' for stdin)") ap.add_argument("--failure-rate", type=float, help="probability that one try fails (default from file or 0.2)") ap.add_argument("--max-amplification", type=float, help="flag chains above this worst-case amplification (default 10)") ap.add_argument("--json", action="store_true") ap.add_argument("--self-test", action="store_true") a = ap.parse_args(argv) if a.self_test: return self_test() if not a.file: ap.error("chain JSON required (or --self-test)") try: raw = json.load(sys.stdin if a.file == "-" else open(a.file, encoding="utf-8")) r = analyze(raw, a.failure_rate, a.max_amplification) except (OSError, PolicyError, ValueError) as e: print(f"error: {e}", file=sys.stderr) return 2 print(json.dumps(r, indent=2) if a.json else to_markdown(r)) return 1 if r["high_flags"] else 0

if name == "main": sys.exit(main())

使用场景

同步入库后可继续人工润色

参考输出

暂无标准答案,建议按评分维度人工评审。

评分维度

重点评估可执行性、事实准确性、边界控制和结构完整度。

试用与模板

填写变量后复制,或保存到个人工作台模板。

这个模板没有变量,可直接复制使用。

用户评分

0 个评分
-

你的评分

登录后评分

评论

0

登录后评论

相关提示词