Gateway P2.5 — Task 2 report: OCR gpu-server compat façade
Commit: c4bfab0 — feat(p2.5): gpu-server compat façade — 12-path routing, gpu-server-shaped poll, wiring
Branch: feat/gateway-p2.5-gpuserver-facade (parent c310926, Task 1)
What shipped
internal/jobs/facade.go— theFacade(Store, Reg, Cfg, Pools, Stats, TokenTenants, UpstreamID, Upstream) + the 12-path → op-label table.internal/server/server.go—EnableGPUServerFacade, sharedstartManager+jobGuardhelpers,EnableJobsrefactored to cooperate.cmd/gateway/main.go— gate on agpuserver-jobupstream (façade takes precedence over the generic Job API).internal/config/config.go—gpuserver-jobadded to the ocr-http/classify adapter validation set.internal/server/facade_test.go— full end-to-end suite through the real mux + stub gpu-server.
Route table (path → op-label, op-label = audit model dimension)
| POST path | op-label |
|---|---|
/jobs |
jobs-generic |
/ktp/cleanup |
ktp-cleanup |
/override-scrutiny |
override-scrutiny |
/perbaikan/advisory |
perbaikan-advisory |
/perbaikan/ai-summary/extract |
perbaikan-ai-summary |
/perbaikan/surat-pernyataan/extract |
perbaikan-surat-pernyataan |
/pendirian-pp/surat-pernyataan/extract |
pendirian-pp-surat-pernyataan |
/bukti-setor/extract |
bukti-setor |
/domisili/extract |
domisili |
/jual-beli/extract |
jual-beli |
/contact-info/extract |
contact-info |
/berita-acara-rups/extract |
berita-acara-rups |
Plus GET /jobs/{id} (gpu-server-shaped poll) and GET /health (mirrors healthz).
POST /jobs collision — how resolved
Both the generic P2 API and the façade want POST /jobs. Resolution: when the façade is
enabled, its jobs-generic handler owns POST /jobs and its gpu-server-shaped handler owns
GET /jobs/{id}. main.go calls either EnableGPUServerFacade (a gpuserver-job upstream
is present) or EnableJobs (generic-only), never both for the shared routes. A
jobRoutesMounted flag on the server is defense-in-depth so a double call no-ops instead of
panicking the net/http.ServeMux on a duplicate method+path. The 11 op-specific paths never
collide with anything.
Shared manager + store
startManager(store, mcfg) starts the job manager + gateway_jobs_total metric exactly once,
guarded by s.jobMgr != nil. Both EnableJobs and EnableGPUServerFacade call it, so a config
carrying both generic job upstreams and the gpu-server upstream runs a single manager over
one store. The op-labels double as the gpu-server upstream's registered models, so the
manager's existing reg.Resolve(job.Model) lands on that single upstream with no manager
change — adapterFor already routes gpuserver-job to the Task-1 GPUServerJobAdapter, which
forwards job.SubmitPath verbatim.
Poll response shape (gpu-server JobStatusResponse)
Top-level {job_id, status} plus, by state:
- queued → queue_position, eta_ms
- processing → stage, progress (job's 0.0–1.0 → int 0–100, rounded/clamped)
- completed → result (the job's raw result JSON, verbatim)
- failed → error (the job's error_code; the store keeps no Detail, so the code is the string — acceptable per plan)
- unknown / cross-tenant → HTTP 404 {"code":"JOB_UNKNOWN"} (engine keys off the 404 status)
Per-op audit
Submit sets Job.Operation="ocr" and Job.Model=opLabel. The manager's terminal audit event
carries Operation="ocr" + Model=<op-label>, giving per-operation attribution while all 12 ops
share the one gpu-server upstream id. Verified in the table test by tapping the server's emit.
Build / vet / test
go build ./...— OKgo vet ./...— OKgo test -race ./internal/jobs/ ./internal/server/— ok (92 PASS lines incl. subtests, race-clean)go test ./...— all 8 packages ok
Table test: TestFacadeAllPaths (subtests per path) — 202 {job_id} → verbatim path forward →
gpu-server-shaped completed result → audit Operation=ocr + Model=opLabel; asserts one submit
per path. Companions: TestFacadeIdempotentResubmit (same key → same job_id, exactly one
gpu-server submit), TestFacadePollUnknown (404), TestFacadeFailedJob (failed + error),
TestFacadeMissingIdempotencyKey (400), TestFacadeDrainReturns503 (503 + Retry-After).
Dormancy proof
TestFacadeDormant: a config with no gpuserver-job upstream leaves the façade routes
unmounted — POST /ktp/cleanup → 404 from the mux — while the generic P2 POST /jobs?model= API
still works. EnableGPUServerFacade returns early (no upstream found) so nothing is registered;
byte-identical to P2.
Concerns / follow-ups
- When the façade is enabled, the generic model-routed
POST /jobs?model=and the fan-out
envelope are shadowed by the façade'sjobs-generichandler (accepted per the plan). A
deployment needing both the OCR compat surface and a CONVENTIONS-native generic Job API on the
same gateway would need a distinct path for the latter — out of scope for the OCR flip. - The failed-poll
errorstring is theerror_code(e.g.GPUSERVER_FAILED), not the
gpu-server's originalerrordetail (the store persists onlyerror_code). Surfacing the
adapter'sDetailwould require a store field — deferred. - Task 3 (example config with the
gpu-serverupstream, README section, integration prompt note,
live-fire checklist) is not part of this task.