think
16px
820px

P2.5 Task 1 report — GPUServerJobAdapter + Job.SubmitPath

Commit: c310926feat(p2.5): GPUServerJobAdapter (async submit→poll gpu-server) + Job.SubmitPath
Branch: feat/gateway-p2.5-gpuserver-facade (parent 6ae7ef2)

What shipped

Job.SubmitPath (internal/jobs/job.go)

New field SubmitPath string \json:"submit_path,omitempty"`, set by the façade (Task 2), read only byGPUServerJobAdapter. Empty for generic/jobsjobs, sosync-http/azure-di/classify` adapters ignore it — no behavior change to them.

AdapterError.Detail (internal/jobs/adapter.go)

Added an optional Detail string field so a failed gpu-server job's error string can be carried out to the façade. Error() includes it when present. Additive; existing adapters set it nowhere so their errors are unchanged.

GPUServerJobAdapter (internal/jobs/adapter_gpuserver.go)

type GPUServerJobAdapter struct {
    Upstream     *config.Upstream
    Client       *http.Client   // nil → http.DefaultClient; timeout comes from ctx
    PollInterval time.Duration  // default ~1s, capped at 5s
    MaxBytes     int64          // default 32 MiB, bounds every submit/poll read
}

Implements Adapter. It reads the target path from job.SubmitPath (the field above): submit POSTs to endpoint + job.SubmitPath. If job == nil || job.SubmitPath == "" it returns AdapterError{Code:"NO_SUBMIT_PATH"} before touching the network.

Submit/poll flow (mirrors AzureDIAdapter):
1. report("processing", 0), then POST payload (Content-Type = job.ContentType, default application/json) to endpoint + job.SubmitPath.
2. Expect 2xx; parse {"job_id": "..."} from the body (bounded via readCapped). Missing/empty id → GPUSERVER_BAD_SUBMIT.
3. Poll GET endpoint + "/jobs/" + gpuJobId on the bounded interval, checking ctx.Err() before each attempt.
4. On 2xx, unmarshal the JobStatusResponse (job_id,status,progress?,stage?,result?,error?):
- completed → return result as json.RawMessage verbatim (not re-encoded/base64).
- failedAdapterError{Retryable:false, Code:"GPUSERVER_FAILED", Detail:<error string>}.
- anything else (queued/processing/other) → keep polling; report(stage, progress/100) (progress int 0–100 → 0.0–1.0; nil → 0.0; empty stage → "processing").

Error mapping:
| condition | AdapterError |
|---|---|
| empty job.SubmitPath | NO_SUBMIT_PATH (no network) |
| submit 429/503 | retryable GPUSERVER_SUBMIT_{code} + Retry-After |
| submit other non-2xx | non-retryable GPUSERVER_SUBMIT_{code} |
| submit 2xx w/o job_id | GPUSERVER_BAD_SUBMIT |
| poll 404 | GPUSERVER_JOB_LOST |
| poll 429/503 | retryable GPUSERVER_POLL_{code} + Retry-After |
| poll status failed | GPUSERVER_FAILED (Detail = gpu-server error) |
| ctx deadline | GPUSERVER_POLL_TIMEOUT |

ctx-honored (manager sets the processing timeout); the poll wait uses time.NewTimer with timer.Stop() on the ctx-done exit — no goroutine/timer leak. Server-side APIKeyEnv injection via Authorization: Bearer (the generic pattern; gpu-server usually needs none but it is supported).

manager.adapterFor (internal/jobs/manager.go)

Added case "gpuserver-job": a = &GPUServerJobAdapter{Upstream: up, Client: m.client, MaxBytes: int64(m.mcfg.BodyCapBytes)}.

Tests (internal/jobs/adapter_gpuserver_test.go, httptest stub gpu-server)

  • TestGPUServerJobAdapter_SubmitPollSucceed — 202{job_id} → processing×2 → completed{result}; asserts exact raw result, submit hit endpoint+SubmitPath (/ktp/cleanup), poll hit /jobs/gpu-job-42, 3 polls, processing progress ~0.4.
  • TestGPUServerJobAdapter_PollFailedNonRetryable — failed+error:"boom" → non-retryable GPUSERVER_FAILED, Detail=="boom".
  • TestGPUServerJobAdapter_Poll404JobLost — poll 404 → GPUSERVER_JOB_LOST.
  • TestGPUServerJobAdapter_PollTimeout — never-terminal, ctx 100ms → GPUSERVER_POLL_TIMEOUT under a 2s watchdog (no leak).
  • TestGPUServerJobAdapter_EmptySubmitPathNoNetwork — no SubmitPath → NO_SUBMIT_PATH, upstream hit 0 times.
  • TestGPUServerJobAdapter_APIKeyInjected — Bearer key injected on both submit and poll.

Results

  • go build ./... — OK
  • go vet ./... — OK
  • go test -race ./internal/jobs/ — ok (6 new GPUServer tests PASS, full package green)
  • go test ./... — all packages ok (jobs, audit, config, idem, pool, proxy, registry, server)

Concerns

  • Added AdapterError.Detail; additive and unused by other adapters, so no drift — but Task 2's façade poll handler should read it to populate the gpu-server error field.
  • Poll uses Authorization: Bearer for APIKeyEnv (matches syncPost); if the real gpu-server ever needs a different header, revisit — for now it needs none.
  • Task 2 owns the façade wiring, config gpuserver-job upstream, and the poll-shape endpoint; this task is the adapter + field only.