think
16px
820px

Gateway P2.5 — Task 2 report: OCR gpu-server compat façade

Commit: c4bfab0feat(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 — the Facade (Store, Reg, Cfg, Pools, Stats, TokenTenants, UpstreamID, Upstream) + the 12-path → op-label table.
  • internal/server/server.goEnableGPUServerFacade, shared startManager + jobGuard helpers, EnableJobs refactored to cooperate.
  • cmd/gateway/main.go — gate on a gpuserver-job upstream (façade takes precedence over the generic Job API).
  • internal/config/config.gogpuserver-job added 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:
- queuedqueue_position, eta_ms
- processingstage, progress (job's 0.0–1.0 → int 0–100, rounded/clamped)
- completedresult (the job's raw result JSON, verbatim)
- failederror (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 ./... — OK
  • go vet ./... — OK
  • go 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's jobs-generic handler (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 error string is the error_code (e.g. GPUSERVER_FAILED), not the
    gpu-server's original error detail (the store persists only error_code). Surfacing the
    adapter's Detail would require a store field — deferred.
  • Task 3 (example config with the gpu-server upstream, README section, integration prompt note,
    live-fire checklist) is not part of this task.