Task 5 review — AzureDIAdapter (Azure DI async submit/poll adapter)
Commit reviewed: 57e973d (base 7465a56). Files: internal/jobs/adapter_azuredi.go,
internal/jobs/adapter_azuredi_test.go. Verified by reading source directly (not just the
diff) and running go build ./..., go vet ./..., go test -race ./internal/jobs/... —
all green; the 8 claimed tests pass under -race.
Spec Compliance — ✅
All Task 5 bullets are met: submit to ep+SubmitPath with {model} substitution, terminal
status handling (succeeded/failed), bounded poll honoring ctx, JobResult-compatible
return, server-side key injection, and the 5 required test scenarios (running×2→succeeded
with progress, failed, poll timeout — plus extra 429/503/missing-header/key-injection cases
beyond the minimum ask).
Strengths
- Exact
Adapterinterface conformance.Runsignature matchesinternal/jobs/adapter.go
verbatim;var _ Adapter = (*AzureDIAdapter)(nil)compile-time check present.AdapterError
conventions (Retryable/Code/Status/RetryAfter) are used identically to
SyncHTTPAdapter— a manager written against Task 4 drives this adapter with no special-casing. - Protocol correctness. Operation-Location is read from the response header
(resp.Header.Get("Operation-Location"), adapter_azuredi.go:182), not the body.
succeeded/failedare terminal;running/notstarted/empty continue; anything else hits
AZURE_DI_UNKNOWN_STATUS(line 247) — no infinite loop, no nil-deref (PercentCompletedis
nil-checked before dereference at adapter_azuredi.go:253-254). - ctx handling is correct and leak-free. ctx is checked before each attempt (line 212),
after a failedDo()(line 224), and viaselect{<-ctx.Done(); <-timer.C}on the inter-poll
wait (lines 282-287) withtimer.Stop()on the cancel branch. Critically, the timer is only
constructed at the bottom of the loop body, after all terminal/error returns for that
iteration — so none of the early-return paths (succeeded, failed, unknown status, bad JSON,
429/503, transport error) ever leave an unstopped timer.TestAzureDIAdapter_PollTimeout
exercises this concurrently and returns well inside its 2s guard. - Poll interval is bounded, no busy-spin risk.
pollInterval()defaults to 1s and clamps
to 5s max regardless of config (lines 80-89); there's no path where a 0/unset value produces
a tight loop. - Key injection is correct and isolated.
injectKey()setsOcp-Apim-Subscription-Key
(notAuthorization) fromos.Getenv(Upstream.APIKeyEnv)on both submit (line 168) and
poll (line 220); absent whenAPIKeyEnvis unset or the env var is empty; no client-supplied
header is ever read or forwarded. Confirmed byTestAzureDIAdapter_APIKeyInjectedand
TestAzureDIAdapter_NoKeyWhenUnset(the latter also assertsAuthorizationis empty). - Bounded reads + close on every path.
readCappedwrapsio.LimitReader(rc, maxBytes)+
rc.Close()and is used for submit (line 178, drained/discarded before the status switch) and
poll (line 231, before the status switch) — every response, on every status branch, gets its
body capped and closed, matching the Task 4 fix. - Tests are namespace-safe (
t5-*tenants/ids), use per-testhttptest.Serverinstances (no
shared global state to race on), and assert on filtered/local state, not bare counts — meets
the P0 test rule.
Issues
Minor:
- Read errors are still silently discarded (
readCapped, adapter_azuredi.go:137-140:
body, _ := io.ReadAll(...)), repeating the Task-4 pattern the report says it addresses.
Functionally this is safer here than in Task 4: a truncated/partial body almost always
failsjson.Unmarshaland maps toAZURE_DI_BAD_RESPONSErather than a "fake success" (the
Task-4 risk), and on submit the body is discarded anyway. However, there's a real (if small)
downside: a transient network blip mid-read of arunning/succeededpoll response now
surfaces as non-retryableAZURE_DI_BAD_RESPONSE, permanently failing the job, when the
more correct classification for a mid-poll transport hiccup would arguably be retryable (the
loop is already built to keep polling). Not a correctness bug — no leak, no fake success —
but worth a one-line fix (check the read error explicitly and return a retryable code, or at
least log it) before this sees heavy live traffic. PollPath(config.Upstream) is unused. The plan text says "pollPollPath/that URL";
this adapter only ever polls the dynamicOperation-Locationheader value and never
referencesUpstream.PollPath. That's defensible — DI's protocol returns a
per-operation URL, so a static configured path doesn't apply the way it might for a
different async protocol — but it's worth an explicit note in the plan/config docs that
PollPathis presently a no-op for theazure-diadapter, so a future config author doesn't
assume setting it changes behavior.- Default
submitPath()for an unsetSubmitPathis/(adapter_azuredi.go:106-108), whereas
SyncHTTPAdapterdefaults to/ocr. Inconsequential (config always setsSubmitPathfor a
real upstream) but a minor cross-adapter inconsistency if someone goes looking for a pattern.
No Critical or Important issues found.
Cross-task note — Operation-Location rewrite for Task 7/9 live-fire
Confirmed: the adapter polls the Operation-Location header value verbatim — no scheme/host/path
rewriting of any kind (opLoc flows straight from resp.Header.Get(...) into
http.NewRequestWithContext(ctx, http.MethodGet, opLoc, nil), adapter_azuredi.go:182-186 and
216). If the on-prem nginx-fronted DI container returns a mangled prefix, the failure mode is
clean rather than silent: the poll GET will most likely 404/5xx against the wrong path, which
maps deterministically to AZURE_DI_POLL_<status> (non-retryable for 404, retryable for
429/503) rather than hanging or looping forever — so this is not a correctness bug in Task 5,
and deferring is acceptable for this task.
Recommendation for Task 7/9: don't leave this to "deployment/config" as a vague TODO — before
the Task 9 live-fire against the real on-prem box, either (a) confirm empirically that the
on-prem instance's Operation-Location is well-formed end-to-end (no rewrite needed), or (b)
add a small, explicit normalization hook — e.g. when the Operation-Location host/scheme differs
from the resolved upstream endpoint's host/scheme, rewrite host+scheme to the endpoint's while
keeping DI's path+query verbatim (a bounded, safe default that doesn't require a new config
field). This should be a named checklist item in Task 9's live-fire step, not something
discovered mid-drill.
Assessment — Task quality: Approved
Correct, interface-conformant, leak-free implementation with thorough test coverage (8 tests,
race-clean, exceeding the plan's 3 required scenarios). The two Minor issues (read-error
classification, unused PollPath) are worth a follow-up note but do not block merging Task 5;
neither is a correctness or safety defect. The nginx Operation-Location concern is correctly
identified and safely deferred, but should be explicitly tracked into Task 9's live-fire plan
rather than left implicit.