FA-30836 / HTTP retries / Open access
Retry hook failure containment: Cancel pending hook violates retry transition semantics · case 01
Cancel pending hook violates retry transition semantics
ROOT CAUSE
Cancel pending hook violates retry transition semantics
VERIFIED REPAIR
Restore the specified transition elif e[0]=='cancel': state='cancelled'.
Unsuccessful approach: The attempted repair changes the faulty site to elif e[0]=='cancel': state='running' but still violates a regression oracle.
Case contract
begin(cap) starts pending request. fail(eligible,before-hook-ok) calls retry hook only for eligible failures with remaining cap; a failing hook terminally records hook-error without spending retry budget. Otherwise admit consumes one cap and enters waiting. wake starts it. success(observer-ok) stores success before observer invocation; observer failure is diagnostic only and never creates a retry. cancel is terminal. Return lifecycle decisions and remaining cap.
Why this case matters
Offline deterministic model of HTTP request retries.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(events):
cap=0; state='idle'; out=[]
for e in events:
if e[0]=='begin': cap=e[1]; state='running'
elif e[0]=='fail':
if state!='running': out.append('ignored'); continue
if not e[1]: state='failed'; out.append('fatal'); continue
if cap==0: state='failed'; out.append('exhausted'); continue
if not e[2]: state='failed'; out.append('hook-error'); continue
cap-=1; state='waiting'; out.append('scheduled')
elif e[0]=='wake':
if state=='waiting': state='running'; out.append('started')
elif e[0]=='success':
if state!='running': out.append('ignored'); continue
state='done'; out.append('success')
if not e[1]: out.append('observer-error')
elif e[0]=='cancel': state='waiting'
return [out,cap,state]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
check('0', solve([]), [[],0,'idle'])
check('1', solve([('begin',2),('fail',True,True),('fail',True,True),('wake',),('success',False),('fail',True,True)]), [['scheduled','ignored','started','success','observer-error','ignored'],1,'done'])
check('2', solve([('begin',N),('fail',True,False),('wake',)]), [['hook-error'],N,'failed'])
check('3', solve([('begin',N),('fail',False,False)]), [['fatal'],N,'failed'])
check('4', solve([('begin',0),('fail',True,False)]), [['exhausted'],0,'failed'])
check('5', solve([('begin',N),('success',True),('success',False)]), [['success','ignored'],N,'done'])
check('6', solve([('begin',N),('fail',True,True),('cancel',),('wake',),('success',False)]), [['scheduled','ignored'],N-1,'cancelled'])
check('7', solve([('begin',N),('fail',False,True)]), [['fatal'],N,'failed'])
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 0 | [[], 0, 'idle'] | [[], 0, 'idle'] | Passed |
| 1 | [['scheduled', 'ignored', 'started', 'success', 'observer-error', 'ignored'], 1, 'done'] | [['scheduled', 'ignored', 'started', 'success', 'observer-error', 'ignored'], 1, 'done'] | Passed |
| 2 | [['hook-error'], 1, 'failed'] | [['hook-error'], 1, 'failed'] | Passed |
| 3 | [['fatal'], 1, 'failed'] | [['fatal'], 1, 'failed'] | Passed |
| 4 | [['exhausted'], 0, 'failed'] | [['exhausted'], 0, 'failed'] | Passed |
| 5 | [['success', 'ignored'], 1, 'done'] | [['success', 'ignored'], 1, 'done'] | Passed |
| 6 | [['scheduled', 'started', 'success', 'observer-error'], 0, 'done'] | [['scheduled', 'ignored'], 0, 'cancelled'] | Failed |
| 7 | [['fatal'], 1, 'failed'] | [['fatal'], 1, 'failed'] | Passed |
SHA-256 / 41e8f96584d4a18ba4d7b41b010f325f5b2098aec9a3a6917e093d680276dcf8
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(events):
cap=0; state='idle'; out=[]
for e in events:
if e[0]=='begin': cap=e[1]; state='running'
elif e[0]=='fail':
if state!='running': out.append('ignored'); continue
if not e[1]: state='failed'; out.append('fatal'); continue
if cap==0: state='failed'; out.append('exhausted'); continue
if not e[2]: state='failed'; out.append('hook-error'); continue
cap-=1; state='waiting'; out.append('scheduled')
elif e[0]=='wake':
if state=='waiting': state='running'; out.append('started')
elif e[0]=='success':
if state!='running': out.append('ignored'); continue
state='done'; out.append('success')
if not e[1]: out.append('observer-error')
elif e[0]=='cancel': state='running'
return [out,cap,state]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
check('0', solve([]), [[],0,'idle'])
check('1', solve([('begin',2),('fail',True,True),('fail',True,True),('wake',),('success',False),('fail',True,True)]), [['scheduled','ignored','started','success','observer-error','ignored'],1,'done'])
check('2', solve([('begin',N),('fail',True,False),('wake',)]), [['hook-error'],N,'failed'])
check('3', solve([('begin',N),('fail',False,False)]), [['fatal'],N,'failed'])
check('4', solve([('begin',0),('fail',True,False)]), [['exhausted'],0,'failed'])
check('5', solve([('begin',N),('success',True),('success',False)]), [['success','ignored'],N,'done'])
check('6', solve([('begin',N),('fail',True,True),('cancel',),('wake',),('success',False)]), [['scheduled','ignored'],N-1,'cancelled'])
check('7', solve([('begin',N),('fail',False,True)]), [['fatal'],N,'failed'])
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 0 | [[], 0, 'idle'] | [[], 0, 'idle'] | Passed |
| 1 | [['scheduled', 'ignored', 'started', 'success', 'observer-error', 'ignored'], 1, 'done'] | [['scheduled', 'ignored', 'started', 'success', 'observer-error', 'ignored'], 1, 'done'] | Passed |
| 2 | [['hook-error'], 1, 'failed'] | [['hook-error'], 1, 'failed'] | Passed |
| 3 | [['fatal'], 1, 'failed'] | [['fatal'], 1, 'failed'] | Passed |
| 4 | [['exhausted'], 0, 'failed'] | [['exhausted'], 0, 'failed'] | Passed |
| 5 | [['success', 'ignored'], 1, 'done'] | [['success', 'ignored'], 1, 'done'] | Passed |
| 6 | [['scheduled', 'success', 'observer-error'], 0, 'done'] | [['scheduled', 'ignored'], 0, 'cancelled'] | Failed |
| 7 | [['fatal'], 1, 'failed'] | [['fatal'], 1, 'failed'] | Passed |
SHA-256 / d7b67943c502d7a7ce727f447c597e16999b0779e65e8d5f6e1feac024f63171
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(events):
cap=0; state='idle'; out=[]
for e in events:
if e[0]=='begin': cap=e[1]; state='running'
elif e[0]=='fail':
if state!='running': out.append('ignored'); continue
if not e[1]: state='failed'; out.append('fatal'); continue
if cap==0: state='failed'; out.append('exhausted'); continue
if not e[2]: state='failed'; out.append('hook-error'); continue
cap-=1; state='waiting'; out.append('scheduled')
elif e[0]=='wake':
if state=='waiting': state='running'; out.append('started')
elif e[0]=='success':
if state!='running': out.append('ignored'); continue
state='done'; out.append('success')
if not e[1]: out.append('observer-error')
elif e[0]=='cancel': state='cancelled'
return [out,cap,state]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
check('0', solve([]), [[],0,'idle'])
check('1', solve([('begin',2),('fail',True,True),('fail',True,True),('wake',),('success',False),('fail',True,True)]), [['scheduled','ignored','started','success','observer-error','ignored'],1,'done'])
check('2', solve([('begin',N),('fail',True,False),('wake',)]), [['hook-error'],N,'failed'])
check('3', solve([('begin',N),('fail',False,False)]), [['fatal'],N,'failed'])
check('4', solve([('begin',0),('fail',True,False)]), [['exhausted'],0,'failed'])
check('5', solve([('begin',N),('success',True),('success',False)]), [['success','ignored'],N,'done'])
check('6', solve([('begin',N),('fail',True,True),('cancel',),('wake',),('success',False)]), [['scheduled','ignored'],N-1,'cancelled'])
check('7', solve([('begin',N),('fail',False,True)]), [['fatal'],N,'failed'])
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 0 | [[], 0, 'idle'] | [[], 0, 'idle'] | Passed |
| 1 | [['scheduled', 'ignored', 'started', 'success', 'observer-error', 'ignored'], 1, 'done'] | [['scheduled', 'ignored', 'started', 'success', 'observer-error', 'ignored'], 1, 'done'] | Passed |
| 2 | [['hook-error'], 1, 'failed'] | [['hook-error'], 1, 'failed'] | Passed |
| 3 | [['fatal'], 1, 'failed'] | [['fatal'], 1, 'failed'] | Passed |
| 4 | [['exhausted'], 0, 'failed'] | [['exhausted'], 0, 'failed'] | Passed |
| 5 | [['success', 'ignored'], 1, 'done'] | [['success', 'ignored'], 1, 'done'] | Passed |
| 6 | [['scheduled', 'ignored'], 0, 'cancelled'] | [['scheduled', 'ignored'], 0, 'cancelled'] | Passed |
| 7 | [['fatal'], 1, 'failed'] | [['fatal'], 1, 'failed'] | Passed |
SHA-256 / 06065f51f00d5003576e0940508b0051d6a7ffa05b1aec0009a7e3582d63776a
Verification & scope
Stipulated bounded simulator, not a complete HTTP implementation or a standards conformance claim. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.
Observations recorded using Python 3.12.14 at 2026-09-29T14:41:56.511971+00:00.
Case digest / 78045cca77957ae47aa579ce53c6a7a184b9a7b5cc53df03e7332a47b822725a