Documentation
¶
Overview ¶
Package sentinel is the single source of truth for how a memory sentinel error crosses the HTTP wire, in both directions: httpsrv reads Table to pick the response status and stamp Header; httpclient reads it to rebuild the sentinel from Header. Neither holds a switch of its own to fall out of date.
Both halves are load-bearing. The status decides retry behaviour — httpclient retries 5xx eight times over a ~17s backoff budget and treats 4xx as permanent. The reconstruction decides whether errors.Is still matches once a call crosses a process boundary: the runner's memory IS an httpclient, so a sentinel with no row arrives as an opaque status error and every errors.Is test against it on the live path silently answers false.
Adding a sentinel ¶
Add a row here. The drift test (sentinel_test.go) fails if a memory sentinel exists with no row and no explicit "not on the wire" entry, so the decision cannot be made by omission.
Index ¶
Constants ¶
const Header = "X-Memory-Sentinel"
Header carries the machine-readable discriminator naming which sentinel a response represents.
The status alone cannot identify one: 403 is answered by TWO sentinels (ErrMissingApproval, ErrKGScopeMismatch) and also by refusals that are no sentinel at all — handleKG's approval door, the read-only-token gate, a cross-session token, any intermediary proxy. Inferring a sentinel from "403" would hand the caller a forged authorization verdict; 400 and 404 are ambiguous the same way (a body-decode failure, an unroutable path).
A header rather than a JSON error envelope keeps the plain-text bodies httpsrv writes (http.Error) intact for the callers and operators that read them as text. A server too old to send the header simply produces no reconstruction — the fail-toward-unknown direction a mixed-version cluster needs.
Variables ¶
var Table = []Mapping{ { Err: memory.ErrInvalidQuery, Status: http.StatusBadRequest, Code: "invalid_query", }, { Err: memory.ErrMissingApproval, Status: http.StatusForbidden, Code: "missing_approval", }, { Err: memory.ErrKGScopeMismatch, Status: http.StatusForbidden, Code: "kg_scope_mismatch", }, { Err: memory.ErrNoSearchProviders, Status: http.StatusNotFound, Code: "no_search_providers", }, { Err: memory.ErrKGUnsupported, Status: http.StatusNotFound, Code: "kg_unsupported", }, { Err: memory.ErrKindNotSessionWritable, Status: http.StatusForbidden, Code: "kind_not_session_writable", }, { Err: memory.ErrKindNotSessionReadable, Status: http.StatusForbidden, Code: "kind_not_session_readable", }, }
Table is the mapping, in match order. Order is only consulted for an error that wraps two sentinels, which no construction in this repo produces.
Functions ¶
func ForCode ¶
ForCode returns the sentinel a wire code names — the CLIENT direction.
Lookup is by code alone, never by status, and an unknown or empty code yields ok=false. Only httpsrv sets Header, so a 403 from an intermediary or from one of the server's non-sentinel refusals stays out of the sentinel space.
func StatusAndCode ¶
StatusAndCode reports the wire status and discriminator for err — the SERVER direction. ok is false for anything that is not a sentinel, which the caller must treat as a fault (5xx) so the client's backoff can ride it out.
Types ¶
type Mapping ¶
type Mapping struct {
// Err is the sentinel itself. Both directions test with errors.Is, so a
// wrapped sentinel matches and keeps its wrapper's message.
Err error
// Status is the HTTP status httpsrv answers Err with. Every one is 4xx:
// these are permanent verdicts (a caller bug, a policy refusal, a
// deployment fact), and no retry can produce a better answer. That is the
// same property that makes their text worth guaranteeing delivery of.
Status int
// Code is Header's value — a stable, machine-readable name. It is wire
// format: never rename one, only add.
Code string
}
Mapping is one sentinel's complete wire contract.