Full-sync HTTP control API
The handler imports no queue or storage implementations. Inject the five
contract interfaces in Dependencies: RunLister, RunReader,
FailurePager, ReplayRequester, and ActionReader. The replay service owns durable
idempotency and execution; the HTTP layer does not cache requests or launch work.
Use NewRunReader(live, archive, rounds) to compose LiveRunReader,
ArchivedRunReader, and RoundStatsReader into a RunReader. Live metadata
wins and returns source: "live"; only ErrNotFound from live metadata permits
archive fallback, returning source: "archive". Live storage errors and round
lookup errors propagate. Archive and round readers may be nil to disable those
lookups. A failure listing always calls FailurePager directly, never archives.
Routes and responses
All routes require Authorization: Bearer <configured token>. Missing, wrong,
or ambiguous credentials return 401 with a WWW-Authenticate challenge. An
empty configured token is a startup configuration error. Responses use JSON and
Cache-Control: no-store.
| Method | Route | Success body |
|---|---|---|
| GET | /internal/v1/full-sync-runs?syncKind=&limit= |
200 contract.RunList |
| GET | /internal/v1/full-sync-runs/{runId} |
200 contract.RunView |
| GET | /internal/v1/full-sync-runs/{runId}/failures?cursor=&limit= |
200 contract.FailurePage |
| POST | /internal/v1/full-sync-runs/{runId}/replay |
202 contract.ReplayAccepted |
| GET | /internal/v1/full-sync-actions/{actionId} |
200 contract.ReplayAction |
Run list results merge active Redis metadata with flat run-<runId>.json
archives under the configured archive root. Active Redis metadata wins when an
ID exists in both sources. syncKind accepts traffic_usage or sim_status;
limit defaults to 50 and is capped at 100.
Failure limits use the contract rules: absent, empty, or zero selects 100;
positive values up to 500 are accepted; other values are rejected. Repeated
cursor/limit parameters and malformed query encoding are rejected. Cursors are
opaque and passed unchanged after URL decoding. The pager validates cursor
contents and run binding. Empty pages serialize items: [] and preserve
nextCursor; only an empty next cursor signals completion.
Replay requires a single Idempotency-Key header and a JSON body such as
{"requestedBy":"operator"}. The body is limited to 64 KiB, must contain one
JSON object with no unknown fields, and must satisfy ReplayRequest.Validate.
If supplied, Content-Type must be application/json (parameters are allowed).
The exact requestedBy value is preserved for fingerprinting. Repeated accepted
requests return the service's existing action/target and current status with
202. The adapter constructs statusUrl and the Location header from actionId.
All application errors use this envelope, without storage error details:
{"error":{"code":"invalid_argument","message":"Invalid request"}}
| Condition | HTTP status | Error code |
|---|---|---|
Invalid path, query, header, body, or ErrInvalidArgument |
400 | invalid_argument |
| Missing/wrong bearer credentials | 401 | unauthorized |
Unknown route/resource or ErrNotFound |
404 | not_found |
| Unsupported route method | 405 | method_not_allowed |
ErrConflict (including changed idempotent request), ErrLeaseLost |
409 | conflict |
ErrFailureDetailsUnavailable |
410 | failure_details_unavailable |
| Oversized replay body | 413 | body_too_large |
ErrBusy |
503 | busy |
| Other service errors or unencodable service output | 500 | internal_error |
Lifecycle
NewHandler(token, deps) can be mounted by an existing HTTP server. For an
independent listener, construct NewServer(Config{Addr: address, BearerToken: token}, deps) and call Start(workerCtx). Start binds synchronously and
returns address-in-use and other startup errors directly. Addr() exposes the
bound address, including an OS-assigned port when configured with port zero.
Call Wait() after Start, normally in the worker's supervision goroutine. It
returns serving or shutdown failures; it also returns startup failures if Start
failed. The server is single-use and rejects a second Start. Wait can have
multiple callers but must not be called before Start.
Canceling workerCtx closes the listener and drains active HTTP requests.
Active request contexts stay usable during the drain. ShutdownTimeout defaults
to ten seconds; if it expires, active connections are closed and Wait returns
an error wrapping context.DeadlineExceeded. Normal cancellation with a
successful drain returns nil. No worker wiring or configuration-file changes
are part of this package.
Verification
gofmt -l internal/fullsync/httpapi
go test ./internal/fullsync/...
go test ./internal/fullsync/httpapi -cover
go vet ./internal/fullsync/...
Tests use contract mocks and local HTTP listeners, requiring no Redis or archive
files. go test -race ./internal/fullsync/... additionally requires a supported
C compiler and CGO enabled.