P2.5 Task 1 report — GPUServerJobAdapter + Job.SubmitPath
Commit: c310926 — feat(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).
- failed → AdapterError{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-retryableGPUSERVER_FAILED, Detail=="boom".TestGPUServerJobAdapter_Poll404JobLost— poll 404 →GPUSERVER_JOB_LOST.TestGPUServerJobAdapter_PollTimeout— never-terminal, ctx 100ms →GPUSERVER_POLL_TIMEOUTunder 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 ./...— OKgo vet ./...— OKgo 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-servererrorfield. - Poll uses
Authorization: BearerforAPIKeyEnv(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-jobupstream, and the poll-shape endpoint; this task is the adapter + field only.