Documentation
¶
Overview ¶
Package cache provides a vendor-neutral cache contract and typed facade.
Cache operations use immutable Key[T] definitions. NewKey[T] declares an exact standalone identity, including an explicit empty string. Keys support Derive and retain their first error. TTL changes write policy, never identity; zero means no expiry.
NewNamespace[T] groups keys under a literal prefix, separator (default ":"), and inherited write TTL. Namespace.Key and Namespace.Subnamespace join bytes without escaping or normalization; a child separator override applies only to future joins. Namespaces are immutable and also support Derive. Applications retaining keys created with the former dot default must set NamespaceBuilder.Separator(".") explicitly. There is no fallback lookup or automatic migration: choosing the new default addresses different entries. Literal prefix bytes and standalone NewKey identities are unchanged. Typed-key operations validate cancellation before definition errors, encoding, or I/O. Lease acquisition instead validates its literal name and positive TTL before asking the driver; invalid lease arguments never perform driver I/O. Name explicitly reveals identity; ordinary formatting and logging redact it. Counters use Key[int64] and their distinct driver expiry rules. Locks and the raw Driver continue to use opaque strings. Increment, Decrement, and IncrementWithTTL reject mathematical results outside int64 atomically, without changing the target value or TTL. Missing targets remain absent. Subtraction accepts MinInt64 as a delta when its mathematical result fits; overflow error identities remain backend-native. Reserve physical names beginning with "lock:" for facade leases, including Remember's internal locks. This is a naming convention in the same driver key space, not backend isolation or escaping. An ordinary Put to "lock:report" can overwrite the lease named "report". Also reserve logical lease names beginning with "remember:" for Remember's cache-miss coordination.
CompareAndPut checks one existing raw Match and writes a typed destination atomically, using its key's TTL. The guard and destination may be the same key or different keys. Missing, expired, or changed guards reject the write; this is resource-level fencing, unlike an expiring cache lease. Read exact guard bytes through Driver.Get rather than re-encoding a decoded value. Matching bytes are not a monotonic version: restoring an earlier guard can permit an earlier writer again. Network errors may leave writes uncertain. Raw Match and Entry descriptors intentionally expose names and bytes; do not log them as if they were redacted Key definitions. Their slices are borrowed during a call, not retained afterwards. Ordinary JSON Put/Get detach cached data from caller values; Remember returns its computed value without cloning that application-owned result. Best-effort Remember lease release has an independent ten-second context budget and falls back to lease expiry on error.
New takes Builder policies for Telemetry and Name, invoked directly once in order on a fresh draft. Nil entries panic; expanded nil/empty slices supply no edits. Publication detaches settings, not the driver or telemetry service; retaining a draft does not permit reconfiguration. Nil telemetry resolves the process default per operation, including changes after construction.
Each facade cache operation emits one client span and the arkoren.cache.operations and arkoren.cache.operation.duration metrics when the selected telemetry (default: telemetry.Default) is started. Telemetry records the operation, store name (Builder.Name), and hit or miss, never keys, values, or error text. Driver and DriverAs are untraced accessors; Lease.Unlock and Lease.Extend are also untraced. Direct driver calls bypass facade instrumentation. A single facade operation can include multiple driver calls; native client transport retries are not additional facade operations or proof of exactly-once effects.
Example:
store := cache.New(driver, func(store *cache.Builder) {
store.Name("sessions")
})
_ = store.Driver()
version := cache.NewKey[string]("application.version", func(entry *cache.KeyBuilder) {
entry.TTL(time.Minute)
})
if err := store.Put(ctx, version, "v1"); err != nil {
return err
}
users := cache.NewNamespace[string]("users")
_ = users.Key("42") // users:42
Index ¶
- Constants
- Variables
- type Builder
- type BuilderFunc
- type Cache
- func (cache *Cache) Add[T any](ctx context.Context, key Key[T], value T) (added bool, err error)
- func (cache *Cache) CompareAndPut[T any](ctx context.Context, match Match, key Key[T], value T) (applied bool, err error)
- func (cache *Cache) Decrement(ctx context.Context, key Key[int64], delta int64) (value int64, err error)
- func (cache *Cache) Delete[T any](ctx context.Context, key Key[T]) (err error)
- func (cache *Cache) Driver() Driver
- func (cache *Cache) DriverAs[T any]() (T, bool)
- func (cache *Cache) Get[T any](ctx context.Context, key Key[T]) (res T, err error)
- func (cache *Cache) Has[T any](ctx context.Context, key Key[T]) (found bool, err error)
- func (cache *Cache) Increment(ctx context.Context, key Key[int64], delta int64) (value int64, err error)
- func (cache *Cache) IncrementWithTTL(ctx context.Context, key Key[int64], delta int64, ttl time.Duration) (value int64, remaining time.Duration, err error)
- func (cache *Cache) Lock(ctx context.Context, name string, ttl time.Duration) (*Lease, error)
- func (cache *Cache) Pull[T any](ctx context.Context, key Key[T]) (res T, err error)
- func (cache *Cache) Put[T any](ctx context.Context, key Key[T], value T) (err error)
- func (cache *Cache) Remember[T any](ctx context.Context, key Key[T], compute func() (T, error)) (res T, err error)
- func (cache *Cache) TTL[T any](ctx context.Context, key Key[T]) (remaining time.Duration, err error)
- func (cache *Cache) TryLock(ctx context.Context, name string, ttl time.Duration) (*Lease, bool, error)
- type Driver
- type Entry
- type Key
- func (key Key[T]) Derive(configure ...KeyBuilderFunc) Key[T]
- func (key Key[T]) Err() error
- func (key Key[T]) Format(state fmt.State, verb rune)
- func (key Key[T]) GoString() string
- func (key Key[T]) LogValue() slog.Value
- func (key Key[T]) Name() string
- func (key Key[T]) String() string
- func (key Key[T]) TTL() time.Duration
- type KeyBuilder
- type KeyBuilderFunc
- type Lease
- type Match
- type Namespace
- func (space Namespace[T]) Derive(configure ...NamespaceBuilderFunc) Namespace[T]
- func (space Namespace[T]) Err() error
- func (space Namespace[T]) Format(state fmt.State, verb rune)
- func (space Namespace[T]) GoString() string
- func (space Namespace[T]) Key(name string, configure ...KeyBuilderFunc) Key[T]
- func (space Namespace[T]) LogValue() slog.Value
- func (space Namespace[T]) Name() string
- func (space Namespace[T]) Separator() string
- func (space Namespace[T]) String() string
- func (space Namespace[T]) Subnamespace(name string, configure ...NamespaceBuilderFunc) Namespace[T]
- func (space Namespace[T]) TTL() time.Duration
- type NamespaceBuilder
- type NamespaceBuilderFunc
Examples ¶
Constants ¶
const DefaultName = "default"
DefaultName is the store name reported in telemetry when Builder.Name is empty.
Example:
name := cache.DefaultName
cache.New(driver, func(store *cache.Builder) {
store.Name(name)
})
Variables ¶
var ( // ErrNotFound is returned when a key does not exist in the cache. // Expired entries are also missing; callers can compute a replacement. // // Example: _ = errors.Is(err, cache.ErrNotFound) ErrNotFound = errors.New("cache key not found") // ErrUnsupported is returned when an operation is unsupported. // A caller must not emulate an unsupported atomic operation with unsafe I/O. // // Example: _ = errors.Is(err, cache.ErrUnsupported) ErrUnsupported = errors.New("cache unsupported operation") // ErrInvalidTTL is returned when a TTL is negative, or not positive for // operations that require an expiry. // // Example: _ = errors.Is(err, cache.ErrInvalidTTL) ErrInvalidTTL = errors.New("cache invalid ttl") // ErrInvalidKey identifies an invalid typed key or namespace definition. // It never includes the supplied identity; inspect definitions with Err. // // if errors.Is(key.Err(), cache.ErrInvalidKey) { // // Correct the declaration before using the key. // } ErrInvalidKey = errors.New("cache invalid key") // ErrEmptyLockName is returned when a lock name is empty. // Ordinary raw cache entries can still have an explicit empty identity. // // Example: _ = errors.Is(err, cache.ErrEmptyLockName) ErrEmptyLockName = errors.New("cache lock name cannot be empty") // ErrLockLost is returned by [Lease.Unlock] and [Lease.Extend] when the // lease no longer holds the lock: it expired, and possibly another holder // took it, before the call. The work done under the lease may have // overlapped with another holder's. // // Example: lost := errors.Is(err, cache.ErrLockLost) ErrLockLost = errors.New("cache lock lost") )
Functions ¶
This section is empty.
Types ¶
type Builder ¶
type Builder struct {
// contains filtered or unexported fields
}
Builder configures New before publication. Callbacks run directly once, synchronously in order on one fresh draft. Nil callback entries panic; expanded nil or empty callback slices supply no edits. Retaining the draft cannot change published settings. Methods return the same draft.
Example:
cache.New(driver, func(store *cache.Builder) {
store.Name("sessions")
})
func (*Builder) Name ¶
Name sets the store name reported as arkoren.cache.store.
This setting replaces the previous value on the construction draft. Services and loggers remain shared; publication detaches owned settings.
Example:
cache.New(driver, func(store *cache.Builder) {
store.Name("primary")
})
func (*Builder) Telemetry ¶
Telemetry sets the telemetry that records operations. Without it the cache follows telemetry.Default.
This setting replaces the previous value on the construction draft. Services and loggers remain shared; publication detaches owned settings.
Example:
cache.New(driver, func(store *cache.Builder) {
store.Telemetry(nil)
})
type BuilderFunc ¶
type BuilderFunc = func(*Builder)
BuilderFunc configures a Builder. Example:
var policy cache.BuilderFunc = func(store *cache.Builder) {
store.Name("sessions")
}
_ = cache.New(driver, policy)
type Cache ¶
type Cache struct {
// contains filtered or unexported fields
}
Cache provides typed JSON caching over a driver. Definition validation and telemetry belong to this facade; raw Driver access intentionally bypasses both. Construct one with New and share immutable keys.
Example:
store := cache.New(memorycache.New())
if err := store.Put(ctx, cache.NewKey[string]("version"), "v1"); err != nil {
return err
}
func New ¶
func New(driver Driver, configure ...BuilderFunc) *Cache
New creates a Cache that delegates storage to driver. Construction policies configure its telemetry. Each cache operation emits one client span and the arkoren.cache.operations and arkoren.cache.operation.duration metrics when telemetry is enabled. Disabled telemetry adds no allocations; JSON encoding and driver operations may still allocate. Keys and values are never recorded.
store := cache.New(driver, func(store *cache.Builder) {
store.Telemetry(app.Telemetry())
store.Name("sessions")
})
Construction callbacks run directly once, synchronously in order on one fresh draft. Nil entries panic; expanded nil or empty slices omit edits. Publication detaches owned settings, not live dependencies or captures.
func (*Cache) Add ¶
Add JSON-encodes value and stores it with key.TTL only when the entry is absent. An existing entry and its expiry remain unchanged. Validation precedes encoding and driver I/O; zero TTL means no expiry.
added, err := store.Add(ctx, cache.NewKey[string]("version"), "v1")
if err != nil {
return err
}
_ = added
func (*Cache) CompareAndPut ¶
func (cache *Cache) CompareAndPut[T any](ctx context.Context, match Match, key Key[T], value T) (applied bool, err error)
CompareAndPut JSON-encodes value and replaces key only while match still equals an existing, unexpired raw guard. Comparison and writing are one atomic driver operation, even when the guard and destination have different names. The destination's key.TTL replaces its expiry on success. A mismatch returns false, nil and changes nothing; it is not a cache miss or telemetry error. Cancellation and invalid destination definitions precede encoding and I/O. Encoding and driver errors are returned without an unconditional fallback. A transport error can leave the write's outcome unknown; do not interpret applied=false with a non-nil error as proof that no write occurred. A successful comparison is not an ever-increasing version: deleting and recreating the guard with identical bytes permits another match (the ABA problem). Use a nonrepeating application version when that distinction matters.
Example:
key := cache.NewKey[string]("document")
raw, err := store.Driver().Get(ctx, key.Name())
if err != nil {
return err
}
applied, err := store.CompareAndPut(ctx, cache.Match{Key: key.Name(), Value: raw}, key, "revised")
if err != nil {
return err
}
_ = applied // Reload or discard the edit if the guard changed.
Example ¶
ExampleCache_CompareAndPut shows the caller retaining an exact raw guard while replacing a typed value. It runs entirely in memory without time waits.
package main
import (
"context"
"fmt"
"arkoren.dev/core/service/cache"
"arkoren.dev/core/service/cache/memorycache"
)
func main() {
ctx := context.Background()
store := cache.New(memorycache.New())
key := cache.NewKey[string]("document")
if err := store.Put(ctx, key, "draft"); err != nil {
fmt.Println(err)
return
}
raw, err := store.Driver().Get(ctx, key.Name())
if err != nil {
fmt.Println(err)
return
}
applied, err := store.CompareAndPut(ctx, cache.Match{Key: key.Name(), Value: raw}, key, "revised")
if err != nil {
fmt.Println(err)
return
}
value, err := store.Get(ctx, key)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(applied, value)
}
Output: true revised
func (*Cache) Decrement ¶
func (cache *Cache) Decrement(ctx context.Context, key Key[int64], delta int64) (value int64, err error)
Decrement atomically decreases the int64 counter by delta, following Increment's expiry rules rather than the key's write TTL. Mathematical subtraction supports every int64 delta, including MinInt64. An out-of-range result returns a backend-native error without changing the target value or TTL or creating a missing target.
value, err := store.Decrement(ctx, cache.NewKey[int64]("visits"), 1)
if err != nil {
return err
}
_ = value
Example (MinimumDelta) ¶
ExampleCache_Decrement_minimumDelta demonstrates valid mathematical subtraction even when negating the delta would not fit int64.
package main
import (
"context"
"fmt"
"math"
"arkoren.dev/core/service/cache"
"arkoren.dev/core/service/cache/memorycache"
)
func main() {
ctx := context.Background()
store := cache.New(memorycache.New())
key := cache.NewKey[int64]("balance")
if err := store.Put(ctx, key, math.MinInt64); err != nil {
fmt.Println(err)
return
}
value, err := store.Decrement(ctx, key, math.MinInt64)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(value)
}
Output: 0
func (*Cache) Delete ¶
Delete removes the entry at key regardless of its value type or write policy. A missing entry succeeds. Cancellation and declaration errors precede I/O.
if err := store.Delete(ctx, cache.NewKey[string]("version")); err != nil {
return err
}
func (*Cache) Driver ¶
Driver returns the underlying Driver. Calls through it use raw bytes and do not emit facade telemetry.
Example: raw := store.Driver()
func (*Cache) DriverAs ¶
DriverAs returns the first driver of type T in the chain of the underlying driver. A driver that wraps another exposes it with an Unwrap() Driver method, so wrappers such as prefixing or tracing drivers do not hide the concrete driver.
Example:
driver, ok := store.DriverAs[*memorycache.Driver]()
if ok {
_ = driver // Access the concrete driver's lifecycle, if owned here.
}
func (*Cache) Get ¶
Get retrieves and JSON-decodes the value for a typed key. Cancellation and declaration errors are checked before driver I/O or decoding. A decode error leaves the stored entry unchanged and still counts as a read hit.
value, err := store.Get(ctx, cache.NewKey[string]("version"))
if err != nil {
return err
}
_ = value
func (*Cache) Has ¶
Has reports whether key exists and has not expired, without decoding its value. Its write policy has no effect. Failed reads have an unknown hit state.
found, err := store.Has(ctx, cache.NewKey[string]("version"))
if err != nil {
return err
}
_ = found
func (*Cache) Increment ¶
func (cache *Cache) Increment(ctx context.Context, key Key[int64], delta int64) (value int64, err error)
Increment atomically increases the int64 counter by delta. The key's write TTL is ignored: existing expiry is preserved; missing counters have no expiry. A mathematical result outside int64 returns the driver's native error without changing the target value or expiry. An absent target remains absent.
value, err := store.Increment(ctx, cache.NewKey[int64]("visits"), 1)
if err != nil {
return err
}
_ = value
Example (Overflow) ¶
ExampleCache_Increment_overflow handles a rejected counter update without relying on a portable overflow sentinel. The previously stored value remains.
package main
import (
"context"
"fmt"
"math"
"arkoren.dev/core/service/cache"
"arkoren.dev/core/service/cache/memorycache"
)
func main() {
ctx := context.Background()
store := cache.New(memorycache.New())
key := cache.NewKey[int64]("visits")
if err := store.Put(ctx, key, math.MaxInt64); err != nil {
fmt.Println(err)
return
}
_, err := store.Increment(ctx, key, 1)
fmt.Println("overflow rejected:", err != nil)
value, err := store.Get(ctx, key)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(value)
}
Output: overflow rejected: true 9223372036854775807
func (*Cache) IncrementWithTTL ¶
func (cache *Cache) IncrementWithTTL( ctx context.Context, key Key[int64], delta int64, ttl time.Duration, ) (value int64, remaining time.Duration, err error)
IncrementWithTTL atomically increases the integer value at key by delta, gives a counter without an expiry ttl, and returns the new value with the remaining lifetime. An existing expiry is kept. A ttl that is not positive fails with an error wrapping ErrInvalidTTL. A driver that cannot do this atomically returns ErrUnsupported. The explicit ttl controls counter initialization; key.TTL is ignored. An out-of-range mathematical sum returns a backend-native error without changing the target value or TTL. No TTL is added on overflow, and an absent target remains absent.
value, remaining, err := store.IncrementWithTTL(ctx, cache.NewKey[int64]("window"), 1, time.Minute)
if err != nil {
return err
}
_ = value
_ = remaining
func (*Cache) Lock ¶
Lock waits until the named lock is held or ctx ends, then returns the lease. The lock is exclusive, not reentrant, and expires ttl after it was taken unless Lease.Extend renews it. An empty name fails with ErrEmptyLockName and a ttl that is not positive with ErrInvalidTTL; when ctx ends first, Lock returns the context's error.
The cache has no blocking primitive, so Lock polls the driver with a jittered backoff that grows from 10ms to 250ms. The names live apart from cache keys: a lock called "report" never collides with a key "report".
Taking the lock, including the wait, is traced as a lock client span; releasing and extending it are not. Reserve physical "lock:" keys and logical "remember:" lease names for the facade. These prefixes do not create a separate backend namespace: ordinary writes to the completed lock key can overwrite a lease. Waiting can make several driver attempts; native transport retry policy belongs to the adapter.
Example:
lease, err := store.Lock(ctx, "nightly-report", time.Minute)
if err != nil {
return err
}
defer lease.Unlock(ctx)
func (*Cache) Pull ¶
Pull retrieves, removes, and JSON-decodes the value for key. The read and the removal are one atomic driver operation, so when several callers pull the same key exactly one receives the value, which makes Pull safe for one-time tokens. The value is removed even when it no longer decodes as T. Cancellation and declaration errors precede the destructive driver read.
value, err := store.Pull(ctx, cache.NewKey[string]("one-time-token"))
if err != nil {
return err
}
_ = value
func (*Cache) Put ¶
Put JSON-encodes value and replaces the entry using key.TTL as write policy. Zero TTL means no expiry. Cancellation and key errors precede encoding and I/O.
if err := store.Put(ctx, cache.NewKey[string]("version"), "v1"); err != nil {
return err
}
func (*Cache) Remember ¶
func (cache *Cache) Remember[T any]( ctx context.Context, key Key[T], compute func() (T, error), ) (res T, err error)
Remember retrieves key or computes and stores its value when it is absent. A stored value that no longer decodes as T, for example after T changed, is recomputed and overwritten. When storing the computed value fails, the value is still returned together with the storage error. It is traced as a single span whose arkoren.cache.hit tells whether compute ran.
Concurrent misses on one key do not stampede: the first takes a lock named after the key, the others wait for it (until ctx ends) and then read the value it stored, so compute runs once per miss. The lock is a lease of ten seconds; a compute that runs longer than that may overlap with another caller's. Hits never take the lock. Best-effort release uses an independent ten-second cleanup deadline, retaining context values but ignoring request cancellation. Cleanup errors are ignored because the lease expires; a backend must honor context cancellation for the deadline to bound I/O. This does not bound compute or renew the lease. The completed key name coordinates misses regardless of its declaration path. Writes use key.TTL; zero means no expiry. Validation precedes compute.
value, err := store.Remember(ctx, cache.NewKey[string]("version"), func() (string, error) {
return "v1", nil
})
if err != nil {
return err
}
_ = value
func (*Cache) TTL ¶
func (cache *Cache) TTL[T any](ctx context.Context, key Key[T]) (remaining time.Duration, err error)
TTL returns the stored entry's remaining lifetime, not key.TTL. Zero means the entry never expires; missing entries return ErrNotFound.
remaining, err := store.TTL(ctx, cache.NewKey[string]("version"))
if err != nil {
return err
}
_ = remaining
func (*Cache) TryLock ¶
func (cache *Cache) TryLock(ctx context.Context, name string, ttl time.Duration) (*Lease, bool, error)
TryLock invokes the driver once to take the named lock. It returns the lease and true when the lock is now held, and nil, false and a nil error when another holder has it. The name and ttl rules are those of Cache.Lock.
Use TryLock when only one process should do a piece of work and the others should skip it rather than queue behind it. The attempt is traced as a try_lock client span, whether or not it succeeded.
Example:
lease, held, err := store.TryLock(ctx, "nightly-report", time.Minute)
if err != nil || !held {
return err // Another instance is running the report.
}
defer lease.Unlock(ctx)
type Driver ¶
type Driver interface {
// Get returns the raw value stored for key, or an error wrapping
// ErrNotFound when key is missing or expired.
//
// Example:
//
// raw, err := driver.Get(ctx, "document")
// if err != nil {
// return err
// }
// _ = raw
Get(ctx context.Context, key string) ([]byte, error)
// Put stores value for key with ttl, replacing any existing value and TTL.
//
// Example:
//
// if err := driver.Put(ctx, "document", []byte("draft"), time.Minute); err != nil {
// return err
// }
Put(ctx context.Context, key string, value []byte, ttl time.Duration) error
// Delete removes the value stored for key. Deleting a missing key succeeds.
//
// Example:
//
// if err := driver.Delete(ctx, "document"); err != nil {
// return err
// }
Delete(ctx context.Context, key string) error
// Has reports whether key exists in the cache.
// It is an observation, not a guard for a later write.
//
// Example:
//
// found, err := driver.Has(ctx, "document")
// if err != nil {
// return err
// }
// _ = found
Has(ctx context.Context, key string) (bool, error)
// Add stores value only when key is absent or expired. It reports whether
// the value was stored and leaves an existing entry and its TTL untouched.
//
// Example:
//
// added, err := driver.Add(ctx, "receipt", []byte("claimed"), time.Hour)
// if err != nil {
// return err
// }
// _ = added
Add(ctx context.Context, key string, value []byte, ttl time.Duration) (bool, error)
// CompareAndPut atomically checks match and writes entry. See [Match] and
// [Entry]: the guard must exist, be unexpired, and equal match.Value byte
// for byte. A mismatch returns false, nil without changing either entry
// or its TTL. A match replaces entry.Key's value and TTL and returns true.
// The keys may be equal; otherwise the guard and its TTL remain unchanged.
// Negative entry.TTL fails even on a mismatch. Implementations must not
// emulate this with separate reads and writes; unsupported atomicity
// returns an error wrapping ErrUnsupported. Network errors can leave the
// outcome unknown, as with Put; callers must not fall back to Put.
//
// Example:
//
// raw, err := driver.Get(ctx, "document")
// if err != nil {
// return err
// }
// applied, err := driver.CompareAndPut(ctx,
// cache.Match{Key: "document", Value: raw},
// cache.Entry{Key: "document", Value: []byte("revised"), TTL: time.Minute})
// if err != nil {
// return err
// }
// _ = applied // false means the observed value no longer matches.
CompareAndPut(ctx context.Context, match Match, entry Entry) (bool, error)
// Increment increases the integer value stored for key by delta and
// returns the new value. A missing or expired key is created from zero
// with no expiry; an existing key keeps its remaining TTL. A stored value
// that is not a base-10 integer fails and is left unchanged. Integers are
// stored as their decimal text, so Get returns it.
// A mathematical result outside int64 must fail without changing the
// target's value or TTL; an absent target must remain absent. Overflow
// error identities are backend-native, not a portable cache sentinel.
//
// Example:
//
// count, err := driver.Increment(ctx, "views", 1)
// if err != nil {
// return err
// }
// _ = count
Increment(ctx context.Context, key string, delta int64) (int64, error)
// Decrement decreases the integer value stored for key by delta and
// returns the new value. It follows the same rules as Increment, so a
// missing key is created from zero and becomes -delta when representable.
// Subtraction must be checked without overflowing negation of delta:
// MinInt64 - MinInt64 is zero, but 0 - MinInt64 must fail. Overflow leaves
// the target value and TTL unchanged; a missing target remains absent.
// Its error identity remains backend-native, as with Increment.
//
// Example:
//
// count, err := driver.Decrement(ctx, "available", 1)
// if err != nil {
// return err
// }
// _ = count
Decrement(ctx context.Context, key string, delta int64) (int64, error)
// Pull atomically returns the raw value stored for key and removes it, so
// when several callers pull the same key exactly one receives the value
// and the others get an error wrapping ErrNotFound. It also returns that
// error when key is missing or expired.
//
// Example:
//
// raw, err := driver.Pull(ctx, "one-time-token")
// if err != nil {
// return err
// }
// _ = raw
Pull(ctx context.Context, key string) ([]byte, error)
// TryLock stores token under name, expiring after ttl, only when name
// is absent or expired. It reports whether the lock is now held by token
// and leaves an existing holder and its expiry untouched. The token
// identifies the holder for [Driver.Unlock] and [Driver.ExtendLock]
// and must be unique to it and non-empty. A ttl that is not positive
// fails with an error wrapping ErrInvalidTTL. Lock names share the key
// space with entries, so the facade prefixes them with "lock:".
//
// Example:
//
// token := rand.Text() // crypto/rand: unique to this attempt.
// held, err := driver.TryLock(ctx, "lock:report", token, time.Minute)
// if err != nil {
// return err
// }
// if held {
// _, err = driver.Unlock(ctx, "lock:report", token)
// }
// return err
TryLock(ctx context.Context, name, token string, ttl time.Duration) (bool, error)
// Unlock removes the lock name only while it still holds token, in
// one atomic step, and reports whether it did. It reports false, without
// error, when the lock expired or another holder took it, and then leaves
// that holder's lock untouched.
//
// Example:
//
// released, err := driver.Unlock(ctx, "lock:report", token)
// if err != nil {
// return err
// }
// _ = released
Unlock(ctx context.Context, name, token string) (bool, error)
// ExtendLock sets the expiry of the lock name to ttl from now, only while
// it still holds token, in one atomic step, and reports whether it did.
// It reports false, without error, when the lock expired or another
// holder took it. A ttl that is not positive fails with an error
// wrapping ErrInvalidTTL.
//
// Example:
//
// extended, err := driver.ExtendLock(ctx, "lock:report", token, time.Minute)
// if err != nil {
// return err
// }
// _ = extended
ExtendLock(ctx context.Context, name, token string, ttl time.Duration) (bool, error)
// IncrementWithTTL atomically increases key by delta and returns the new
// value with the remaining lifetime. A missing key is created from zero;
// a key without an expiry receives ttl; an existing expiry is kept. A ttl
// that is not positive fails with an error wrapping ErrInvalidTTL. The
// key therefore has an expiry after a successful write, which makes the
// operation suitable for rate-limit windows.
// A mathematical result outside int64 fails without changing the target's
// value or TTL, including leaving a persistent target without expiry.
// An absent target remains absent; overflow errors remain backend-native.
//
// Example:
//
// count, remaining, err := driver.IncrementWithTTL(ctx, "requests", 1, time.Minute)
// if err != nil {
// return err
// }
// _, _ = count, remaining
IncrementWithTTL(ctx context.Context, key string, delta int64, ttl time.Duration) (int64, time.Duration, error)
// TTL returns the remaining lifetime for key, or zero when the entry
// never expires. It returns an error wrapping ErrNotFound when key is
// missing or expired.
//
// Example:
//
// remaining, err := driver.TTL(ctx, "document")
// if err != nil {
// return err
// }
// _ = remaining
TTL(ctx context.Context, key string) (time.Duration, error)
}
Driver defines the raw-byte operations a cache implementation provides. A zero TTL means an entry does not expire; a negative TTL is invalid and fails with an error wrapping ErrInvalidTTL. Expired entries behave exactly like missing ones. Values handed to or returned from a driver are independent copies: callers may mutate them freely. Every operation fails with an error wrapping the context's error when ctx is already canceled, and then leaves the cache unchanged.
Example:
var driver cache.Driver = memorycache.New()
store := cache.New(driver)
if err := store.Put(ctx, cache.NewKey[string]("version"), "v1"); err != nil {
return err
}
type Entry ¶
type Entry struct {
// Key is the literal destination, which may equal the guard's key.
// The empty name is valid, just as it is for Driver.Put.
//
// Example: entry := cache.Entry{Key: "document", Value: []byte("revised")}
Key string
// Value contains the replacement bytes. Nil and empty slices both store
// an empty value; neither deletes the destination.
//
// Example: entry := cache.Entry{Key: "document", Value: []byte("revised")}
Value []byte
// TTL replaces the destination's expiry on success. Zero means no expiry;
// negative durations fail with ErrInvalidTTL before evaluating the guard.
//
// Example: entry := cache.Entry{Key: "document", TTL: time.Minute}
TTL time.Duration
}
Entry is the raw write performed by Driver.CompareAndPut after its guard matches. It groups a destination, bytes, and write lifetime without changing any naming or encoding rules. The call borrows Value, copies it into storage on success, and never modifies or retains the caller's slice afterwards.
Example:
entry := cache.Entry{Key: "document", Value: []byte("revised"), TTL: time.Minute}
applied, err := driver.CompareAndPut(ctx, match, entry)
if err != nil {
return err
}
_ = applied
type Key ¶
type Key[T any] struct { // contains filtered or unexported fields }
Key identifies a JSON value of T and its write lifetime. TTL is policy, not identity: different policies can address the same stored entry. The zero value is invalid; NewKey can deliberately address an empty identity. Formatting and logging redact identities; Name is the explicit escape hatch.
key := cache.NewKey[string]("application.version")
value, err := store.Get(ctx, key)
if err != nil {
return err
}
_ = value
func NewKey ¶
func NewKey[T any](exact string, configure ...KeyBuilderFunc) Key[T]
NewKey declares a standalone typed key with an exact physical name. It does not join, normalize, or validate the name's bytes; an explicit empty name is valid. The default TTL is zero (no expiry); a negative lifetime invalidates the key. Callbacks follow KeyBuilderFunc's publication rules.
key := cache.NewKey[string]("application.version", func(entry *cache.KeyBuilder) {
entry.TTL(time.Minute)
})
if err := store.Put(ctx, key, "v1"); err != nil {
return err
}
func (Key[T]) Derive ¶
func (key Key[T]) Derive(configure ...KeyBuilderFunc) Key[T]
Derive copies this key and applies write-policy edits. Identity is unchanged; inherited errors remain sticky and a zero Key cannot become valid through edits.
base := cache.NewKey[string]("version")
local := base.Derive(func(entry *cache.KeyBuilder) {
entry.TTL(time.Minute)
})
_ = local.Name() // version
func (Key[T]) Err ¶
Err returns the first declaration error or nil. A zero Key is invalid, unlike NewKey with an explicit empty name. Returned errors contain no identities.
key := cache.NewKey[string]("")
if err := key.Err(); err != nil {
return err
}
func (Key[T]) Format ¶
Format prevents any format verb from reflecting a key's private identity, including mismatched verbs such as %d. String quoting and hexadecimal verbs apply to the redacted marker; other verbs render that marker as a string. Width and precision never apply to the actual key name. Use Name explicitly when the physical identity is required.
Example:
key := cache.NewKey[string]("private-key")
text := fmt.Sprintf("%d", key) // cache.Key[redacted]
_ = text
func (Key[T]) GoString ¶
GoString redacts identity even in Go-syntax diagnostic formatting.
text := cache.NewKey[string]("secret").GoString()
_ = text // cache.Key[redacted]
func (Key[T]) LogValue ¶
LogValue redacts the complete key for structured loggers, not just its name.
value := cache.NewKey[string]("secret").LogValue()
_ = value.String() // cache.Key[redacted]
func (Key[T]) Name ¶
Name explicitly reveals the exact driver identity, even for invalid keys. Do not log it indiscriminately: ordinary formatting intentionally redacts it.
name := cache.NewKey[string]("version").Name() // version
_ = name
type KeyBuilder ¶
type KeyBuilder struct {
// contains filtered or unexported fields
}
KeyBuilder configures only the write lifetime of a key. It does not alter identity. A retained draft cannot change a published Key.
key := cache.NewKey[string]("version", func(entry *cache.KeyBuilder) {
entry.TTL(time.Minute)
})
_ = key.TTL()
func (*KeyBuilder) TTL ¶
func (entry *KeyBuilder) TTL(ttl time.Duration) *KeyBuilder
TTL sets a key's write lifetime without changing its identity. Zero disables expiration. Negative durations preserve the first error wrapping ErrInvalidTTL.
key := cache.NewKey[string]("version", func(entry *cache.KeyBuilder) {
entry.TTL(time.Minute)
})
_ = key.TTL()
type KeyBuilderFunc ¶
type KeyBuilderFunc = func(*KeyBuilder)
KeyBuilderFunc configures a key draft once, synchronously and in order, even when its definition is invalid. Nil entries panic; empty slices omit edits.
var policy cache.KeyBuilderFunc = func(entry *cache.KeyBuilder) {
entry.TTL(time.Minute)
}
_ = cache.NewKey[string]("version", policy)
type Lease ¶
type Lease struct {
// contains filtered or unexported fields
}
Lease is a held cache lock. It is returned by Cache.Lock and Cache.TryLock and is safe for concurrent use.
A lease is time-limited: the lock expires ttl after it was taken or last extended, whether or not the holder is still working, and another process may then take it. Long work must call Lease.Extend before the lease runs out, and must treat ErrLockLost as "someone else may have run concurrently". A cache lock is therefore a way to avoid duplicate work, not a mutual-exclusion guarantee for correctness; use a database lock (see the database service) or fencing in the protected resource for that.
Example:
lease, held, err := store.TryLock(ctx, "report", time.Minute)
if err != nil {
return err
}
if held {
return lease.Unlock(ctx)
}
func (*Lease) Extend ¶
Extend sets the lock to expire ttl from now, so long work can keep the lease alive. It returns ErrLockLost when the lease no longer holds the lock, because it expired first or was already released, and ErrInvalidTTL for a ttl that is not positive. Extending is not traced.
Example:
for _, batch := range batches {
if err := lease.Extend(ctx, time.Minute); err != nil {
return err // Stop: another process may be working too.
}
process(batch)
}
func (*Lease) Unlock ¶
Unlock releases the lock if the lease still holds it. It is safe to call more than once and from several goroutines: after a confirmed release or confirmed loss, later calls do nothing and return nil. It returns ErrLockLost, once, when the lease had expired and the lock was no longer this holder's, in which case another holder's lock is left untouched. A cache failure is returned as is and the call can be retried. Releasing is not traced. A backend error can leave release uncertain; a retry may report ErrLockLost because the earlier call released it. Neither that error nor successful cleanup proves the protected work never overlapped another holder.
Example:
if err := lease.Unlock(ctx); err != nil {
if !errors.Is(err, cache.ErrLockLost) {
return err // A backend failure remains retryable with a fresh context.
}
}
type Match ¶
type Match struct {
// Key is the literal guard name, including an explicitly empty name.
// It uses the same key space as Driver.Get; no prefix is added.
//
// Example: match := cache.Match{Key: "document", Value: raw}
Key string
// Value is the exact expected content, not a decoded or normalized value.
// A nil slice denotes empty content, never an absent guard.
//
// Example: match := cache.Match{Key: "document", Value: []byte("draft")}
Value []byte
}
Match describes the existing raw value required by Driver.CompareAndPut and Cache.CompareAndPut. It is an exact byte comparison, not JSON equality or a version generated by the cache. Read the original bytes with Driver.Get; decoding and re-encoding can change their representation. Missing and expired guards never match, including when Value is nil or empty. An existing empty value matches either nil or empty bytes. The slice is borrowed during a call; do not mutate it concurrently. It is neither modified nor retained afterwards.
Example:
raw, err := store.Driver().Get(ctx, "document")
if err != nil {
return err
}
match := cache.Match{Key: "document", Value: raw}
_ = match
type Namespace ¶
type Namespace[T any] struct { // contains filtered or unexported fields }
Namespace is an immutable literal prefix and write policy for typed keys. Names are opaque bytes: no escaping, splitting, or normalization occurs. A zero Namespace is invalid. Use Name explicitly when inspecting identities; ordinary formatting and structured logging redact them.
users := cache.NewNamespace[string]("users")
key := users.Key("42") // key.Name() == "users:42"
func NewNamespace ¶
func NewNamespace[T any](name string, configure ...NamespaceBuilderFunc) Namespace[T]
NewNamespace declares a nonempty literal prefix with separator ":" and no expiry by default. Invalid declarations retain ErrInvalidKey or ErrInvalidTTL for validation at use. Callback panics propagate, including nil callbacks. To retain identities created with the former dot default, explicitly set NamespaceBuilder.Separator("."). Existing entries are not renamed or read through aliases; prefix bytes and standalone NewKey identities stay unchanged.
users := cache.NewNamespace[string]("users")
_ = users.Key("42")
func (Namespace[T]) Derive ¶
func (space Namespace[T]) Derive(configure ...NamespaceBuilderFunc) Namespace[T]
Derive copies this namespace and applies edits without reapplying defaults. Inherited errors remain sticky. Callbacks follow NamespaceBuilderFunc's rules.
base := cache.NewNamespace[string]("users")
local := base.Derive(func(namespace *cache.NamespaceBuilder) {
namespace.TTL(time.Minute)
})
_ = local.Key("42")
func (Namespace[T]) Err ¶
Err returns the first declaration failure, or nil for a valid namespace. Empty and zero namespaces wrap ErrInvalidKey; errors never contain names.
if err := cache.NewNamespace[string]("").Err(); err != nil {
_ = errors.Is(err, cache.ErrInvalidKey)
}
func (Namespace[T]) Format ¶
Format prevents diagnostic formatting from reflecting a namespace's private prefix. Quoting and hexadecimal verbs format only its redacted marker; all other verbs render that marker as a string. Explicit Name access remains the intentional escape hatch, independently of formatting flags or precision.
Example:
space := cache.NewNamespace[string]("private-prefix")
text := fmt.Sprintf("%d", space) // cache.Namespace[redacted]
_ = text
func (Namespace[T]) GoString ¶
GoString redacts the namespace identity in Go-syntax diagnostic formatting.
text := cache.NewNamespace[string]("secret").GoString()
_ = text // cache.Namespace[redacted]
func (Namespace[T]) Key ¶
func (space Namespace[T]) Key(name string, configure ...KeyBuilderFunc) Key[T]
Key joins a nonempty literal child using this namespace's separator, inheriting TTL and sticky errors. Use NewKey for standalone or empty identities.
space := cache.NewNamespace[string]("users")
_ = space.Key("42") // users:42
func (Namespace[T]) LogValue ¶
LogValue redacts the complete definition when a structured logger inspects it.
value := cache.NewNamespace[string]("secret").LogValue()
_ = value.String() // cache.Namespace[redacted]
func (Namespace[T]) Name ¶
Name explicitly reveals the exact prefix, without validation or normalization. Avoid logging it: names may contain personal identifiers or secrets.
name := cache.NewNamespace[string]("users").Name() // users
_ = name
func (Namespace[T]) Separator ¶
Separator returns the literal join string used for future children, including an explicit empty string. It does not describe separators already in Name.
separator := cache.NewNamespace[string]("users").Separator() // :
_ = separator
func (Namespace[T]) String ¶
String redacts the namespace identity in ordinary diagnostic formatting.
text := cache.NewNamespace[string]("secret").String()
_ = text // cache.Namespace[redacted]
func (Namespace[T]) Subnamespace ¶
func (space Namespace[T]) Subnamespace(name string, configure ...NamespaceBuilderFunc) Namespace[T]
Subnamespace joins name using the parent's separator, then applies child edits. A child's Separator changes only its future children, never this join. TTL and errors are inherited. An empty child is invalid, not a singleton key.
base := cache.NewNamespace[string]("app")
child := base.Subnamespace("users", func(namespace *cache.NamespaceBuilder) {
namespace.Separator("/")
})
_ = child.Key("42") // app:users/42
type NamespaceBuilder ¶
type NamespaceBuilder struct {
// contains filtered or unexported fields
}
NamespaceBuilder configures a fresh namespace draft. Retaining and changing a draft after publication cannot change an existing definition. Methods return the draft for fluent configuration; the first invalid setting stays invalid.
space := cache.NewNamespace[string]("users", func(namespace *cache.NamespaceBuilder) {
namespace.Separator(":").TTL(time.Minute)
})
_ = space.Key("42")
func (*NamespaceBuilder) Separator ¶
func (namespace *NamespaceBuilder) Separator(separator string) *NamespaceBuilder
Separator sets the literal join string for future children. Empty means concatenation, not the default. Existing name bytes are never rewritten.
space := cache.NewNamespace[string]("users:", func(namespace *cache.NamespaceBuilder) {
namespace.Separator("")
})
_ = space.Key("42") // users:42
func (*NamespaceBuilder) TTL ¶
func (namespace *NamespaceBuilder) TTL(ttl time.Duration) *NamespaceBuilder
TTL sets the lifetime inherited by future keys and subnamespaces. Zero means no expiry; negative values preserve an error wrapping ErrInvalidTTL, even if a later callback supplies a valid lifetime.
space := cache.NewNamespace[string]("users", func(namespace *cache.NamespaceBuilder) {
namespace.TTL(time.Minute)
})
_ = space.TTL()
type NamespaceBuilderFunc ¶
type NamespaceBuilderFunc = func(*NamespaceBuilder)
NamespaceBuilderFunc configures one shared draft synchronously. Callbacks run once in order even for invalid declarations; nil entries panic. Expanded nil or empty slices omit edits. Publication copies the completed draft.
var policy cache.NamespaceBuilderFunc = func(namespace *cache.NamespaceBuilder) {
namespace.TTL(time.Minute)
}
_ = cache.NewNamespace[string]("users", policy)
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package cachetest provides a conformance suite for cache.Driver implementations, modeled on testing/slogtest and testing/fstest.
|
Package cachetest provides a conformance suite for cache.Driver implementations, modeled on testing/slogtest and testing/fstest. |
|
internal
|
|
|
lease
Package lease owns cache-lock polling and serialized holder state.
|
Package lease owns cache-lock polling and serialized holder state. |
|
Package memorycache provides an in-memory cache.Driver for tests, local development, and single-process applications.
|
Package memorycache provides an in-memory cache.Driver for tests, local development, and single-process applications. |
|
internal/storage
Package storage owns atomic cache values, counters, expiry and sweeping.
|
Package storage owns atomic cache values, counters, expiry and sweeping. |