fix(upload): refresh Entra token per request + bounded retry for large backfills (#338)#72
Conversation
…ures Fixes microsoft-amplifier/amplifier-support#338. The batch uploader captured the Entra bearer token once and baked it into the httpx.Client, so it was never refreshed (HTTP 401 past the ~67min token lifetime), and aborted the whole run on the first transient HTTP error. This fetches the auth header per request (exercising EntraTokenAuth's existing cache-and-refresh) and wraps each POST in bounded exponential backoff (transient = 5xx/429/conn-reset/timeout; permanent = other 4xx, 3xx, DNS/TLS = fail fast). Adds --timeout and --max-retries CLI flags and cause-classification of ConnectError. auth.py untouched; idempotent uploads make retries safe. Verified: 233 module tests + 753 top-level tests pass, ruff+pyright clean, and a live upload against the team-shared Entra/APIM server (run twice, idempotent). 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Additional evidence — large-session live upload (long-running backfill)Following the smaller live upload in the PR body, I ran the fixed CLI against the Target: session Command: modified Run: ~30 minutes of continuous uploading (spans well past a token-lifetime window, The 11,110-event root uploaded to completion and the run advanced into its session What each layer of evidence covers:
The live network was healthy (0 transient blips), so the retry path was not naturally |
Summary
Fixes microsoft-amplifier/amplifier-support#338. The batch uploader
(
context-intelligence-upload) was not robust for large Entra/APIM-protectedbackfills for two independent reasons: (1) the Entra bearer token was captured once
and baked into the
httpx.Client, so it was never refreshed — every POST past the~67 min token lifetime returned 401 and the run aborted; and (2) the POST loop
returned failure on the first
httpx.HTTPErroror any non-2xx, so a singletransient blip aborted an entire multi-thousand-event run. Uploads are idempotent
(SHA-256 key + Neo4j
MERGE), so this is purely a robustness/operability fix.The fix fetches the auth header per request (so
EntraTokenAuth's existingcache-and-refresh actually fires mid-run) and wraps each POST in a bounded
exponential backoff. Transient = 5xx / 429 / connection-reset / timeout;
permanent = 4xx other than 429, 3xx, and DNS/TLS transport failures — fail fast,
loud. Two small operability follow-ups are included:
--timeoutto tune theread/write timeout, and cause-classification of
ConnectErrorso DNS/TLS faultsfail fast instead of burning the retry budget on a dead host.
Scope / guardrails
modules/tool-context-intelligence-upload(
uploader.py+cli.py) plus its tests.context_intelligence/auth.pyisuntouched — the fix simply exercises its existing refresh path.
fanout.py/_DestinationDispatcher/logging_handler) is untouched. The uploader deliberately adapts (does notimport) the hook's
_classify_http_outcome: the uploader must treat 401 asPERMANENT (per-request refresh owns token expiry), whereas the hook treats it
as transient. Copying it verbatim would silently reintroduce the bug.
ApiKeyAuth.headers()is constant, so per-requestfetch is identical to the old baked-once behavior.
identically except that transient failures now retry instead of aborting.
Verification
modules/tool-context-intelligence-upload: 233 passedtests/: 753 passedruff check+ruff format --checkclean (19 files already formatted)pyrightclean (0 errors)change in one module;
bundle.dot/bundle.pngunaffected,BUNDLE_DOT_STALEnot triggered).
Real evidence on seams (not mock-only)
The client↔server boundary is the seam this change touches. Proven with a LIVE
run of the modified CLI against the team-shared Entra/APIM Context Intelligence
server (
--auth-mode entra, realaz-backed token,--max-retries 3), uploading areal session — run twice to prove idempotent re-run (no duplicates):
Both bug paths are additionally proven by deterministic unit tests: mid-run token
rollover (a near-expiry credential re-fetched per POST → later POST carries the new
bearer), transient-retry-then-succeed, persistent-transient fails after exactly
max_retries+1, permanent 4xx / 401 / DNS / TLS fail fast with zero retries, andheaders()raising fails loud instead of crashing the run.Docs & diagrams
bundle.dot/bundle.png— N/A, bundle structure unchangedadditive optional CLI flags with safe defaults:
--max-retries,--timeout)Notes / follow-ups
Retry-After(the numeric-seconds formis honored today); server-side hardening of the lone-child placeholder-parent path
(the "minor" item in #338, server-side, out of scope for this client change).