diff --git a/api/common.yaml b/api/common.yaml index f1aea51..7b373d9 100644 --- a/api/common.yaml +++ b/api/common.yaml @@ -51,14 +51,10 @@ parameters: name: cursor in: query description: | - Opaque cursor from the previous page's next_cursor, bound to the running - instance, the endpoint and resource it pages, the retained snapshot or - record, the filters and limit. A cursor the server no longer recognises - (snapshot expired or evicted, record left the ring, process restarted, - never issued, or issued for another endpoint or resource) returns - 410 snapshot_expired; discard it and restart the walk without a cursor. A recognised cursor sent - with different filters or a different limit returns 400 invalid_request. - The server never continues a walk against a different snapshot. + Opaque cursor from the previous page's next_cursor, bound to the walk that + issued it (its snapshot or retained records, filters and limit). An unrecognised + cursor returns 410 snapshot_expired; see + [Page cursors](/v0.1.0/en/docs/errors.html#Page-cursors). schema: type: string minLength: 1 @@ -113,7 +109,7 @@ parameters: IfMatchOptional: name: If-Match in: header - description: Evaluated as RFC 9110 §13.1.1 defines; see Conditional requests in the error contract. A new group-configuration PATCH without If-Match returns 428 precondition_required; a retained idempotent replay may omit it. + description: Evaluated as RFC 9110 §13.1.1 defines; see Conditional requests in the error contract. For when a retained idempotent replay may omit it, see [Choosing the status](/v0.1.0/en/docs/errors.html#Choosing-the-status). schema: $ref: ./openapi.yaml#/components/schemas/EntityTagList example: '"17"' diff --git a/api/config.yaml b/api/config.yaml index 8a7c22d..f552282 100644 --- a/api/config.yaml +++ b/api/config.yaml @@ -522,9 +522,7 @@ paths: The source's content_sha256, from GET /config or the ETag of GET /config/sources/{source_id}, enclosed in double quotes. The server compares it with the source's current bytes in the configuration store, - not the snapshot revision, as RFC 9110 §13.1.1 defines: `*` matches the - existing source, a list matches when any strong tag in it matches, and a - weak tag never matches. + not the snapshot revision, as [Conditional requests](/v0.1.0/en/docs/errors.html#Conditional-requests) defines. - $ref: ./openapi.yaml#/components/parameters/IdempotencyKey requestBody: required: true @@ -622,7 +620,7 @@ paths: "409": $ref: ./openapi.yaml#/components/responses/Conflict "412": - description: If-Match does not match the stored content hash; nothing is stored + description: If-Match does not match the stored content hash, on arrival or at commit; the replacement is not stored headers: Cache-Control: $ref: ./openapi.yaml#/components/headers/NoStore diff --git a/api/events.yaml b/api/events.yaml index 1e1f41a..c131917 100644 --- a/api/events.yaml +++ b/api/events.yaml @@ -5,10 +5,8 @@ paths: tags: [ Events ] summary: Follow bounded resumable invalidation events description: | - Send stream.ready first on every connection, including a valid resume. - Its cursor must not skip pending replay; retain the supplied Last-Event-ID - until replay advances it. Filtered-out IDs may leave gaps, so clients must - not infer loss by subtracting IDs. + Every connection begins with stream.ready; a resumed connection then replays + retained events after Last-Event-ID; see [Replay and recovery](/v0.1.0/en/docs/events.html#Replay-and-recovery). x-permission: observe security: - bearerAuth: [] diff --git a/api/nodes-groups.yaml b/api/nodes-groups.yaml index 873d9e5..9ca2227 100644 --- a/api/nodes-groups.yaml +++ b/api/nodes-groups.yaml @@ -642,8 +642,8 @@ paths: A change to a field absent from mutable_config, such as interrupt_connections on an engine without that option, returns 422 unsupported_value. Requires resources.groups.config_patch. If-Match carries the - configuration-wide revision from the ETag of GET, so an accepted change - anywhere in the configuration makes an older revision fail with 412. + configuration-wide revision from the ETag of GET; see + [Groups](/v0.1.0/en/docs/groups.html) for 412 and 409. x-permission: control security: - bearerAuth: [] @@ -731,7 +731,7 @@ paths: "409": $ref: ./openapi.yaml#/components/responses/Conflict "412": - description: If-Match does not equal the current configuration revision + description: If-Match does not match the current configuration revision, on arrival or at commit headers: Cache-Control: $ref: ./openapi.yaml#/components/headers/NoStore diff --git a/source/openapi.yaml b/source/openapi.yaml index 457e737..6225e02 100644 --- a/source/openapi.yaml +++ b/source/openapi.yaml @@ -1407,9 +1407,7 @@ paths: The source's content_sha256, from GET /config or the ETag of GET /config/sources/{source_id}, enclosed in double quotes. The server compares it with the source's current bytes in the configuration store, - not the snapshot revision, as RFC 9110 §13.1.1 defines: `*` matches the - existing source, a list matches when any strong tag in it matches, and a - weak tag never matches. + not the snapshot revision, as [Conditional requests](/v0.1.0/en/docs/errors.html#Conditional-requests) defines. - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true @@ -1514,7 +1512,7 @@ paths: '409': $ref: '#/components/responses/Conflict' '412': - description: If-Match does not match the stored content hash; nothing is stored + description: If-Match does not match the stored content hash, on arrival or at commit; the replacement is not stored headers: Cache-Control: $ref: '#/components/headers/NoStore' @@ -3596,8 +3594,8 @@ paths: A change to a field absent from mutable_config, such as interrupt_connections on an engine without that option, returns 422 unsupported_value. Requires resources.groups.config_patch. If-Match carries the - configuration-wide revision from the ETag of GET, so an accepted change - anywhere in the configuration makes an older revision fail with 412. + configuration-wide revision from the ETag of GET; see + [Groups](/v0.1.0/en/docs/groups.html) for 412 and 409. x-permission: control security: - bearerAuth: [] @@ -3685,7 +3683,7 @@ paths: '409': $ref: '#/components/responses/Conflict' '412': - description: If-Match does not equal the current configuration revision + description: If-Match does not match the current configuration revision, on arrival or at commit headers: Cache-Control: $ref: '#/components/headers/NoStore' @@ -5169,10 +5167,8 @@ paths: - Events summary: Follow bounded resumable invalidation events description: | - Send stream.ready first on every connection, including a valid resume. - Its cursor must not skip pending replay; retain the supplied Last-Event-ID - until replay advances it. Filtered-out IDs may leave gaps, so clients must - not infer loss by subtracting IDs. + Every connection begins with stream.ready; a resumed connection then replays + retained events after Last-Event-ID; see [Replay and recovery](/v0.1.0/en/docs/events.html#Replay-and-recovery). x-permission: observe security: - bearerAuth: [] @@ -6730,14 +6726,10 @@ components: name: cursor in: query description: | - Opaque cursor from the previous page's next_cursor, bound to the running - instance, the endpoint and resource it pages, the retained snapshot or - record, the filters and limit. A cursor the server no longer recognises - (snapshot expired or evicted, record left the ring, process restarted, - never issued, or issued for another endpoint or resource) returns - 410 snapshot_expired; discard it and restart the walk without a cursor. A recognised cursor sent - with different filters or a different limit returns 400 invalid_request. - The server never continues a walk against a different snapshot. + Opaque cursor from the previous page's next_cursor, bound to the walk that + issued it (its snapshot or retained records, filters and limit). An unrecognised + cursor returns 410 snapshot_expired; see + [Page cursors](/v0.1.0/en/docs/errors.html#Page-cursors). schema: type: string minLength: 1 @@ -6792,7 +6784,7 @@ components: IfMatchOptional: name: If-Match in: header - description: Evaluated as RFC 9110 §13.1.1 defines; see Conditional requests in the error contract. A new group-configuration PATCH without If-Match returns 428 precondition_required; a retained idempotent replay may omit it. + description: Evaluated as RFC 9110 §13.1.1 defines; see Conditional requests in the error contract. For when a retained idempotent replay may omit it, see [Choosing the status](/v0.1.0/en/docs/errors.html#Choosing-the-status). schema: $ref: '#/components/schemas/EntityTagList' example: '"17"' diff --git a/source/v0.1.0/en/docs/configuration.md b/source/v0.1.0/en/docs/configuration.md index b48b3b2..7737e08 100644 --- a/source/v0.1.0/en/docs/configuration.md +++ b/source/v0.1.0/en/docs/configuration.md @@ -147,8 +147,7 @@ The server refuses the following changes before storing anything: After validation, the server commits the replacement atomically, either before activation or after the new generation becomes active: store readers see the old bytes or the new bytes, never a mix. Concurrent API writes serialize the -hash check, validation, and commit. At commit, a changed stored hash causes -`412`. The server starts a reload operation with `kind: reload`; on failure, +hash check, validation, and commit. The server then starts a reload operation with `kind: reload`; on failure, `written` reports whether the store holds the replacement. The operation succeeds only after both the commit and the activation finish; a `202` means the server accepted the replacement, not that it is stored. @@ -158,9 +157,16 @@ renaming it over the source, preserving the file mode. Each write activates the candidate it validated. The next configuration write waits until the previous activation finishes; a server that does not queue -writes returns `409 state_conflict` instead. An edit made to the store outside -the API before the commit makes the commit fail with `412`. An edit made after -the commit is not part of this activation; it takes effect at a later reload. +writes returns `409 state_conflict` instead. An edit made after the commit is +not part of this activation; it takes effect at a later reload. + +The stored source can change before the commit if it is edited outside the +API. At commit the server evaluates the request's `If-Match` again against the +stored hash. If the condition no longer matches, the commit fails +with `412 stale_revision`. If the stored hash changed but the condition still +matches, as `*` or a list containing the new hash does, the commit fails with +`409 state_conflict`, because the validated candidate is stale. Either way the +replacement is not stored. {% api_example replaceConfigSource 202 queued http %} @@ -175,8 +181,8 @@ replacement before retrying. |-----------------|--------------------| | `403 permission_denied` | Missing `control`, disabled server-wide editing, a read-only source, a replacement that sets or changes API listener settings or secrets, or, in a file store, a source path that is no longer a regular file. Do not offer writes for that source. | | `404 resource_not_found` | Unknown source ID. Refetch the accepted source set. | -| `409 state_conflict` | Another configuration write is still activating and this server does not queue writes; nothing is stored. Retry after it finishes. | -| `412 stale_revision` | The stored content hash differs from `If-Match`; the server stores nothing. Reconcile the changed source before retrying. Refetching the accepted snapshot alone may still return the old hash. | +| `409 state_conflict` | Another configuration write is still activating and this server does not queue writes, or the stored source changed before the commit while `If-Match` still matched it; nothing is stored. Retry after the write finishes, or reconcile the changed source. | +| `412 stale_revision` | `If-Match` does not match the stored content hash, on arrival or [at commit](#Validation-and-commit); the server stores nothing. Reconcile the changed source before retrying. Refetching the accepted snapshot alone may still return the old hash. | | `422 unsupported_value` | Full validation found error diagnostics, including `restart-required`; the server stores nothing and starts no reload. Display diagnostics and correct the candidate. | | `428 precondition_required` | `If-Match` is missing; the server writes nothing. Supply the retained source hash. | diff --git a/source/v0.1.0/en/docs/errors.md b/source/v0.1.0/en/docs/errors.md index 88dcf6c..327336e 100644 --- a/source/v0.1.0/en/docs/errors.md +++ b/source/v0.1.0/en/docs/errors.md @@ -33,7 +33,7 @@ for a failed configuration change listed under | 404 | `resource_not_found` | The requested resource does not exist. | | 404 | `capability_not_supported` | The running engine does not expose the resource or action. | | 405 | `method_not_allowed` | The path exists, but not with this method ([RFC 9110 §15.5.6](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.6)); the response lists the supported methods in `Allow`. An unknown path is `404`. | -| 409 | `state_conflict` | The request is supported, but the current state prevents it: a name already in use, a referenced object that is not current, a transition the current state does not allow, or a configuration change while a write without `If-Match` was being admitted. The same request can succeed after the state changes. | +| 409 | `state_conflict` | The request is supported, but the current state prevents it: a name already in use, a referenced object that is not current, a transition the current state does not allow, a configuration change while a write without `If-Match` was being admitted, or a change between validation and a conditional write's commit that its `If-Match` still matches. The same request can succeed after the state changes. | | 409 | `idempotency_conflict` | An idempotency key was reused with a different request body. | | 409 | `event_cursor_expired` | Event or log SSE cursor cannot be replayed; open a fresh stream and establish a new baseline. | | 409 | `setup_required` | Password login was requested before an administrator was created. | @@ -108,7 +108,10 @@ The server evaluates `If-Match` as defines. `*` matches when the resource exists. A comma-separated list matches when any strong tag in it equals the current one. A weak tag (`W/"…"`) never matches. A header that does not match returns `412 stale_revision`; a header -that is not `*` or a list of entity tags returns `400 invalid_request`. +that is not `*` or a list of entity tags returns `400 invalid_request`. The +server evaluates the condition again when it commits the write; a change that +the condition still matches then returns `409`, as +[validation and commit](configuration.html#Validation-and-commit) defines. The server checks the request in the order under [choosing the status](#Choosing-the-status): body parsing and schema checks come diff --git a/source/v0.1.0/en/docs/groups.md b/source/v0.1.0/en/docs/groups.md index a35fd1c..73e710f 100644 --- a/source/v0.1.0/en/docs/groups.md +++ b/source/v0.1.0/en/docs/groups.md @@ -110,14 +110,17 @@ Updates group configuration only. It does not change runtime selection. Use RFC 6902 JSON Patch and send the `ETag` of `GET /groups/{group_id}/config` in `If-Match`, evaluated as [conditional requests](errors.html#Conditional-requests) -defines. A new group-configuration PATCH without `If-Match` returns `428`; a -retained idempotent replay may omit it. +defines; a retained idempotent replay may omit it, as +[choosing the status](errors.html#Choosing-the-status) describes. Requires `resources.groups.config_patch`; without it the request returns `404 capability_not_supported`. A group patch is a configuration write, so `config_patch` is true only when `resources.config.writable` is. -Because `config_revision` is configuration-wide, a patch sent after an -unrelated accepted change returns `412`. Read `GET /groups/{group_id}/config` -again and retry with its current `ETag`. +Because `config_revision` is configuration-wide, an unrelated accepted change +makes an older `If-Match` fail with `412`; read `GET /groups/{group_id}/config` +again and retry with its current `ETag`. At commit the server evaluates +`If-Match` again: a mismatch returns `412`, and an intervening change that +`If-Match` still matches returns `409`, as for a +[source replacement](configuration.html#Validation-and-commit). {% api_example patchGroupConfig request tolerance http %} @@ -177,9 +180,9 @@ as a `comment`, are ignored ([RFC 6902 §4](https://www.rfc-editor.org/rfc/rfc69 |--------|---------| | 200 | Configuration was applied; the body is the updated configuration document and `ETag` its new revision. | | 202 | The update was accepted and returns the shared `group_update` operation summary. | -| 412 | `If-Match` does not match the current configuration-wide `config_revision`. | +| 412 | `If-Match` does not match the current configuration-wide `config_revision`, on arrival or at commit. | | 404 | The group does not exist, or `resources.groups.config_patch` is false. | -| 409 | A `test` operation failed, or current runtime state prevents the requested transition. | +| 409 | A `test` operation failed, current runtime state prevents the requested transition, or the configuration changed before the commit while `If-Match` still matched. | | 422 | The patch is syntactically valid but the field or value is unsupported. | | 428 | A new patch has no `If-Match`. |