Task 5 report — AzureDIAdapter (Azure DI async-poll adapter)
Status: DONE. Commit 57e973d — feat(p2): Azure DI async-poll adapter.
Branch feat/gateway-p2-jobs-ocr (parent HEAD was 7465a56).
Files
internal/jobs/adapter_azuredi.go(new) —AzureDIAdapter, implementsAdapter.internal/jobs/adapter_azuredi_test.go(new) — httptest stub DI server tests.
Both additive; no existing files touched. SyncHTTPAdapter and the Adapter
contract are unchanged.
DI protocol flow (as implemented)
- Submit:
POST endpoint + SubmitPathwith the raw document payload.
The DI model id (job.Model) is substituted for the{model}placeholder
inSubmitPath(e.g./di/documentModels/{model}:analyze→
.../t5-layout:analyze). Content-Type fromjob.ContentType, defaulting to
application/json(reuses the sync adapter'sdefaultSubmitContentType). - Expect
202(any 2xx accepted) with anOperation-Locationresponse
header = the poll URL. Missing header → non-retryable
AZURE_DI_NO_OPERATION_LOCATION. - Poll:
GETthe Operation-Location URL until the JSONstatusis
terminal.succeeded→ return the whole poll body (containsanalyzeResult)
asjson.RawMessage.failed→ non-retryableAZURE_DI_FAILED.
running/notStarted/empty → keep polling. Unknown status →
AZURE_DI_UNKNOWN_STATUS.
Confirmed against the OCR repo (ahu-ocr-akta-notaris/backend/src/ocr/azure-on-prem-*.ts
and PRDs): submit → Operation-Location → poll GET until succeeded, ~1s
cadence. The Operation-Location URL is polled verbatim.
Key header
Injected server-side on both submit and poll from Upstream.APIKeyEnv:
header Ocp-Apim-Subscription-Key (NOT Authorization: Bearer), matching
the on-prem Azure DI container the OCR repo talks to. Mirrors the sync adapter's
env-read pattern; a client-provided key is never forwarded. When APIKeyEnv is
unset/empty, no key (and no auth) header is sent. Note: on-prem DI accepts an
empty key, so unset is safe.
Poll / timeout / progress handling
- Interval:
PollIntervalfield, default1s, capped at5sso a mis-set
config can't starve the processing timeout. - Timeout: the manager's processing timeout arrives via
ctx. Checked
before each attempt, onDoerror (ctx.Err()!=nil), and via a
select { <-ctx.Done() ; <-timer.C }on the inter-poll wait — all three paths
return non-retryableAZURE_DI_POLL_TIMEOUT. The timer isStop()ped on every
exit, so there is no goroutine/timer leak. - Progress:
report("processing", 0)fires first; then between polls
report("processing", p)withp = percentCompleted/100when DI supplies it,
else a monotonic heartbeat incrementing by 0.1 capped at 0.9. - Retry mapping:
429/503on submit or poll →AdapterError{Retryable:true}
carrying the upstreamRetry-Afterverbatim; other non-2xx → non-retryable
withAZURE_DI_SUBMIT_<n>/AZURE_DI_POLL_<n>.
Bounded reads
readCapped(rc, maxBytes) wraps io.ReadAll(io.LimitReader(rc, maxBytes)) and
closes the body — used on submit (drained/discarded) and every poll. Cap is the
adapter's MaxBytes (default 32MB, matching the config default ceiling) so a
runaway analyzeResult can't exhaust memory (the unbounded-io.ReadAll concern
flagged in Task 4). Response bodies are closed on every path.
Tests (RED → GREEN)
RED first: go test -run AzureDI failed to compile (undefined: AzureDIAdapter).
After implementing, all green. Cases (namespace-safe, t5-* tenants/ids):
- submit → 202 + Operation-Location → running×2 → succeeded: returns the
body, {model} substituted in submit path, POST submit + GET poll verified,
a processing progress report > 0 observed.
- poll failed → non-retryable AZURE_DI_FAILED.
- poll never terminates + ctx deadline → prompt AZURE_DI_POLL_TIMEOUT, returns
well within 2s (no leak).
- submit 429 + Retry-After → retryable carrying 23, status 429.
- APIKeyEnv set → stub sees Ocp-Apim-Subscription-Key on submit AND poll;
unset → key + Authorization absent.
- missing Operation-Location → non-retryable OPERATION_LOCATION code.
- poll 503 + Retry-After → retryable carrying the value.
Verification
go build ./...— OKgo vet ./...— OKgo test -race ./internal/jobs/...— ok (1.5s)
Concerns / notes for the manager (Task 7)
- On-prem poll-URL rewriting NOT done here. The OCR repo notes the on-prem DI
server behind nginx can returnOperation-Locationwith a wrong path prefix
(e.g.http://host/formrecognizer/...) and rewrites it. This adapter polls the
header verbatim (correct for standard DI). If the deployed on-prem endpoint
exhibits that quirk, rewriting should be handled by the deployment/proxy or a
future config hook — flagging so it isn't a surprise in live-fire. - Model substitution uses
job.Modelinto a{model}placeholder in
SubmitPath; if the config puts the model id directly inSubmitPath(no
placeholder), the path is used as-is. PollInterval/MaxBytesare exported fields the manager can set; both have
sane defaults so zero-value construction works.