{"id":"rate-limit-handling-with-retry","version":"1.0.0","primitive":"code_execution","description":"Handle API rate limits with exponential backoff and jitter using tenacity","registry_refs":["tenacity","requests"],"tags":[],"solves":[],"auth_required":false,"verified":false,"last_verified":"null","next_check":"2026-07-19","eval_result":"null","eval_env":"null","mast":[],"ref":"https://arxiv.org/abs/2503.13657","inputs":[],"executable":"# ============================================\n# checklist:     rate-limit-handling-with-retry\n# version:       1.0.0\n# primitive:     code_execution\n# description:   Handle API rate limits with exponential backoff and jitter using tenacity\n# registry_refs: tenacity, requests\n# auth_required: false\n# verified:      false\n# last_verified: null\n# next_check:    2026-07-19\n# eval_result:   null\n# eval_env:      null\n#\n# INPUTS:\n#   - TARGET_URL: HTTP endpoint to call (default: httpbin.org/status/429 to simulate rate limit)\n#   - MAX_ATTEMPTS: max retry attempts (default: 5)\n#   - BASE_WAIT: base wait seconds (default: 1)\n#\n# OUTPUTS:\n#   - response: final successful HTTP response object\n#   - attempts_made: number of attempts before success\n#   - result: structured dict with status, attempts, elapsed_ms\n#\n# MAST FAILURE MODES ADDRESSED:\n# FM-1.3 Step Repetition      — tenacity enforces max attempt ceiling, never loops forever\n# FM-1.5 Unaware of Termination — stop_after_attempt terminates definitively\n# FM-2.4 Information Withholding — surfaces rate limit headers (Retry-After, X-RateLimit-*)\n# FM-2.6 Reasoning-Action Mismatch — correct tenacity import from registry\n# FM-3.2 No or Incomplete Verification — asserts final response is 2xx before PASS\n# FM-3.3 Incorrect Verification — exact status code check, not fuzzy\n#\n# ref: https://arxiv.org/abs/2503.13657\n# ============================================\n\nimport subprocess, sys, time, json\n\n# ── PRE_EXECUTION ─────────────────────────────────────────────\nimport urllib.request, urllib.error\n\nREGISTRY_URL = \"https://checklist.day/api/registry/tenacity\"\nfor attempt in range(2):\n    try:\n        with urllib.request.urlopen(REGISTRY_URL, timeout=10) as resp:\n            registry = json.loads(resp.read())\n        assert \"imports\" in registry, \"ABORT: registry missing imports field\"\n        assert \"warnings\" in registry, \"ABORT: registry missing warnings field\"\n        # Surface breaking warnings\n        for w in registry.get(\"warnings\", []):\n            if w.get(\"severity\") == \"breaking\":\n                print(f\"[WARNING] {w['message']}\")\n        break\n    except Exception as e:\n        if attempt == 1:\n            print(f\"ABORT: registry unreachable after 2 attempts — {e}\")\n            sys.exit(1)\n        time.sleep(1)\n\n# ── EXECUTION ──────────────────────────────────────────────────\n# Auto-install deps\nfor pkg in [\"tenacity\", \"requests\"]:\n    try:\n        __import__(pkg)\n    except ImportError:\n        subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", pkg, \"-q\"])\n\nfrom tenacity import (\n    retry,\n    stop_after_attempt,\n    wait_exponential_jitter,\n    retry_if_exception_type,\n    before_sleep_log,\n    RetryError,\n)\nimport requests\nimport logging\n\nlogging.basicConfig(level=logging.WARNING)\nlogger = logging.getLogger(__name__)\n\n# Config\nTARGET_URL = \"https://httpbin.org/status/200\"   # swap to /status/429 to test retry path\nMAX_ATTEMPTS = 5\nBASE_WAIT = 1\nattempts_made = 0\n\nclass RateLimitError(Exception):\n    pass\n\n@retry(\n    retry=retry_if_exception_type(RateLimitError),\n    stop=stop_after_attempt(MAX_ATTEMPTS),          # FM-1.3, FM-1.5 — hard ceiling\n    wait=wait_exponential_jitter(initial=BASE_WAIT, max=30),  # jitter prevents thundering herd\n    before_sleep=before_sleep_log(logger, logging.WARNING),\n    reraise=True,\n)\ndef call_with_retry(url: str) -> requests.Response:\n    global attempts_made\n    attempts_made += 1\n    resp = requests.get(url, timeout=10)\n\n    # Surface rate limit headers — FM-2.4\n    retry_after = resp.headers.get(\"Retry-After\")\n    ratelimit_remaining = resp.headers.get(\"X-RateLimit-Remaining\")\n    if retry_after:\n        print(f\"[RATE-LIMIT] Retry-After: {retry_after}s\")\n    if ratelimit_remaining is not None:\n        print(f\"[RATE-LIMIT] X-RateLimit-Remaining: {ratelimit_remaining}\")\n\n    if resp.status_code == 429:\n        raise RateLimitError(f\"Rate limited (429). Attempt {attempts_made}/{MAX_ATTEMPTS}\")\n    if resp.status_code == 503:\n        raise RateLimitError(f\"Service unavailable (503). Attempt {attempts_made}/{MAX_ATTEMPTS}\")\n\n    return resp\n\nt0 = time.time()\ntry:\n    response = call_with_retry(TARGET_URL)\nexcept RetryError as e:\n    print(f\"FAIL: exhausted {MAX_ATTEMPTS} attempts — {e}\")\n    sys.exit(1)\n\nelapsed_ms = int((time.time() - t0) * 1000)\n\n# ── POST_EXECUTION ─────────────────────────────────────────────\n# Verify final response is 2xx — FM-3.2, FM-3.3\nassert response.status_code in range(200, 300), (\n    f\"FAIL: final response was {response.status_code}, expected 2xx\"\n)\nassert attempts_made >= 1, \"FAIL: no attempts recorded\"\nassert attempts_made <= MAX_ATTEMPTS, (\n    f\"FAIL: exceeded MAX_ATTEMPTS ({attempts_made} > {MAX_ATTEMPTS})\"\n)\n\nresult = {\n    \"status\": response.status_code,\n    \"attempts_made\": attempts_made,\n    \"elapsed_ms\": elapsed_ms,\n    \"url\": TARGET_URL,\n}\n\nprint(json.dumps(result, indent=2))\nprint(\"PASS\")"}