Documentation
¶
Overview ¶
Package cas provides a typed-Go-heavy Protocol primitive for compare-and-swap (CAS) optimistic concurrency control.
Conceptually, CAS is modeled after Kubernetes resourceVersion and etcd compare-then-put: a writer reads the current version, passes it as an expected version on the mutation, and the store rejects the write if the current version has advanced (i.e. a concurrent writer won).
Design contrast with session and ledger ¶
Unlike runtime/auth/session and runtime/audit/ledger, cas does NOT expose a Store interface, mem_store, or storetest suite. CAS is a write-time policy primitive, not an entity lifecycle: each cell owns its own private entities (User, ConfigEntry, FeatureFlag, …) and their schemas cannot be unified into a shared generic Store without leaking cell-private domain types across cell boundaries — a violation of the GoCell cross-cell isolation rule.
Conformance is proved by cell-private repository race-condition integration tests (real DB with concurrent writers), not by a shared storetest suite.
What this package provides ¶
- ConflictPolicy sealed interface — today only ConflictPolicyStrictReject (HTTP 409, no retry). Future siblings (LastWriteWins, RetryWithMerge) may be added here without breaking callers.
- Protocol value — bundles the versionField name and a ConflictPolicy. Constructed via NewProtocol (composition root only).
- CheckVersionMatch — translates UPDATE/DELETE RowsAffected into the standard ErrVersionConflict error vocabulary so cell repos share a single check point.
ref: docs/architecture/202605101200-adr-typed-go-heavy-protocol-primitives.md
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CheckVersionMatch ¶
CheckVersionMatch translates UPDATE/DELETE ... WHERE version=$expected RowsAffected outcome into the standard CAS error vocabulary.
- rowsAffected == 1: nil (caller applied successfully)
- rowsAffected == 0: ErrVersionConflict (KindConflict / HTTP 409) — caller may follow up with a NotFound probe (separate GetByKey) to distinguish "key absent" from "version mismatch"; cas treats both as conflict from CAS perspective and lets the caller decide upstream error mapping.
- rowsAffected > 1: also returns ErrVersionConflict — the WHERE clause matched more rows than expected, indicating a schema or query error.
entityDesc and entityKey are persisted in the Internal field (server-side slog only) — they are NOT emitted to the HTTP wire as Details. This avoids leaking entity type / record identifiers (userID, config key, etc.) to clients via the standard error envelope. Operators retrieve them from the service log when correlating retries.
Callers MUST distinguish key-absent vs version-mismatch by probing existence first (e.g. via SELECT FOR UPDATE before UPDATE, or a GetByKey probe after UPDATE/DELETE returns no rows). CheckVersionMatch unconditionally translates rowsAffected==0 to ErrVersionConflict; the caller's pre-check provides the NotFound branch. Without a pre-check, a DELETE on a non-existent key returns ErrVersionConflict (409) instead of the correct ErrNotFound (404).
Types ¶
type ConflictPolicy ¶
type ConflictPolicy interface {
// contains filtered or unexported methods
}
ConflictPolicy is a sealed marker interface — it carries no behavior, only type discrimination. Today only ConflictPolicyStrictReject is implemented; future siblings (LastWriteWins / RetryWithMerge) may be added as additional types in this package. The marker method conflictPolicyOK is unexported, so external packages cannot implement ConflictPolicy — this prevents callers from injecting unsanctioned conflict strategies past the runtime/state/cas API boundary. Matches the sealed-marker pattern in runtime/auth/session (FingerprintMode / OrderingModel) and runtime/audit/ledger (RestartRecoveryMode / IdempotencyMode).
type ConflictPolicyStrictReject ¶
type ConflictPolicyStrictReject struct{}
ConflictPolicyStrictReject means CAS mismatch returns ErrVersionConflict (HTTP 409) without retry. This is the safe default and the only policy implemented today.
type Option ¶
Option configures a Protocol during construction.
func WithConflictPolicy ¶
func WithConflictPolicy(c ConflictPolicy) Option
WithConflictPolicy sets the policy. Nil interface (bare or typed) is sticky-rejected via sentinel — fail-fast at NewProtocol.
Both bare-nil and typed-nil ConflictPolicy values are rejected by NewProtocol so the conflict policy is never silently absent. Pattern mirrors runtime/auth/session.WithFingerprint (strong-dependency wiring option).
func WithVersionField ¶
WithVersionField names the DB column / domain field that carries the monotonic version counter (e.g. "password_version", "version"). Required. Empty string is rejected at Option apply time.
type Protocol ¶
type Protocol struct {
// contains filtered or unexported fields
}
Protocol bundles CAS protocol decisions for a particular consumer (cell). Construct via NewProtocol (composition root only).
func NewProtocol ¶
NewProtocol fail-fasts on missing required fields and typed-nil. Defaults conflict policy to ConflictPolicyStrictReject when omitted.
func (*Protocol) Conflict ¶
func (p *Protocol) Conflict() ConflictPolicy
Conflict returns the configured ConflictPolicy.
func (*Protocol) VersionField ¶
VersionField returns the configured DB column / domain field name that carries the monotonic version counter.