Documentation
¶
Overview ¶
Package idemlease implements a lease-based idempotency state machine for retry-safe APIs, with zero dependencies beyond the Go standard library (and no net/http in the core).
The core treats idempotency keys, request fingerprints, and stored payloads as opaque values. It exposes two entry points, Begin and Finish, on top of a pluggable Store. HTTP-specific concerns (header grammar, fingerprinting, response capture, middleware) live in the httpidem subpackage, whose middleware is the default entry point for most users.
Guarantee: for a given key (within a scope), at most one execution holds a valid lease at any time. Exactly-once execution is NOT guaranteed; see the README for details.
Index ¶
Constants ¶
const ( DefaultLeaseTTL = 30 * time.Second DefaultRecordTTL = 24 * time.Hour )
Defaults applied when the corresponding Options field is zero or negative.
Variables ¶
var ( // ErrAlreadyExists is returned by Reserve, together with the existing // record, when a valid (non-expired) record already holds the key. ErrAlreadyExists = errors.New("idemlease: record already exists") // ErrTokenMismatch is returned by Complete and Release when a record // exists for the key but is held under a different reservation token. ErrTokenMismatch = errors.New("idemlease: reservation token mismatch") // ErrNotFound is returned by Complete and Release when no record // exists for the key (for example, expired and removed). ErrNotFound = errors.New("idemlease: record not found") )
Sentinel errors that Store implementations must return (wrapped or not) so that callers can match them with errors.Is.
Functions ¶
func Finish ¶
func Finish(ctx context.Context, s Store, key, token string, d Decision, payload []byte, o Options) (leaseLost bool, err error)
Finish performs the trailing state transition for a Proceed outcome: Persist stores payload via Complete using o.RecordTTL, Discard drops the reservation via Release (payload is ignored).
ErrTokenMismatch and ErrNotFound from the store are normalized to (leaseLost=true, nil): the lease was lost to lease expiry or another execution, so the caller's result was not persisted and will not be replayed. Any other error is returned as-is.
Types ¶
type Action ¶
type Action int
Action tells the caller how to proceed after Begin.
const ( // Proceed means no valid record existed: the caller now holds the // lease. Execute the operation, then call Finish with Outcome.Token. Proceed Action = iota // Replay means a completed record with a matching fingerprint // exists. Serve Outcome.Payload instead of executing. Replay // RejectInFlight means another execution holds a valid lease. // Outcome.RetryAfter carries the remaining lease. RejectInFlight // RejectFingerprintMismatch means a completed record exists but was // created by a request with a different fingerprint. RejectFingerprintMismatch )
type Decision ¶
type Decision int
Decision selects the trailing state transition performed by Finish.
type Options ¶
type Options struct {
// LeaseTTL bounds a single in-flight execution. While the lease is
// valid, concurrent executions of the same key are rejected; after
// it expires the key becomes re-executable.
LeaseTTL time.Duration
// RecordTTL bounds how long a completed payload is replayed. It is
// passed to Store.Complete by Finish when the decision is Persist.
RecordTTL time.Duration
}
Options carries the TTLs for one Begin/Finish pair. Callers should pass the same Options value to both calls.
type Outcome ¶
type Outcome struct {
Action Action
Token string // set on Proceed; pass to Finish
Payload []byte // set on Replay; the stored payload
RetryAfter time.Duration // set on RejectInFlight; remaining lease (>= 0)
}
Outcome is the result of Begin. Only the fields relevant to Action are set; on a non-nil error from Begin the Outcome is meaningless.
func Begin ¶
func Begin(ctx context.Context, s Store, key string, fingerprint []byte, o Options) (Outcome, error)
Begin inspects the record stored under key and performs the leading state transition in a single Reserve call:
no record, or expired record only → reserve the key and Proceed valid reserved (any fingerprint) → RejectInFlight + remaining lease valid completed, matching print → Replay + stored payload valid completed, different print → RejectFingerprintMismatch
The reservation token is generated here (crypto/rand, 128 bits) and carried to the Store on the Record; stores never generate or alter it.
On Proceed the caller must execute the operation and then call Finish with Outcome.Token. If Finish is never called — a caller bug — the record stays reserved until the lease expires: the key keeps being rejected in-flight until then and becomes re-executable afterwards. The core never reclaims reservations on its own.
A non-nil error means infrastructure failed (the Store, or in the extreme case token generation) before any idempotency decision was made; interpreting it as fail-open or fail-closed is the caller's responsibility.
Begin also defends against stores that break the §3.2 contract: receiving an already-expired record as existing is retried — the record may have expired legitimately in the instants after the store's atomic check — and reported as a store bug if it persists.
type Record ¶
type Record struct {
// Key identifies the record. The core does not limit its length.
Key string
// Fingerprint identifies the request that created the record.
Fingerprint []byte
// State is StateReserved or StateCompleted.
State State
// Token is the reservation token generated by Begin (crypto/rand,
// at least 128 bits). Stores persist and compare it verbatim.
Token string
// LeaseExpiresAt bounds the in-flight execution: a reserved record
// at or past this instant is logically absent.
LeaseExpiresAt time.Time
// RecordExpiresAt bounds replay: a completed record at or past this
// instant is logically absent. It is zero while reserved and is set
// by Complete.
RecordExpiresAt time.Time
// Payload is the stored result, set when State is StateCompleted.
Payload []byte
}
Record is the unit persisted by a Store. The core treats Key, Fingerprint, and Payload as opaque values: it neither interprets nor validates them (key grammar, fingerprint algorithms, and payload encodings belong to callers such as httpidem).
type Store ¶
type Store interface {
// Reserve atomically claims the key in the reserved state.
//
// If a valid (non-expired) record already exists, Reserve returns it
// as existing together with ErrAlreadyExists. If only an expired
// record exists, Reserve must claim the key atomically as if no
// record existed (overwrite-reserve); it must never return an
// expired record as existing. A two-step GET-then-SET implementation
// is not allowed: use a single atomic operation such as SETNX or
// INSERT ... ON CONFLICT.
Reserve(ctx context.Context, rec Record) (existing *Record, err error)
// Complete transitions the reserved record to completed, storing
// payload and setting the record expiry to now+recordTTL. It must be
// a compare-and-set on the token: a record held under a different
// token yields ErrTokenMismatch; a missing (or expired) record
// yields ErrNotFound.
Complete(ctx context.Context, key, token string, payload []byte, recordTTL time.Duration) error
// Release deletes the reserved record, allowing re-execution. Like
// Complete it is a compare-and-set on the token: ErrTokenMismatch on
// a token mismatch, ErrNotFound when no record exists.
Release(ctx context.Context, key, token string) error
// Get returns the record stored under key, for operations,
// debugging, adapters, and conformance tests. It is not used on the
// request path (Reserve's existing return value covers it). Expired
// records may be reported as (nil, nil).
Get(ctx context.Context, key string) (*Record, error)
}
Store persists idempotency records. Implementations must follow the semantics documented on each method; the conformance suite in idemleasetest verifies them.
Stores never generate or alter reservation tokens: they persist the token carried by the Record passed to Reserve and compare it verbatim in Complete and Release. Token generation is Begin's job.
Directories
¶
| Path | Synopsis |
|---|---|
|
errtrailadapter
module
|
|
|
ginadapter
module
|
|
|
grpcidem
module
|
|
|
Package httpidem adds Idempotency-Key semantics to net/http handlers, backed by the idemlease state machine.
|
Package httpidem adds Idempotency-Key semantics to net/http handlers, backed by the idemlease state machine. |
|
httpidemtest
Package httpidemtest provides an HTTP-level conformance suite for idemlease.Store implementations driven through the httpidem middleware.
|
Package httpidemtest provides an HTTP-level conformance suite for idemlease.Store implementations driven through the httpidem middleware. |
|
Package idemleasetest provides conformance test suites for idemlease.Store implementations and for the Begin/Finish state machine running on top of them.
|
Package idemleasetest provides conformance test suites for idemlease.Store implementations and for the Begin/Finish state machine running on top of them. |
|
Package memstore provides an in-memory idemlease.Store for development and testing.
|
Package memstore provides an in-memory idemlease.Store for development and testing. |
|
pgstore
module
|
|
|
redistore
module
|