Add HTTP upload idempotency support
This commit is contained in:
@@ -30,6 +30,14 @@ Returns `200 OK` when the server is running:
|
||||
|
||||
Accepts one source bundle archive and returns after the archive is staged and validated.
|
||||
|
||||
Producers may include:
|
||||
|
||||
```text
|
||||
Idempotency-Key: <key>
|
||||
```
|
||||
|
||||
Idempotency keys are scoped to the authenticated pipeline selected by the bearer token. Valid keys are non-empty ASCII strings up to 128 bytes using letters, digits, `.`, `_`, `-`, and `:`. Invalid keys return `400`.
|
||||
|
||||
Accepted content types:
|
||||
|
||||
- `application/x-tar`
|
||||
@@ -44,8 +52,9 @@ Successful admission returns `202 Accepted`:
|
||||
|
||||
Common error responses:
|
||||
|
||||
- `400`: pipeline query supplied, archive rejected, malformed archive, or invalid staged source bundle.
|
||||
- `400`: pipeline query supplied, invalid idempotency key, archive rejected, malformed archive, or invalid staged source bundle.
|
||||
- `401`: missing, empty, or unknown bearer token.
|
||||
- `409`: repeated idempotency key conflicts with another source manifest, or the same key is already being staged.
|
||||
- `413`: upload body exceeds the selected pipeline size limit.
|
||||
- `415`: unsupported content type.
|
||||
- `503`: upload queue is full.
|
||||
@@ -56,6 +65,14 @@ Error bodies use:
|
||||
{"error":"<message>"}
|
||||
```
|
||||
|
||||
Retryable idempotency conflicts include:
|
||||
|
||||
```json
|
||||
{"error":"upload idempotency key is already being processed","retryable":true}
|
||||
```
|
||||
|
||||
When `Idempotency-Key` is omitted, upload admission preserves the raw HTTP behavior: every valid accepted upload receives its own run id. When a key is supplied, the server records the accepted run after archive staging and source bundle validation succeed. Reusing the same key for the same authenticated pipeline and the same normalized source manifest returns the original `202 Accepted` response and does not enqueue another run. Reusing the same key for a different normalized source manifest returns `409 Conflict`.
|
||||
|
||||
### `GET /runs/<run-id>`
|
||||
|
||||
Returns an in-memory status record while retained:
|
||||
@@ -95,6 +112,8 @@ The uploaded archive size and extracted bundle size are bounded by the selected
|
||||
|
||||
Completed status records expire after `server.http.retention`; expiration removes committed staged bundle directories for completed uploads. Server restart clears queue state and status records.
|
||||
|
||||
Idempotency records are memory-only, expire with completed upload status records, and are cleared by server restart.
|
||||
|
||||
## Boundaries
|
||||
|
||||
The HTTP API does not expose pipeline selection by request parameter, TLS, public routing policy, or durable status storage. Put public access controls, TLS termination, and rate limiting in deployment infrastructure.
|
||||
|
||||
@@ -36,17 +36,19 @@ Run workflows discover and validate source bundles through `internal/bundle`. De
|
||||
|
||||
HTTP uploads stage and validate archives before enqueueing a pipeline run with a local staged source root.
|
||||
|
||||
Upload idempotency is owned by the upload coordinator. Optional `Idempotency-Key` values are scoped to the authenticated pipeline. The coordinator reserves a key while staging is in progress, records the accepted run id with the validated source manifest identity after staging succeeds, returns the original accepted record for the same key and same manifest, and rejects the same key with a different manifest as a conflict.
|
||||
|
||||
## Skip And Resume Behavior
|
||||
|
||||
Fan-out destinations are independent. A destination failure is recorded and does not prevent later destinations from being attempted. Dry-run builds plans and reports without destination writes, destination state writes, notifier calls, or SSH known-host persistence.
|
||||
|
||||
HTTP upload status is in memory. Accepted jobs move through accepted, queued, running, succeeded, or failed states and expire after configured retention.
|
||||
HTTP upload status is in memory. Accepted jobs move through accepted, queued, running, succeeded, or failed states and expire after configured retention. Upload idempotency records are also memory-only, expire with the completed status record for their accepted run, and are cleared by process restart.
|
||||
|
||||
## Failure Behavior
|
||||
|
||||
Runtime setup fails for config load, config validation, secret loading, or credential resolution errors. Source setup failures stop the affected run before destination planning. Destination open, planning, execution, and notification failures are recorded as destination failures where a partial result exists.
|
||||
|
||||
HTTP upload startup fails if upload tokens are missing, empty, or duplicated. Upload requests can fail during authentication, content-type validation, queue admission, archive staging, source validation, or later publish execution.
|
||||
HTTP upload startup fails if upload tokens are missing, empty, or duplicated. Upload requests can fail during authentication, idempotency-key validation, content-type validation, idempotency conflict checks, queue admission, archive staging, source validation, or later publish execution.
|
||||
|
||||
## Tests To Inspect
|
||||
|
||||
@@ -63,4 +65,5 @@ HTTP upload startup fails if upload tokens are missing, empty, or duplicated. Up
|
||||
- Fan-out destinations remain independent after a destination-scoped failure.
|
||||
- Secret values are never printed; warnings may name variables only.
|
||||
- Upload admission stages and validates a bundle before returning a run id.
|
||||
- Idempotent upload retries compare normalized source manifest identity, not archive bytes.
|
||||
- Runtime backend registration remains app-owned.
|
||||
|
||||
@@ -138,6 +138,16 @@ curl -X POST http://127.0.0.1:8080/upload \
|
||||
--data-binary @bundle.tar.gz
|
||||
```
|
||||
|
||||
For safe producer retries, include an idempotency key that is stable for the producer operation:
|
||||
|
||||
```sh
|
||||
curl -X POST http://127.0.0.1:8080/upload \
|
||||
-H "Authorization: Bearer $DISTRIBUTOR_EXAMPLE_UPLOAD_TOKEN" \
|
||||
-H "Content-Type: application/gzip" \
|
||||
-H "Idempotency-Key: producer.run.20260604T120000Z" \
|
||||
--data-binary @bundle.tar.gz
|
||||
```
|
||||
|
||||
Accepted uploads return after the archive is staged and validated:
|
||||
|
||||
```json
|
||||
@@ -154,6 +164,8 @@ Status values are `accepted`, `queued`, `running`, `succeeded`, and `failed`. Co
|
||||
|
||||
Upload admission is bounded by `server.http.queue_size`. Publication concurrency is bounded by `server.http.max_concurrency`, and the coordinator does not run two uploads for the same pipeline at the same time.
|
||||
|
||||
`Idempotency-Key` is optional for raw HTTP clients. When present, it is scoped to the authenticated pipeline. Reusing the same key with the same normalized source manifest returns the original accepted run response and does not enqueue another run. Reusing the key with a different source manifest returns `409 Conflict`. If another request with the same key is still being staged before its manifest is known, the server returns a retryable `409 Conflict`. Idempotency records are memory-only and expire with completed upload status records.
|
||||
|
||||
The upload server accepts `application/x-tar`, `application/gzip`, and `application/x-gzip`. Archives are extracted into a temporary staging directory, must contain exactly one root-level `manifest.json`, and must validate as one complete source bundle before a run id is issued. Per-source `max_upload_size` bounds both uploaded archive size and extracted bundle size. The implementation also caps extracted file count.
|
||||
|
||||
The default bind address is private loopback. Put TLS, public routing, rate limiting, and external access policy in a reverse proxy or deployment layer.
|
||||
|
||||
@@ -451,6 +451,26 @@ Safe fix: send one valid tar or tar.gz source bundle archive with `Content-Type:
|
||||
|
||||
Reference: [Operations](operations.md#http-upload-operation).
|
||||
|
||||
## Upload Idempotency Conflict
|
||||
|
||||
Symptom: `POST /upload` returns `409`.
|
||||
|
||||
Likely cause: the request reused an `Idempotency-Key` for the same authenticated pipeline with a different source manifest, or another request with the same key is still being staged before its manifest is known.
|
||||
|
||||
Diagnostic:
|
||||
|
||||
```sh
|
||||
curl -i -X POST http://127.0.0.1:8080/upload \
|
||||
-H "Authorization: Bearer $DISTRIBUTOR_UPLOAD_TOKEN" \
|
||||
-H "Content-Type: application/gzip" \
|
||||
-H "Idempotency-Key: <key>" \
|
||||
--data-binary @bundle.tar.gz
|
||||
```
|
||||
|
||||
Safe fix: if the response includes `"retryable":true`, retry the same upload later with the same key. Otherwise, inspect the producer operation and use the same key only for the same source bundle.
|
||||
|
||||
Reference: [HTTP Upload API Contract](integrations/http-upload.md#post-upload).
|
||||
|
||||
## Upload Status Is Missing
|
||||
|
||||
Symptom: `GET /runs/<run_id>` returns `404`.
|
||||
|
||||
Reference in New Issue
Block a user