Retries
Retry writes safely with IDs you choose before the first attempt, and find out whether a commit happened.
Networks drop responses, so a write can succeed even when your client sees a timeout. To make retrying safe, every write in Spiral takes an ID that you choose before the first attempt, such as a transaction ID or an idempotency key. If you send the same request with the same ID again, you get the original result instead of a second write. If you send a different request with that ID, the service rejects it.
Automatic request retries
The Rust, Python, TypeScript and Go SDKs retry replayable GET, HEAD and
PUT requests after a transient connection failure or HTTP 408, 429,
500, 502, 503 or 504. They use bounded backoff, with at most ten
retries within the request's deadline. Cancellation stops retry backoff as
well as the current attempt.
A supplied HTTP client or fetch function receives the same policy. Ordinary
POST, PATCH and DELETE requests and streamed uploads are sent once by
the transport. Use the operation's recovery API and the same request IDs
when a write's outcome is uncertain. Table reads and other operations that
already recover progress keep their own retry policy.
Each wire attempt carries trace context and emits a client span when your application configures OpenTelemetry. Retry diagnostics omit credentials, query strings and response bodies.
Pick an ID before the first attempt
| Operation | ID that you choose | Result of a retry |
|---|---|---|
| Publish a Catalog commit | transactionId (UUIDv7) and idempotencyKey | The saved publication |
| Create, rename, or delete a branch, or create a tag | idempotencyKey | The saved result |
| Publish a Catalog operation | operation.operationId | The saved operation result |
| Create a Location version | versionId (UUIDv7) | The existing version, if the content matches |
| Register a Table version | rootObjectId (UUIDv7) of the uploaded snapshot | The registered version |
| Create a Task | idempotencyKey | The saved Task |
| Run a task trigger now | requestId (UUID) | The saved invocation |
An idempotencyKey is an opaque string of 1 to 128 bytes. The Catalog and
the Task service scope it to the Project, and the Catalog also scopes it to
the caller. Generate a UUIDv7 for each new ID: it sorts by creation time, and
the Catalog requires that form for transaction IDs.
Reusing an ID for something else
An ID stands for one intent. If you reuse it for a different request, the
service rejects the request with status 409:
| Service | Error |
|---|---|
| Catalog | Catalog:IdempotencyKeyReused, or Catalog:OperationInputMismatch for an operation ID |
| Locations | A conflict kind body |
| Tasks | A conflict kind body |
Create a new ID for each new intent, and keep the old one for retries of the old intent.
Finding out whether a commit happened
A Catalog commit can succeed after the connection fails. To learn the outcome, recover it by its transaction ID:
curl -X POST "https://api.spiraldb.dev/api/catalog/projects/$PROJECT_ID/publications/recover" \
-H "Authorization: Bearer $SPIRAL_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"transactionId\": \"$TRANSACTION_ID\"}"The response is either published, with the commit ID and its parents, or
notPublished. notPublished means the commit had not happened when you
asked; a request that is still in flight can complete afterwards. So do not
treat it as a failure. To finish the commit, send it again with the same
transactionId and idempotencyKey; if the first attempt did land, you get
its result back.
For a Catalog operation, read its saved result with
GET /api/catalog/projects/{project_id}/operations/{operation_id}. A result
exists only once the operation has completed, and it always keeps the first
saved result.
Writes through the data plane
A write through the data plane has two steps after the data is uploaded: the new Table version is registered, and then it is committed. If the outcome of either step is unknown, retry that step with the same IDs. Do not upload the data again; it is already in storage and immutable. The Rust SDK follows these rules for you, and lets you save a commit request and resume it after a restart.
Conflicts are not retries
A conflict is a different outcome from a lost response. When a Catalog commit
returns a conflict, another commit changed the branch first, and the
service does not rebase your change for you. Read the new state, decide what
to write, and commit with a new transaction ID and a new idempotency key. See
Commits and Branches.