Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions changelog.d/20261102_write-behind-receipt-delay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
### SaaS API — the writer's write-behind copy, and the receipt-delay excess

- [`spec/saas-api.md` § Remaining Freshness](spec/saas-api.md#remaining-freshness), the writer's
own copy: [API-36](spec/saas-api.md#api-36) and [API-37](spec/saas-api.md#api-37) now cover any
copy kept before the SDK's local cache learns of a `2xx`, including a write-behind copy, filled
before its `PUT` is sent, while the write is pending. Once the local cache learns of the `2xx`,
[API-103](spec/saas-api.md#api-103) binds the copy, its clock started at receipt. Receipt is now
defined as the instant the SDK's transport receives the `2xx`.
- Code the application supplies between an SDK's transport and its local cache, such as a backend
wrapper, can hold the `2xx` back, pass it on without its receipt instant, or drop it. The copy
can then outlive API-103's bound: the new **receipt-delay excess**, capped for a copy filled
before the local cache learned of the `2xx`, uncapped for one filled when it learned. Serving
within the excess conforms to API-103 only where such code caused it, never the SDK's own code,
and an SDK SHOULD require such code to pass the `2xx` on at once. The excess is added to the
revocation-propagation sum.
- API-36's recommended anchor is now the earlier of the `PUT`'s first dispatch and the local
write, so it never starts a write-behind copy's clock after the local write.
- The write-path tolerance now runs from the store's stamp to the later of receipt and the SDK's
local write, which covers a copy filled before its `PUT` was sent.
- No wire change.
4 changes: 2 additions & 2 deletions conformance/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ Marked not-a-requirement in `spec/intent-presets.md`:
| [API-32](../spec/saas-api.md#api-32) | *Remaining Freshness*: The value **MUST** be 1–7 ASCII digits and at most `2,592,000` (the header's cap: 30 days, itself seven digits; emission above keeps … | Binds the server.<br>**Gap:** No public vector exercises the server's headers, so nothing here catches a server or tier that emits a value of more than seven digits or over 2,592,000; API-33 is the SDK side. | n/a | n/a | n/a |
| [API-33](../spec/saas-api.md#api-33) | *Remaining Freshness*: … within it). Anything else — empty, non-digit, signed, longer than seven digits, or over the cap — **MUST** be treated as `0` (do not extend local service — the conservative action, mirroring the … | `freshness-headers.json`: `fresh_for_empty`, `fresh_for_negative`, `fresh_for_plus_sign`, `fresh_for_decimal`, `fresh_for_exponent`, `fresh_for_hex`, `fresh_for_digit_separator`, `fresh_for_inner_space`, `fresh_for_nbsp_prefix`, `fresh_for_eight_digits`, `fresh_for_over_cap`, `fresh_for_wraps_32_bit`, `fresh_for_over_64_bits`, `fresh_for_4301_digits`, `fresh_for_superscript_two`, `fresh_for_arabic_indic_one`, `fresh_for_list`<br>`tools/freshness-headers-verify.py:verify`<br>**Gap:** The rows reach the parser an SDK's test hands each value, not its read path: whether the GET 200's header reaches it as its HTTP stack delivers the bytes and the result bounds the backfill (API-29), and what it does with two field lines of the header, which the rows hold only combined into one value. | partial (gap) | partial (gap) | partial (gap) |
| [API-34](../spec/saas-api.md#api-34) | *Remaining Freshness*: … is protocol-impossible, a buggy or misconfigured tier rather than a real bound. The length check **MUST** run first, so the range check never depends on a fixed-width integer conversion that could wrap an over-cap value back … | `freshness-headers.json`: `fresh_for_eight_digits`, `fresh_for_wraps_32_bit`, `fresh_for_over_64_bits`, `fresh_for_4301_digits`<br>`tools/freshness-headers-verify.py:verify`<br>**Gap:** A parser that converts first, with arithmetic that neither wraps nor fails on a long value, and checks the length after returns the same values, so the order shows only through the conversions the rows break (32- and 64-bit integers, CPython's 4,300-digit limit); and the rows reach the parser, not the read path (API-33). | partial (gap) | partial (gap) | partial (gap) |
| [API-36](../spec/saas-api.md#api-36) | *Remaining Freshness*: … absorbed. The store may still have accepted that write, and no `2xx` starts the clock, so the SDK **MUST** start that copy's `min(local_ttl, effective_ttl)` clock no later than its local write, and MUST NOT serve the copy, in … | **Gap:** As API-103, for an SDK that keeps a copy after a timeout or a transport error and starts its clock only at a later read, or never. | gap | gap | gap |
| [API-36](../spec/saas-api.md#api-36) | *Remaining Freshness*: … that write, and until a `2xx` reaches the local cache nothing starts the clock, so the SDK **MUST** start that copy's `min(local_ttl, effective_ttl)` clock no later than its local write, and MUST NOT serve the copy, in … | **Gap:** As API-103, for an SDK that keeps a copy after a timeout or a transport error and starts its clock only at a later read, or never. | gap | gap | gap |
| [API-37](../spec/saas-api.md#api-37) | *Remaining Freshness*: … MUST start that copy's `min(local_ttl, effective_ttl)` clock no later than its local write, and **MUST NOT** serve the copy, in any form, once that has elapsed. Such a copy can then outlive `fresh_until` by up to the time from … | **Gap:** As API-103, for an SDK that serves such a copy past its bound through a local stale-grace policy. | gap | gap | gap |
| [API-38](../spec/saas-api.md#api-38) | *Remaining Freshness*: … this sum, so TTL bounds the revocation window only together with them. Security-sensitive caches **MUST** size TTL (and local TTL) to their revocation tolerance, or version their keys (see the invalidation-race note in … | Binds application code.<br>**Gap:** The TTL an application gives a security-sensitive cache, and whether it versions its keys, are its own choices, which no vector or SDK test can observe. | n/a | n/a | n/a |
| [API-39](../spec/saas-api.md#api-39) | *Remaining Freshness*: … keys (see the invalidation-race note in Semantics notes); keys whose TTL is a revocation boundary **MUST** be stored with an explicit `X-CacheKit-TTL` — a no-expiry entry has no revocation tolerance to size to. | Binds application code.<br>**Gap:** As API-38: whether a key whose TTL is a revocation boundary is written with an explicit TTL is the application's configuration. | n/a | n/a | n/a |
Expand Down Expand Up @@ -294,7 +294,7 @@ Marked not-a-requirement in `spec/intent-presets.md`:
| [API-100](../spec/saas-api.md#api-100) | *Remaining Freshness*: Emitted on a write, too. A store that applies a plan maximum TTL **MUST** emit `X-CacheKit-Fresh-For` on every `PUT` `2xx` whose stored entry has a freshness bound, computed as above from the … | Binds the server.<br>**Gap:** No public vector exercises the server's responses, so nothing here catches a clamping store that answers a bounded PUT without X-CacheKit-Fresh-For, or reports the stated TTL instead of the remainder it stamped. | n/a | n/a | n/a |
| [API-101](../spec/saas-api.md#api-101) | *Remaining Freshness*: … the one it stated when the plan maximum clamped the write (the writer's own copy below). A server **MUST NOT** clamp any write or `PATCH` until its store emits the header on `PUT`, every server-side hop that relays a `PUT` `2xx` … | Binds the server.<br>**Gap:** As API-100, for a deployment that clamps writes before its store emits X-CacheKit-Fresh-For on PUT, or while a server-side hop that relays the PUT 2xx drops the header or a tier bounds its copy by the stated TTL. | n/a | n/a | n/a |
| [API-102](../spec/saas-api.md#api-102) | *Remaining Freshness*: … — under any label. A tier, or any other server-side hop, that relays the store's `PUT` `2xx` **MUST NOT** drop the `X-CacheKit-Fresh-For` of the store's `2xx` from the response it relays, nor relay a value larger than the … | Binds the server.<br>**Gap:** As API-19, for a tier or other server-side hop that drops the X-CacheKit-Fresh-For of the store's PUT 2xx, or relays a larger value. | n/a | n/a | n/a |
| [API-103](../spec/saas-api.md#api-103) | *Remaining Freshness*: … on the `PUT` `2xx` (emission above). Once the store accepts the `PUT`, the SDK **MUST NOT** serve that copy, in any form, after `min(local_ttl, effective_ttl, fresh_for)` has elapsed, where `effective_ttl` is … | **Gap:** No vector reaches an SDK's write path or its local cache, so nothing here catches one that bounds its copy of an accepted write by the local TTL alone, keeps the stated TTL when the PUT 2xx reported a shorter bound, or lengthens the copy by X-CacheKit-Stale-TTL. | gap | gap | gap |
| [API-103](../spec/saas-api.md#api-103) | *Remaining Freshness*: … on the `PUT` `2xx` (emission above). Once the store accepts the `PUT`, the SDK **MUST NOT** serve that copy, in any form, after `min(local_ttl, effective_ttl, fresh_for)` has elapsed, where `effective_ttl` is … | **Gap:** No vector reaches an SDK's write path or its local cache, so nothing here catches one that bounds its copy of an accepted write by the local TTL alone, keeps the stated TTL when the PUT 2xx reported a shorter bound, or lengthens the copy by X-CacheKit-Stale-TTL; nor one that serves its copy before its local cache learns of the PUT 2xx (write-behind) and, once it learns, keeps the copy's API-36 bound from its local write, or restarts the clock then instead of at receipt, though it holds the receipt instant. | gap | gap | gap |
| [API-104](../spec/saas-api.md#api-104) | *PATCH /v1/cache/{key}/ttl*: … reports the remaining time to `evict_at`, stale window included), so a `PATCH` response **MUST NOT** create, refresh or extend any local entry's service bound. For entries stored with a stale-grace window, see SWR write … | **Gap:** No vector reaches an SDK's local cache, so nothing here catches one that re-anchors its local copy to the requested ttl when a PATCH /ttl succeeds, though the store may have clamped it. | gap | gap | gap |
| [API-105](../spec/saas-api.md#api-105) | *Remaining Freshness*: Decay, never re-stamp. A tier **MUST** emit its copy's remaining bound minus the seconds elapsed since that bound was obtained, flooring at `0`. A copy has … | Binds the server.<br>**Gap:** As API-19, for a tier that stamps each hit with its copy's original bound instead of the decayed remainder, or bounds its copy of a forwarded write by the stated TTL when the store's PUT 2xx reported a shorter bound. | n/a | n/a | n/a |
| [API-106](../spec/saas-api.md#api-106) | *Remaining Freshness*: … no later than `fresh_until`, and one from the effective TTL ends less than one second after it. It **MUST NOT** emit a value larger than that decayed remainder or than `2592000` (API-32) — its own copy TTL included — under any … | Binds the server.<br>**Gap:** As API-19, for a tier that emits its own copy TTL when that is larger than the decayed remainder, or a remainder above 2,592,000. | n/a | n/a | n/a |
Expand Down
2 changes: 1 addition & 1 deletion conformance/requirements.json
Original file line number Diff line number Diff line change
Expand Up @@ -2214,7 +2214,7 @@
"API-103": {
"section": "Remaining Freshness",
"binds": "sdk",
"gap": "No vector reaches an SDK's write path or its local cache, so nothing here catches one that bounds its copy of an accepted write by the local TTL alone, keeps the stated TTL when the PUT 2xx reported a shorter bound, or lengthens the copy by X-CacheKit-Stale-TTL."
"gap": "No vector reaches an SDK's write path or its local cache, so nothing here catches one that bounds its copy of an accepted write by the local TTL alone, keeps the stated TTL when the PUT 2xx reported a shorter bound, or lengthens the copy by X-CacheKit-Stale-TTL; nor one that serves its copy before its local cache learns of the PUT 2xx (write-behind) and, once it learns, keeps the copy's API-36 bound from its local write, or restarts the clock then instead of at receipt, though it holds the receipt instant."
},
"API-104": {
"section": "PATCH /v1/cache/{key}/ttl",
Expand Down
Loading
Loading