Documentation
¶
Overview ¶
Package idempotency defines durable ownership and replay semantics for operations that may be retried.
The package does not guarantee exactly-once execution. A lease proves only that an owner may act during a bounded interval; it cannot prove that an expired owner stopped. Applications must carry the fencing token into the transaction or conditional write that protects the business side effect.
Index ¶
- Constants
- func WithOwnership(ctx context.Context, ownership Ownership) context.Context
- type AcquireRequest
- type AcquireResult
- type AvailabilityPolicy
- type BeginRequest
- type BeginResult
- type CompleteRequest
- type Error
- type FailRequest
- type Fingerprint
- type HeartbeatRequest
- type Key
- type KeyHasher
- type Observation
- type Observer
- type ObserverFunc
- type Outcome
- type Ownership
- type Reason
- type Record
- type Service
- func (s *Service) Begin(ctx context.Context, request BeginRequest) (result BeginResult, err error)
- func (s *Service) Complete(ctx context.Context, request CompleteRequest) (record Record, err error)
- func (s *Service) Expire(ctx context.Context, key Key) (record Record, err error)
- func (s *Service) Fail(ctx context.Context, request FailRequest) (record Record, err error)
- func (s *Service) Heartbeat(ctx context.Context, request HeartbeatRequest) (record Record, err error)
- func (s *Service) Inspect(ctx context.Context, key Key) (record Record, err error)
- func (s *Service) Release(ctx context.Context, ownership Ownership) (record Record, err error)
- type ServiceOptions
- type State
- type Store
- type Transition
Examples ¶
Constants ¶
const ( // MaxKeyPartBytes bounds each individual logical key component. MaxKeyPartBytes = 256 // MaxFingerprintVersionBytes bounds the canonicalization policy identifier. MaxFingerprintVersionBytes = 128 // MaxOwnerTokenBytes bounds opaque ownership proofs stored by adapters. MaxOwnerTokenBytes = 256 // MaxResultBytes bounds a result stored for terminal replay. MaxResultBytes = 1 << 20 // MaxMetadataEntries bounds the number of stored metadata pairs. MaxMetadataEntries = 32 // MaxMetadataKeyBytes bounds each metadata key. MaxMetadataKeyBytes = 128 // MaxMetadataValueBytes bounds each metadata value. MaxMetadataValueBytes = 1024 // MaxLease is the longest ownership lease accepted by the semantic core. MaxLease = 24 * time.Hour )
Variables ¶
This section is empty.
Functions ¶
Types ¶
type AcquireRequest ¶
type AcquireRequest struct {
Key Key
Fingerprint Fingerprint
Lease time.Duration
}
AcquireRequest supplies the stable identity, fingerprint, and requested lease.
type AcquireResult ¶
AcquireResult contains the acquisition outcome and authoritative record.
type AvailabilityPolicy ¶
type AvailabilityPolicy uint8
AvailabilityPolicy controls whether work may run after acquisition storage fails.
const ( // AvailabilityFailClosed rejects execution when ownership is unavailable. AvailabilityFailClosed AvailabilityPolicy = iota // AvailabilityAllowUntracked permits explicitly duplicate-tolerant execution. AvailabilityAllowUntracked )
type BeginRequest ¶
type BeginRequest struct {
Acquire AcquireRequest
Availability AvailabilityPolicy
}
BeginRequest combines durable acquisition with an availability policy.
type BeginResult ¶
BeginResult describes whether work should execute and whether it is tracked.
type CompleteRequest ¶
CompleteRequest records a bounded successful terminal result.
type Error ¶
type Error struct {
// Reason classifies the failure for programmatic handling.
Reason Reason
// Field identifies the input, transition, or backend property involved.
Field string
// Cause retains the underlying failure without changing the stable reason.
Cause error
}
Error reports a stable reason and field while retaining an optional cause.
type FailRequest ¶
FailRequest records a bounded terminal failure result.
type Fingerprint ¶
type Fingerprint struct {
// contains filtered or unexported fields
}
Fingerprint is a versioned SHA-256 digest of canonical business input.
func NewFingerprint ¶
func NewFingerprint(version string, canonical []byte) (Fingerprint, error)
NewFingerprint hashes canonical business input under a stable policy version.
func NewFingerprintFromSum ¶
func NewFingerprintFromSum(version string, sum []byte) (Fingerprint, error)
NewFingerprintFromSum reconstructs a fingerprint from a persisted SHA-256 sum.
func (Fingerprint) Equal ¶
func (f Fingerprint) Equal(other Fingerprint) bool
Equal reports whether both the policy version and digest match.
func (Fingerprint) Sum ¶
func (f Fingerprint) Sum() []byte
Sum returns a copy of the SHA-256 digest bytes.
func (Fingerprint) Version ¶
func (f Fingerprint) Version() string
Version returns the canonicalization policy version.
type HeartbeatRequest ¶
HeartbeatRequest extends a live current owner's lease.
type Key ¶
type Key struct {
// contains filtered or unexported fields
}
Key is the fully scoped logical identity of one repeatable operation. Construct keys with NewKey so every component is present and bounded.
type KeyHasher ¶
KeyHasher returns a non-reversible correlation value for a logical key.
func NewHMACKeyHasher ¶
NewHMACKeyHasher constructs a deterministic SHA-256 HMAC key hasher. The secret is copied and must contain at least 32 bytes.
type Observation ¶
type Observation struct {
// Transition identifies the semantic operation.
Transition Transition
// Outcome is populated for acquisition attempts.
Outcome Outcome
// Reason classifies a failed transition and is empty on success.
Reason Reason
// Durable reports whether the requested state or result is durably established.
Durable bool
// Correlation is a keyed digest when ServiceOptions provides a KeyHasher.
Correlation string
}
Observation is a bounded service transition signal safe for instrumentation. Correlation is suitable for restricted logs, never metric labels.
type Observer ¶
type Observer interface {
Observe(context.Context, Observation)
}
Observer receives bounded semantic service transition signals. Service isolates observer and key-hasher panics so instrumentation cannot change a semantic result. Implementations should still return quickly and honor ctx.
type ObserverFunc ¶
type ObserverFunc func(context.Context, Observation)
ObserverFunc adapts a function to Observer.
func (ObserverFunc) Observe ¶
func (f ObserverFunc) Observe(ctx context.Context, observation Observation)
Observe calls f with the bounded transition signal.
type Outcome ¶
type Outcome string
Outcome describes the semantic result of attempting acquisition.
const ( // OutcomeAcquired means the caller owns a new executable attempt. OutcomeAcquired Outcome = "acquired" // OutcomeReplayed means the same fingerprint has a completed result. OutcomeReplayed Outcome = "replayed" // OutcomeInProgress means another unexpired owner is current. OutcomeInProgress Outcome = "in_progress" // OutcomeConflict means the retained key has a different fingerprint. OutcomeConflict Outcome = "conflict" OutcomeUnavailable Outcome = "unavailable" // OutcomeStaleOwnerTakeover means the caller replaced an elapsed active owner. OutcomeStaleOwnerTakeover Outcome = "stale_owner_takeover" // OutcomeTerminalFailure means a terminal failure is available for replay. OutcomeTerminalFailure Outcome = "terminal_failure" )
type Reason ¶
type Reason string
Reason is a stable machine-readable classification for a semantic error.
const ( // ReasonInvalidKey identifies a missing or invalid logical key component. ReasonInvalidKey Reason = "invalid_key" // ReasonInvalidFingerprint identifies an invalid fingerprint or policy version. ReasonInvalidFingerprint Reason = "invalid_fingerprint" // ReasonLimitExceeded identifies input that crosses a documented resource bound. ReasonLimitExceeded Reason = "limit_exceeded" // ReasonStaleOwner identifies an ownership proof from a superseded attempt. ReasonStaleOwner Reason = "stale_owner" // ReasonLeaseExpired identifies a current proof used after its lease boundary. ReasonLeaseExpired Reason = "lease_expired" // ReasonNotFound identifies an operation targeting a missing record. ReasonNotFound Reason = "not_found" // ReasonInvalidTransition identifies an operation illegal for the current state. ReasonInvalidTransition Reason = "invalid_transition" ReasonUnavailable Reason = "unavailable" // ReasonInvalidConfiguration identifies invalid constructor or policy options. ReasonInvalidConfiguration Reason = "invalid_configuration" // ReasonInvalidLease identifies a nonpositive lease duration. ReasonInvalidLease Reason = "invalid_lease" // ReasonInvalidPayload identifies malformed or unsupported persisted data. ReasonInvalidPayload Reason = "invalid_payload" // ReasonUnsafeBackend identifies a backend configuration that breaks correctness. ReasonUnsafeBackend Reason = "unsafe_backend" )
type Record ¶
type Record struct {
Key Key
Fingerprint Fingerprint
State State
OwnerToken string
FencingToken uint64
LeaseExpiresAt time.Time
HeartbeatAt time.Time
Attempt uint64
CreatedAt time.Time
UpdatedAt time.Time
CompletedAt time.Time
FailedAt time.Time
AbandonedAt time.Time
ExpiredAt time.Time
Result []byte
Metadata map[string]string
}
Record is a snapshot of one retained idempotency state machine.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service normalizes store failures and applies availability policy to acquisition.
func NewService ¶
NewService constructs the semantic service over a non-nil store.
func NewServiceWithOptions ¶
func NewServiceWithOptions(store Store, options ServiceOptions) (*Service, error)
NewServiceWithOptions constructs a service with optional bounded observation.
func (*Service) Begin ¶
func (s *Service) Begin( ctx context.Context, request BeginRequest, ) (result BeginResult, err error)
Begin attempts ownership and decides whether the caller may execute.
Example ¶
package main
import (
"context"
"fmt"
"time"
"github.com/faustbrian/go-idempotency"
"github.com/faustbrian/go-idempotency/memory"
)
type exampleClock struct {
now time.Time
}
func (c exampleClock) Now() time.Time { return c.now }
func main() {
clock := exampleClock{now: time.Date(2026, 7, 15, 12, 0, 0, 0, time.UTC)}
token := 0
store, err := memory.New(memory.Options{
Clock: clock,
OwnerTokens: func() (string, error) {
token++
return fmt.Sprintf("owner-%d", token), nil
},
})
if err != nil {
panic(err)
}
service, err := idempotency.NewService(store)
if err != nil {
panic(err)
}
key, err := idempotency.NewKey(
"billing", "tenant-42", "create-invoice", "api-client-7", "request-123",
)
if err != nil {
panic(err)
}
fingerprint, err := idempotency.NewFingerprint(
"invoice-v1", []byte("invoice:9001:EUR"),
)
if err != nil {
panic(err)
}
request := idempotency.BeginRequest{Acquire: idempotency.AcquireRequest{
Key: key, Fingerprint: fingerprint, Lease: 30 * time.Second,
}}
first, err := service.Begin(context.Background(), request)
if err != nil {
panic(err)
}
fmt.Println(first.Outcome, first.Execute, first.Durable)
_, err = service.Complete(context.Background(), idempotency.CompleteRequest{
Ownership: first.Record.Ownership(),
Result: []byte(`{"invoice_id":"inv-9001"}`),
})
if err != nil {
panic(err)
}
retry, err := service.Begin(context.Background(), request)
if err != nil {
panic(err)
}
fmt.Printf("%s: %s\n", retry.Outcome, retry.Record.Result)
}
Output: acquired true true replayed: {"invoice_id":"inv-9001"}
func (*Service) Complete ¶
func (s *Service) Complete( ctx context.Context, request CompleteRequest, ) (record Record, err error)
Complete conditionally records a successful terminal result.
func (*Service) Expire ¶
Expire records that an active lease elapsed without granting new ownership.
func (*Service) Heartbeat ¶
func (s *Service) Heartbeat( ctx context.Context, request HeartbeatRequest, ) (record Record, err error)
Heartbeat extends a live current owner's lease.
type ServiceOptions ¶
type ServiceOptions struct {
// Observer receives one signal after each instrumented semantic transition.
Observer Observer
// KeyHasher produces restricted-log correlation without exposing logical keys.
KeyHasher KeyHasher
}
ServiceOptions configures optional bounded service instrumentation.
type State ¶
type State string
State is the durable lifecycle state of an idempotency record.
const ( // StateAcquired means an owner holds a fresh lease but has not heartbeated. StateAcquired State = "acquired" // StateRunning means the current owner has extended its lease. StateRunning State = "running" // StateCompleted means a successful bounded result is terminal and replayable. StateCompleted State = "completed" // StateFailed means a bounded terminal failure is replayable. StateFailed State = "failed" // StateExpired means an elapsed active lease was explicitly recorded. StateExpired State = "expired" // StateAbandoned means the owner deliberately released without a result. StateAbandoned State = "abandoned" )
type Store ¶
type Store interface {
Acquire(context.Context, AcquireRequest) (AcquireResult, error)
Inspect(context.Context, Key) (Record, error)
Heartbeat(context.Context, HeartbeatRequest) (Record, error)
Complete(context.Context, CompleteRequest) (Record, error)
Fail(context.Context, FailRequest) (Record, error)
Release(context.Context, Ownership) (Record, error)
Expire(context.Context, Key) (Record, error)
}
Store atomically persists every semantic state-machine transition. Implementations must satisfy the ownership and fencing contract described by the package, including backend-authoritative time when durable.
type Transition ¶
type Transition string
Transition identifies one fixed-cardinality semantic service operation.
const ( // TransitionAcquire identifies a Service.Begin acquisition attempt. TransitionAcquire Transition = "acquire" // TransitionInspect identifies a Service.Inspect read. TransitionInspect Transition = "inspect" // TransitionHeartbeat identifies a Service.Heartbeat lease extension. TransitionHeartbeat Transition = "heartbeat" // TransitionComplete identifies a Service.Complete terminal transition. TransitionComplete Transition = "complete" // TransitionFail identifies a Service.Fail terminal transition. TransitionFail Transition = "fail" // TransitionRelease identifies a Service.Release abandonment transition. TransitionRelease Transition = "release" // TransitionExpire identifies a Service.Expire audit transition. TransitionExpire Transition = "expire" )
Directories
¶
| Path | Synopsis |
|---|---|
|
Package canonical provides bounded, explicit request fingerprint policies.
|
Package canonical provides bounded, explicit request fingerprint policies. |
|
Package idempotencycommand provides durable named command and source-record import execution with bounded result replay.
|
Package idempotencycommand provides durable named command and source-record import execution with bounded result replay. |
|
Package idempotencyhttp provides buffered net/http middleware backed by an idempotency.Service.
|
Package idempotencyhttp provides buffered net/http middleware backed by an idempotency.Service. |
|
Package idempotencylog adapts bounded idempotency observations to log/slog.
|
Package idempotencylog adapts bounded idempotency observations to log/slog. |
|
Package idempotencyoutbox coordinates a transactional outbox insert with PostgreSQL idempotency completion in one caller-owned transaction.
|
Package idempotencyoutbox coordinates a transactional outbox insert with PostgreSQL idempotency completion in one caller-owned transaction. |
|
Package idempotencyqueue provides durable consumer ownership and redelivery deduplication for messages exposing a Payload method.
|
Package idempotencyqueue provides durable consumer ownership and redelivery deduplication for messages exposing a Payload method. |
|
Package idempotencyrpc provides method-aware durable JSON-RPC invocation ownership and bounded response or protocol-error replay.
|
Package idempotencyrpc provides method-aware durable JSON-RPC invocation ownership and bounded response or protocol-error replay. |
|
Package idempotencytelemetry adapts bounded observations to OpenTelemetry metrics.
|
Package idempotencytelemetry adapts bounded observations to OpenTelemetry metrics. |
|
Package idempotencytest provides reusable adapter conformance and fixtures.
|
Package idempotencytest provides reusable adapter conformance and fixtures. |
|
Package idempotencywebhook provides provider-delivery deduplication for webhook messages with bounded payload fingerprints and durable ownership.
|
Package idempotencywebhook provides provider-delivery deduplication for webhook messages with bounded payload fingerprints and durable ownership. |
|
Package memory implements deterministic, process-local idempotency storage.
|
Package memory implements deterministic, process-local idempotency storage. |
|
Package postgres implements durable idempotency storage on PostgreSQL using pgx, transaction-scoped advisory locks, row locks, server time, and bounded retention cleanup.
|
Package postgres implements durable idempotency storage on PostgreSQL using pgx, transaction-scoped advisory locks, row locks, server time, and bounded retention cleanup. |