Documentation
¶
Overview ¶
Package memcache is a modern memcache client for Go implementing the memcached meta protocol (version 1.6+).
The high-level Client is the recommended entry point. It provides multi-server support with consistent key distribution, per-server circuit breakers, and connection pooling with health checks:
servers := memcache.StaticServers("localhost:11211", "localhost:11212")
client := memcache.NewClient(servers, memcache.Config{
MaxConnsPerServer: 20,
})
defer client.Close()
_, _ = client.Set(ctx, "mykey", []byte("hello"))
item, _ := client.Get(ctx, "mykey")
Every Config field is optional: the zero value selects a documented default. The one worth sizing on day one is Config.MaxConnsPerServer, which caps how many operations can be in flight to a single server.
The runnable examples below cover the usual patterns: cache-aside, read-modify-write guarded by a CAS, counters, and mixed-command batches.
Key Distribution ¶
Keys are mapped to servers by a ServerSelector. The default, StableServerSelector, uses Rendezvous (Highest-Random-Weight) hashing: the result depends only on the set of addresses, not their order, so reordering the server list never remaps a key and adding or removing one server moves only ~1/N of keys. It hashes once per server per call.
OrderedServerSelector is the simpler, cheaper consistent-hash strategy — Jump Hash, one hash per call regardless of fleet size — but it is position-based: inserting or removing a server anywhere but the end remaps a large fraction of keys. Use it only for a static, stably ordered list.
Any function of the form func(key string, servers []Server) Server satisfies ServerSelector, so a weighted or zone-aware policy can be supplied through Config.ServerSelector.
Batching ¶
Batching comes in two forms.
The convenience operations — Commands.MultiGet, Commands.MultiSet and Commands.MultiDelete — cover the common case. Keys are grouped by server, each group is pipelined as a single round trip, groups run concurrently, and results are returned in the order the keys were passed.
Client.ExecuteBatch pipelines an arbitrary mix of meta commands — a get, a set and an increment in one round trip — by taking meta.Request values and returning the meta.Response values by position. Unlike the responses handed to a ResponseFunc, batch responses are owned by the caller, so their values can be kept without copying. Requests using the quiet flag are rejected: they suppress responses, which would break the by-position matching.
Circuit Breakers ¶
A circuit breaker can guard each server, so a failing server fails fast with ErrBreakerOpen instead of tying up connections. It is off unless BreakerConfig.Enabled is set; see BreakerConfig for the policy and its defaults.
Only transport-level errors count as failures — dial errors, socket I/O errors, and operations cut off by Config.OperationTimeout. A cache miss is a normal outcome. Errors the caller caused (a canceled context, an expired caller deadline, a request rejected by client-side validation) say nothing about server health and are not counted in either direction, so an impatient caller cannot trip a healthy server's breaker.
That attribution rule has a consequence worth designing for: a timeout counts against the server only when Config.OperationTimeout is the binding deadline. If every caller passes a context deadline at or below OperationTimeout, a hung server's timeouts are attributed to the caller and excluded — the breaker never opens, never sheds, and every operation keeps paying the full timeout. For a breaker to shed a hung server, callers need a budget looser than OperationTimeout (a context with no deadline works too: it is capped at OperationTimeout).
Timeouts ¶
An operation goes through up to three phases, each bounded differently:
- Pool checkout: waiting for a connection from a saturated pool is bounded only by the caller's context. There is deliberately no separate pool timeout knob; pass a context with a deadline. With context.Background() and a fully busy pool, an operation can wait indefinitely.
- Dial: establishing a new connection is bounded by Config.DialTimeout (which defaults to Config.OperationTimeout).
- I/O: socket reads and writes are bounded by the earlier of the context deadline and now plus Config.OperationTimeout, so even a caller with a far-future deadline cannot be stalled by a hung-but-connected server. The cap cannot be disabled — a non-positive OperationTimeout selects the default; set a large value when a long budget is genuinely needed.
Batch operations (MultiGet, MultiSet, MultiDelete, ExecuteBatch) apply the I/O bound per read, not per batch: the deadline is extended before each one so that a large batch is not cut short. A batch of n requests takes one window to write and n+1 to read (the responses plus the NoOp marker), so a slow server can hold it for far longer than a single Config.OperationTimeout. Only the context deadline caps the total, so always pass one on batch calls.
The intent is that a cache client fails fast: a timeout is a fast failure the caller is expected to tolerate. For reads that means falling back to the origin, like a miss. A timed-out write is different — it is ambiguous (the server may or may not have applied it), so callers that need certainty must verify or accept the ambiguity.
Checkout waits are visible in ConnPoolMetrics (AcquireWaitCount, AcquireWaitDuration), and an error during checkout is prefixed with "acquire:" to distinguish it from an I/O failure on the wire.
Errors ¶
A returned error means the operation did not complete: a transport failure, a breaker rejection, or a protocol error. It never means "miss" or "not stored" — those are outcomes, reported as a Status: in the Status field of Item, StoreResult and Counter, or returned directly by Delete and Touch. Status.OK is the one way to ask whether an operation took effect.
Failures are wrapped in an OpError carrying the operation, the key and the server it was routed to. The operation is named, not coded: "get", "touch", "add", … (the Op* constants), never the mg or ms that carried it. Branch on the cause, and read the OpError only for logging and metrics:
if errors.Is(err, memcache.ErrBreakerOpen) { /* server is shedding */ }
if errors.Is(err, context.DeadlineExceeded) { /* caller budget spent */ }
if opErr, ok := errors.AsType[*memcache.OpError](err); ok {
log.Printf("op=%s server=%s: %v", opErr.Op, opErr.Address, err)
}
Keys are deliberately absent from error messages — they often carry user identifiers and would give log lines unbounded cardinality — so read OpError.Key when the key is wanted.
A protocol error from the server is classified through the error types this package aliases from meta, so telling a server failure from a malformed request needs no import of the low-level package:
if _, ok := errors.AsType[*memcache.ServerError](err); ok {
// the server failed the operation (out of memory, internal error)
}
Building Blocks ¶
The client is assembled from smaller pieces that can be used on their own to build a custom client:
- The meta package serializes requests and parses responses for the memcached meta protocol.
- Connection wraps a single net.Conn and implements Executor.
- Commands holds the command logic (Get, Set, Delete, Increment, MultiGet, …) on top of any Executor.
Connection pooling is built into Client (backed by jackc/puddle) and is not a standalone building block.
Example ¶
Example shows the shape of a typical client: one client per process, shared by every goroutine, closed on shutdown.
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/pior/memcache"
)
func main() {
client := memcache.NewClient(
memcache.StaticServers("localhost:11211", "localhost:11212"),
memcache.Config{MaxConnsPerServer: 20},
)
defer client.Close()
ctx := context.Background()
// A store reports its outcome in the result; the error is reserved for
// transport failures.
if _, err := client.Set(ctx, "greeting", []byte("hello"),
memcache.StoreOptions{TTL: memcache.ExpiresIn(time.Hour)}); err != nil {
log.Printf("set: %v", err)
}
item, err := client.Get(ctx, "greeting")
if err != nil {
log.Printf("get: %v", err)
}
if item.Status.OK() {
fmt.Printf("%s\n", item.Value)
}
}
Output:
Index ¶
- Constants
- Variables
- type ActiveOp
- type BreakerConfig
- type BreakerMetrics
- type BreakerState
- type CAS
- type Client
- func (c *Client) Close()
- func (c *Client) Execute(ctx context.Context, req *meta.Request, fn ResponseFunc) (err error)
- func (c *Client) ExecuteBatch(ctx context.Context, reqs []*meta.Request) ([]*meta.Response, error)
- func (c *Client) FlushAll(ctx context.Context) error
- func (c *Client) PoolMetrics() []PoolMetrics
- func (c *Client) Stats(ctx context.Context, args ...string) ([]ServerStats, error)
- type ClientError
- type Commands
- func (c *Commands) Add(ctx context.Context, key string, value []byte, opts ...StoreOptions) (StoreResult, error)
- func (c *Commands) Append(ctx context.Context, key string, value []byte, opts ...ConcatOptions) (StoreResult, error)
- func (c *Commands) Decrement(ctx context.Context, key string, delta uint64, opts ...CounterOptions) (Counter, error)
- func (c *Commands) Delete(ctx context.Context, key string, opts ...DeleteOptions) (Status, error)
- func (c *Commands) Get(ctx context.Context, key string, opts ...GetOptions) (Item, error)
- func (c *Commands) Increment(ctx context.Context, key string, delta uint64, opts ...CounterOptions) (Counter, error)
- func (c *Commands) MultiDelete(ctx context.Context, keys []string) ([]Status, error)
- func (c *Commands) MultiGet(ctx context.Context, keys []string, opts ...GetOptions) ([]Item, error)
- func (c *Commands) MultiSet(ctx context.Context, items []SetItem) ([]StoreResult, error)
- func (c *Commands) Prepend(ctx context.Context, key string, value []byte, opts ...ConcatOptions) (StoreResult, error)
- func (c *Commands) Replace(ctx context.Context, key string, value []byte, opts ...StoreOptions) (StoreResult, error)
- func (c *Commands) Set(ctx context.Context, key string, value []byte, opts ...StoreOptions) (StoreResult, error)
- func (c *Commands) Touch(ctx context.Context, key string, ttl TTL) (Status, error)
- type ConcatOptions
- type Config
- type ConnPoolMetrics
- type Connection
- func (c *Connection) Close() error
- func (c *Connection) Execute(ctx context.Context, req *meta.Request, fn ResponseFunc) error
- func (c *Connection) ExecuteBatch(ctx context.Context, reqs []*meta.Request) ([]*meta.Response, error)
- func (c *Connection) ExecuteStats(ctx context.Context, args ...string) (map[string]string, error)
- func (c *Connection) Ping(ctx context.Context) error
- type ConnectionError
- type Counter
- type CounterOptions
- type DeleteOptions
- type Dialer
- type Executor
- type GenericError
- type GetOptions
- type InvalidRequestError
- type Item
- type Observer
- type OpError
- type OpInfo
- type OpResult
- type ParseError
- type PoolMetrics
- type Querier
- type ResponseFunc
- type Server
- type ServerError
- type ServerPool
- func (sp *ServerPool) Address() string
- func (sp *ServerPool) Close()
- func (sp *ServerPool) Execute(ctx context.Context, req *meta.Request, fn ResponseFunc) error
- func (sp *ServerPool) ExecuteBatch(ctx context.Context, reqs []*meta.Request) ([]*meta.Response, error)
- func (sp *ServerPool) ExecuteStats(ctx context.Context, args ...string) (map[string]string, error)
- func (sp *ServerPool) Metrics() PoolMetrics
- type ServerSelector
- type ServerStats
- type Servers
- type SetItem
- type StatsExecutor
- type Status
- type StoreOptions
- type StoreResult
- type TTL
Examples ¶
Constants ¶
const ( // DefaultBreakerTripMinRequests requires a meaningful sample before the // failure ratio is trusted; it also means very-low-traffic pools (under // one operation per second) never trip, which is fine: a breaker // protects against load piling onto a bad server, and there is no // pile-up without load. DefaultBreakerTripMinRequests = 10 // DefaultBreakerTripFailureRatio tolerates transient error bursts (a // rolling restart, a dropped connection) but trips well before total // failure. DefaultBreakerTripFailureRatio = 0.6 // DefaultBreakerTripWindow keeps the counts fresh: with the default // operation timeout of 1s, a short window bounds how much healthy // history a sudden outage has to overcome before the ratio trips. DefaultBreakerTripWindow = 10 * time.Second // DefaultBreakerOpenDuration sheds load long enough for a struggling // server to recover while retesting quickly: a false trip costs at most // a few seconds of fast-failing operations. DefaultBreakerOpenDuration = 5 * time.Second // DefaultBreakerHalfOpenMaxRequests closes the breaker after a single // successful probe: memcache operations are cheap and frequent, so one // probe per OpenDuration is signal enough, and a failed probe reopens // immediately anyway. DefaultBreakerHalfOpenMaxRequests = 1 )
Defaults for BreakerConfig. A field left at zero (or negative) gets its default.
const ( // DefaultMaxConnsPerServer comfortably serves a typical application's // concurrency against a sub-millisecond backend while keeping the // per-server socket footprint small; deployments with high per-server // concurrency should size it explicitly. DefaultMaxConnsPerServer = 10 // DefaultOperationTimeout is far above healthy memcached latencies // (sub-millisecond to low milliseconds), so it never constrains a working // server; it exists so that no configuration is ever unbounded — the // stress soak showed that a hung-but-connected server otherwise stalls // every operation whose context carries no deadline. Latency-sensitive // deployments should set a much lower OperationTimeout explicitly. DefaultOperationTimeout = time.Second // DefaultIdleConnCheckAfter sits well above the sub-millisecond idle gaps // of a pool under load (so the hot path pays nothing) and well below the // idle timeouts that reset a connection (server restart, LB/middlebox idle // timeout), so genuinely idle connections are the ones probed. DefaultIdleConnCheckAfter = time.Second // DefaultMaintenanceInterval keeps the background cost negligible while // bounding how long a departed server's connections can linger. The loop // always runs, so a zero-value Config already reaps departed-server pools // and enforces lifetime/idle limits on pools no traffic touches. DefaultMaintenanceInterval = 30 * time.Second )
Defaults for Config. A field left at zero (or negative, where the field's documentation does not give a negative value its own meaning) gets its default.
const ( OpGet = "get" OpSet = "set" OpAdd = "add" OpReplace = "replace" OpAppend = "append" OpPrepend = "prepend" OpTouch = "touch" OpDelete = "delete" OpIncrement = "increment" OpDecrement = "decrement" OpDebug = "debug" // OpBatch is the Op of pipelined batch executions. OpBatch = "batch" // OpStats is the Op of stats retrievals. OpStats = "stats" // OpFlushAll is the Op of all-server cache invalidation. OpFlushAll = "flush_all" )
Operation names. They identify the client operation in OpError.Op and in OpInfo.Op, and they are the vocabulary an Observer sees.
They name the operation, not the wire command that carries it: a Get and a Touch are both an "mg", and Set, Add, Replace, Append and Prepend are all an "ms", so a command code could not tell them apart. A request for a command this package does not model keeps its wire code as its name.
Variables ¶
var ( // ErrClientClosed is returned by operations issued after Client.Close. ErrClientClosed = errors.New("memcache: client is closed") // ErrNoServers is returned when the client has no server to talk to. ErrNoServers = errors.New("memcache: no servers available") // ErrPoolClosed is returned by an operation that reaches a server's pool // after that pool was closed. Two different races produce it: // // - the operation held the pool when Client.Close closed it. An // operation that starts after Close gets ErrClientClosed instead, so // detecting shutdown means checking both sentinels. // - the operation held the pool of a server that left the server set // and was then reaped by the maintenance loop (see // Config.MaintenanceInterval). Nothing is shutting down here, and a // retry routes to a server still in the set. ErrPoolClosed = errors.New("memcache: pool is closed") // ErrBreakerOpen is returned when the server's circuit breaker rejects // the operation without attempting it: the breaker is open after too // many recent failures, or it is half-open and its probe quota is // already in flight. See Config.Breaker. ErrBreakerOpen = errors.New("memcache: circuit breaker open") )
Sentinel errors returned by the client. Check them with errors.Is; they may be wrapped with additional context.
var NoTTL = TTL{}
NoTTL is the zero TTL: the item never expires (it persists until evicted).
Functions ¶
This section is empty.
Types ¶
type ActiveOp ¶
type ActiveOp interface {
End(OpResult)
}
ActiveOp is an in-flight operation returned by Observer.StartOp. End is called exactly once with the operation's outcome, closing the span and/or recording metrics. Implementations must be safe for concurrent use.
type BreakerConfig ¶
type BreakerConfig struct {
// Enabled turns the circuit breaker on. When false (the zero value) no
// breaker is created, every operation is always attempted, and the other
// fields are ignored.
Enabled bool
// TripMinRequests is the minimum number of operations that must have been
// observed within TripWindow before the breaker can trip. Below this
// volume the failure ratio is statistically meaningless (one failure out
// of two operations is not an outage), so the breaker stays closed
// regardless of the ratio.
// The default is DefaultBreakerTripMinRequests (10).
TripMinRequests int
// TripFailureRatio is the fraction of failed operations at which the
// breaker trips. Each time an operation fails, the breaker looks at the
// operations observed within TripWindow and opens when at least
// TripMinRequests were observed and failures/operations reaches this
// ratio. Must be in (0, 1]: 0.6 means "open when 60% of recent
// operations failed", 1 means "open only when every recent operation
// failed".
// The default is DefaultBreakerTripFailureRatio (0.6).
TripFailureRatio float64
// TripWindow is how far back the trip condition looks: the operation and
// failure counts cover approximately the last TripWindow (a rolling
// window), so old failures age out and cannot combine with fresh ones to
// trip the breaker long after a blip.
// The default is DefaultBreakerTripWindow (10s).
TripWindow time.Duration
// OpenDuration is how long the breaker stays open after tripping. While
// open, every operation to the server fails immediately with
// ErrBreakerOpen, without dialing or using a connection. When it
// elapses, the breaker becomes half-open and probes the server.
// The default is DefaultBreakerOpenDuration (5s).
OpenDuration time.Duration
// HalfOpenMaxRequests is the number of operations let through while the
// breaker is half-open; any further operations fail with ErrBreakerOpen
// until the probes complete. A single failed probe reopens the breaker
// for another OpenDuration; once this many probes have succeeded the
// breaker closes.
// The default is DefaultBreakerHalfOpenMaxRequests (1).
HalfOpenMaxRequests int
// OnStateChange, if set, is called whenever a server's breaker changes
// state. address is the server's host:port; from and to are the states it
// moved between (never BreakerDisabled).
// It is called synchronously from the operation's goroutine while the
// breaker's internal lock is held: it must return quickly and must not
// perform memcache operations. Use it for logging and metrics.
OnStateChange func(address string, from, to BreakerState)
}
BreakerConfig configures the circuit breaker guarding each server.
Every server gets its own breaker. While the breaker is closed, operations flow normally and their outcomes are counted. When failures dominate (see TripMinRequests and TripFailureRatio), the breaker opens: operations to that server fail immediately with ErrBreakerOpen instead of tying up connections on a server that is misbehaving. After OpenDuration the breaker becomes half-open and lets HalfOpenMaxRequests probe operations through: if a probe fails the breaker reopens, once they all succeed it closes.
Only transport-level errors count as failures: dial errors, socket I/O errors, and operations cut off by the operation timeout (Config.OperationTimeout). A cache miss is a normal outcome, not a failure. Errors caused by the caller — a canceled context, an expired caller deadline, a request rejected by client-side validation — say nothing about server health and are not counted at all, in either direction.
Callers need a budget looser than OperationTimeout ¶
Because a caller's own deadline is excluded, a timeout counts against the server only when Config.OperationTimeout is the binding deadline. If every caller passes a context deadline at or below OperationTimeout, a hung server's timeouts are attributed to the caller and excluded: the breaker never opens, never sheds, and every operation keeps paying the full timeout. The breaker looks configured but is inert for exactly the failure it exists to contain.
So size the two together. Set OperationTimeout to what a healthy server should never exceed, and let callers pass a looser budget (or no deadline at all, which is capped at OperationTimeout anyway).
Choosing the policy ¶
The zero value — Enabled alone — is the usual choice; the defaults below come from the stress and chaos runs. When tuning, the trade-off is reaction speed against sensitivity to noise: a shorter TripWindow and a lower TripMinRequests react faster but trip on a blip, a higher TripFailureRatio tolerates partial failure, and OpenDuration is what a false trip costs (that much fast-failing traffic before the next probe).
type BreakerMetrics ¶
type BreakerMetrics struct {
State BreakerState
Requests int
TotalSuccesses int
TotalFailures int
ConsecutiveSuccesses int
ConsecutiveFailures int
}
BreakerMetrics is a snapshot of a server's circuit breaker. When no breaker is configured, State is BreakerDisabled and the counts are zero.
Unlike the lifetime counters in ConnPoolMetrics, these counts are not monotonic: they cover only the operations observed within the current TripWindow and drop back as that window rolls forward. Export them as gauges, never as Prometheus counters.
type BreakerState ¶
type BreakerState int
BreakerState is the state of one server's circuit breaker.
const ( // BreakerDisabled means no breaker is configured for the server: every // operation is always attempted. It is the zero value. BreakerDisabled BreakerState = iota BreakerClosed // operations flow normally and are counted BreakerHalfOpen // probing the server with a limited number of operations BreakerOpen // shedding: operations fail fast with ErrBreakerOpen )
func (BreakerState) String ¶
func (s BreakerState) String() string
type CAS ¶
type CAS uint64
CAS is a compare-and-swap token, memcached's optimistic-concurrency handle (its ETag). A CAS is obtained from Get (or any operation that returns one) and supplied back through an options struct to make a write conditional. The zero value means "no precondition": stores apply unconditionally and deletes ignore CAS.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a memcache client that implements the Querier interface using a connection pool.
It embeds Commands, so the operations of Querier — Get, Set, Delete, Increment, MultiGet, … — are called directly on the client. The embedded value is not exported: a client's command surface is fixed at construction.
func NewClient ¶
NewClient creates a new memcache client with the given servers and configuration. For a single server, use: NewClient(StaticServers("host:port"), config)
Example ¶
A client with defaults: every Config field is optional. The one worth sizing on day one is MaxConnsPerServer, which caps how many operations can be in flight to a single server; callers queue once it is full, bounded only by their context.
package main
import (
"context"
"github.com/pior/memcache"
)
func main() {
client := memcache.NewClient(
memcache.StaticServers("localhost:11211", "localhost:11212"),
memcache.Config{MaxConnsPerServer: 20},
)
defer client.Close()
_, _ = client.Set(context.Background(), "user:123", []byte("John"))
}
Output:
Example (Breaker) ¶
Circuit breakers are per server and off by default. Enabling them with the zero-value policy is the usual choice: trip when 60% of the last 10s failed (at least 10 operations observed), shed for 5s, then probe.
Only transport failures count — dial errors, socket errors, and operations cut off by Config.OperationTimeout. Misses are normal, and caller-caused errors (a canceled context, an expired caller deadline) are excluded so an impatient caller cannot trip a healthy server.
That exclusion has a consequence to design for: for the breaker to shed a hung server, Config.OperationTimeout must be the binding deadline. If every caller passes a context deadline at or below OperationTimeout, the timeouts are attributed to the caller, the breaker never opens, and every operation keeps paying the full timeout. Give callers a looser budget — or no deadline at all, which is capped at OperationTimeout.
package main
import (
"context"
"time"
"github.com/pior/memcache"
)
func main() {
client := memcache.NewClient(
memcache.StaticServers("localhost:11211", "localhost:11212"),
memcache.Config{
MaxConnsPerServer: 20,
OperationTimeout: 100 * time.Millisecond,
Breaker: memcache.BreakerConfig{Enabled: true},
},
)
defer client.Close()
// Callers get 500ms, well above OperationTimeout, so a hung server's
// timeouts are attributed to the server and open its breaker.
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
_, _ = client.Set(ctx, "user:123", []byte("John"))
}
Output:
Example (BreakerTuning) ¶
Tuning the breaker: every knob is a value on BreakerConfig, so the policy is explicit and there is no third-party type in the API.
The trade-off is how fast it reacts against how easily a blip trips it. A shorter TripWindow and a lower TripMinRequests react faster but trip on noise; a higher TripFailureRatio tolerates partial failure. OpenDuration is what a false trip costs: that much fast-failing traffic before the next probe.
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/pior/memcache"
)
func main() {
client := memcache.NewClient(
memcache.StaticServers("localhost:11211", "localhost:11212"),
memcache.Config{
MaxConnsPerServer: 20,
Breaker: memcache.BreakerConfig{
Enabled: true,
TripMinRequests: 20, // ignore the ratio below this volume
TripFailureRatio: 0.5, // trip at 50% failures
TripWindow: 5 * time.Second, // "recent" means the last 5s
OpenDuration: 2 * time.Second, // shed for 2s, then probe
HalfOpenMaxRequests: 3, // 3 successful probes to close
OnStateChange: func(address string, from, to memcache.BreakerState) {
log.Printf("memcache breaker %s: %s -> %s", address, from, to)
},
},
},
)
defer client.Close()
_, _ = client.Set(context.Background(), "user:123", []byte("John"))
// Breaker state and counts are exported per server, alongside the pool
// metrics, for dashboards and alerting.
for _, m := range client.PoolMetrics() {
fmt.Printf("%s: breaker=%s requests=%d failures=%d conns=%d/%d\n",
m.Address, m.Breaker.State,
m.Breaker.Requests, m.Breaker.TotalFailures,
m.Conns.ActiveConns, m.Conns.TotalConns)
}
}
Output:
Example (Tls) ¶
Connecting to TLS-enabled servers (memcached running with --enable-ssl, e.g. AWS ElastiCache with in-transit encryption).
A *tls.Dialer satisfies memcache.Dialer, so it plugs straight into Config. One dialer covers every server in the set: crypto/tls fills ServerName from each dial address, so each server is verified against its own hostname.
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"os"
"time"
"github.com/pior/memcache"
)
func main() {
// Trust the CA that signed the servers' certificates. Omit RootCAs to use
// the system trust store.
caPEM, err := os.ReadFile("/etc/ssl/memcache-ca.pem")
if err != nil {
panic(err)
}
roots := x509.NewCertPool()
roots.AppendCertsFromPEM(caPEM)
client := memcache.NewClient(
memcache.StaticServers(
"cache-001.example.com:11211",
"cache-002.example.com:11211",
),
memcache.Config{
MaxConnsPerServer: 20,
Dialer: &tls.Dialer{
Config: &tls.Config{RootCAs: roots},
},
// A TLS handshake costs more than a plain dial.
DialTimeout: 2 * time.Second,
},
)
defer client.Close()
_, _ = client.Set(context.Background(), "user:123", []byte("John"))
}
Output:
func (*Client) Close ¶
func (c *Client) Close()
Close closes the client and destroys all connections in all pools. It is safe to call multiple times. Operations issued after Close fail. Close stops the maintenance loop, waits for any in-flight pass to finish, then blocks until in-flight operations return their connections to the pools.
func (*Client) Execute ¶
Execute implements the Executor interface with automatic server routing. See ResponseFunc for the contract: the response is only valid during the fn call.
func (*Client) ExecuteBatch ¶
ExecuteBatch executes multiple requests with automatic server routing. Requests are grouped by server and executed concurrently using pipelined requests. Returns responses in the same order as requests.
Responses are matched to requests by position, which requires every request to produce a response: requests using the quiet flag are rejected. Use Connection.ExecuteBatch directly for quiet pipelining.
If any server batch fails, an error is returned and the responses are discarded, including those from servers that succeeded.
Example ¶
ExecuteBatch is the general form of batching: any mix of meta commands in one pipelined round trip per server. This is what MultiGet and friends are built on, and what the legacy text protocol cannot express.
Requests are built with the meta package and responses come back by position: resps[i] answers reqs[i]. Unlike the response handed to a ResponseFunc, batch responses are owned by the caller, so their values can be kept without copying. Quiet requests are rejected — they suppress responses, which would break the positional matching.
package main
import (
"context"
"fmt"
"log"
"strconv"
"time"
"github.com/pior/memcache"
"github.com/pior/memcache/meta"
)
func main() {
client := memcache.NewClient(
memcache.StaticServers("localhost:11211"),
memcache.Config{MaxConnsPerServer: 20},
)
defer client.Close()
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
reqs := []*meta.Request{
// Read a profile, asking for the value and its CAS token.
meta.NewRequest(meta.CmdGet, "profile:42", nil).
AddReturnValue().
AddReturnCAS(),
// Refresh a session, expiring in 5 minutes.
meta.NewRequest(meta.CmdSet, "session:42", []byte("live")).
AddTTL(300),
// Bump a hit counter, creating it on miss with a 1 hour TTL.
meta.NewRequest(meta.CmdArithmetic, "hits:42", nil).
AddModeIncrement().
AddDelta(1).
AddInitialValue(0).
AddVivify(3600).
AddReturnValue(),
}
resps, err := client.ExecuteBatch(ctx, reqs)
if err != nil {
log.Printf("batch: %v", err)
return
}
// A per-request protocol error is carried on the response, not returned.
for i, resp := range resps {
if resp.HasError() {
log.Printf("request %d: %v", i, resp.Error)
}
}
if profile := resps[0]; profile.HasValue() {
cas, _ := profile.CAS()
fmt.Printf("profile=%s cas=%d\n", profile.Data, cas)
}
fmt.Printf("session stored: %t\n", resps[1].IsSuccess())
if hits := resps[2]; hits.HasValue() {
n, _ := strconv.ParseUint(string(hits.Data), 10, 64)
fmt.Printf("hits=%d\n", n)
}
}
Output:
func (*Client) FlushAll ¶
FlushAll invalidates all items on every currently configured server. Servers are flushed concurrently; a failure on any server is preserved and all failures are combined with errors.Join.
func (*Client) PoolMetrics ¶
func (c *Client) PoolMetrics() []PoolMetrics
PoolMetrics returns connection-pool metrics for all server pools.
func (*Client) Stats ¶
Stats retrieves statistics from all memcache servers. Sends a stats request to each server and collects the responses. Returns a slice of ServerStats, one per server. Individual server errors are returned in ServerStats.Error, not as a Go error.
args are the sub-command tokens sent after "stats", space-separated and verbatim: none for the general statistics, one for a named sub-command ("items", "slabs", "settings"), several for the ones that take parameters ("cachedump", "1", "100").
type ClientError ¶
type ClientError = meta.ClientError
ClientError is a CLIENT_ERROR reply: the server rejected the request as malformed. The client closes the connection, since the protocol state is then undefined.
type Commands ¶
type Commands struct {
// contains filtered or unexported fields
}
Commands provides the memcache operations, single-key and multi-key, on top of an Executor. It can be used independently with a custom Executor, or embedded in Client for full resilience features. It covers the Querier interface.
func NewCommands ¶
NewCommands creates a new Commands instance with the given executor.
Example ¶
Building a minimal client from the low-level pieces: Commands runs the command logic on top of any Executor, here a single unpooled Connection. There is no pool, no server selection and no breaker — that is what Client adds.
package main
import (
"context"
"fmt"
"net"
"time"
"github.com/pior/memcache"
)
func main() {
conn, err := net.Dial("tcp", "localhost:11211")
if err != nil {
panic(err)
}
defer conn.Close()
commands := memcache.NewCommands(memcache.NewConnection(conn, time.Second))
ctx := context.Background()
_, _ = commands.Set(ctx, "mykey", []byte("value"))
item, _ := commands.Get(ctx, "mykey")
if item.Status.OK() {
fmt.Printf("Value: %s\n", item.Value)
}
}
Output:
func (*Commands) Add ¶
func (c *Commands) Add(ctx context.Context, key string, value []byte, opts ...StoreOptions) (StoreResult, error)
Add stores value at key only if the key does not already exist. When it does, the result reports StatusExists.
func (*Commands) Append ¶
func (c *Commands) Append(ctx context.Context, key string, value []byte, opts ...ConcatOptions) (StoreResult, error)
Append adds value after the existing value without changing its TTL. When the key is absent the result reports StatusNotFound, unless ConcatOptions.CreateOnMiss is set, in which case the key is created and the result reports StatusApplied.
func (*Commands) Decrement ¶
func (c *Commands) Decrement(ctx context.Context, key string, delta uint64, opts ...CounterOptions) (Counter, error)
Decrement decrements a counter key by delta, stopping at zero. By default a missing key reports StatusNotFound; set CounterOptions.Create to create it on miss. NoTTL means infinite TTL.
func (*Commands) Delete ¶
Delete removes the item at key. A DeleteOptions with a non-zero CAS deletes only if the CAS matches; a mismatch reports StatusCASMismatch and a missing key reports StatusNotFound.
func (*Commands) Get ¶
Get retrieves a single item from memcache, populating Value, Flags, and CAS. A GetOptions with a non-zero TTL also touches the item, updating its expiration while reading (get-and-touch).
func (*Commands) Increment ¶
func (c *Commands) Increment(ctx context.Context, key string, delta uint64, opts ...CounterOptions) (Counter, error)
Increment increments a counter key by delta. By default a missing key reports StatusNotFound; set CounterOptions.Create to create it on miss, seeded with CounterOptions.Initial. NoTTL means infinite TTL.
func (*Commands) MultiDelete ¶
MultiDelete removes multiple items in a single batch operation. Returns one Status per key, in order: StatusApplied, or StatusNotFound for a key that did not exist.
func (*Commands) MultiGet ¶
MultiGet retrieves multiple items in a single batch operation, populating Value, Flags and CAS. Returns items in the same order as the keys, with Status StatusNotFound for missing items. The options apply to every key: a non-zero TTL touches each item read (get-and-touch).
Config.OperationTimeout bounds each response read, not the whole batch, so a slow server can hold the operation for a multiple of it (see the Timeouts section in the package documentation). Pass a context with a deadline to cap the total.
func (*Commands) MultiSet ¶
MultiSet stores multiple items in a single batch operation. Returns one StoreResult per item, in order; an item that did not land (CAS mismatch) is reported there, not as an error. The error is reserved for transport failures and protocol errors, in which case no results are returned.
func (*Commands) Prepend ¶
func (c *Commands) Prepend(ctx context.Context, key string, value []byte, opts ...ConcatOptions) (StoreResult, error)
Prepend adds value before the existing value without changing its TTL. When the key is absent the result reports StatusNotFound, unless ConcatOptions.CreateOnMiss is set, in which case the key is created.
func (*Commands) Replace ¶
func (c *Commands) Replace(ctx context.Context, key string, value []byte, opts ...StoreOptions) (StoreResult, error)
Replace stores value at key only if the key already exists. When it does not, the result reports StatusNotFound.
func (*Commands) Set ¶
func (c *Commands) Set(ctx context.Context, key string, value []byte, opts ...StoreOptions) (StoreResult, error)
Set stores value at key. A StoreOptions with a non-zero CAS makes the write conditional: it applies only if the stored item's CAS matches, otherwise the result reports StatusCASMismatch.
type ConcatOptions ¶
type ConcatOptions struct {
CAS CAS
CreateOnMiss bool // create the key when it is absent (memcached's autovivify)
TTL TTL // expiration of the created item; ignored unless CreateOnMiss
Flags uint32 // client flags of the created item; ignored unless CreateOnMiss
}
ConcatOptions modifies Append and Prepend. The zero value requires the key to exist; a missing key reports StatusNotFound.
type Config ¶
type Config struct {
// MaxConnsPerServer is the maximum number of connections the client opens
// to a single server. It is therefore also the number of operations that
// can be in flight to that server at once: past it, callers wait for a
// connection to come back.
//
// That wait is bounded only by the caller's context (see the Timeouts
// section in the package documentation), so a pool sized below peak
// concurrency plus deadline-less contexts queues waiters without shedding.
// Size it against peak per-server concurrency, and pass contexts with
// deadlines.
// The default is DefaultMaxConnsPerServer (10).
MaxConnsPerServer int
// MaxConnLifetime is the maximum duration a connection can be reused.
// Enforced when a connection is checked out of the pool, when it is
// returned after an operation, and by the maintenance loop for idle
// connections.
//
// Useful values are seconds to minutes. Below a second, connections are
// recycled faster than they are useful and most operations pay a fresh
// dial — which collapses throughput when the dial includes a TLS
// handshake.
// The default is no limit.
MaxConnLifetime time.Duration
// MaxConnIdleTime is the maximum duration a connection can be idle before
// being closed. Enforced when a connection is checked out of the pool and
// by the maintenance loop.
//
// Note that checkout-time enforcement needs traffic to act: a pool that
// receives no operations at all only shrinks when the background
// maintenance loop prunes it. That loop always runs; see
// MaintenanceInterval for how its period affects how promptly this happens.
//
// Set it below MaxConnLifetime, or it never fires: the lifetime limit
// always preempts it.
// The default is no limit.
MaxConnIdleTime time.Duration
// MaintenanceInterval is how often the background maintenance loop runs.
// The loop always runs; this only tunes its period.
// The default is DefaultMaintenanceInterval (30s).
//
// Each pass, for every pool:
//
// - pings idle connections and closes any that are broken or past
// MaxConnLifetime/MaxConnIdleTime, so limits are enforced even on a pool
// no operations are flowing through (checkout/return also enforce them,
// but that needs traffic to act);
// - reaps the pool of any server absent from the server set for
// reapAfterMissedPasses consecutive passes, so a client against a
// dynamic server set (e.g. Kubernetes endpoints) does not accumulate a
// pool — with its idle connections and its circuit breaker — for every
// address it ever routed to.
//
// The interval is the resolution of this maintenance, not a deadline, so a
// longer period trades promptness for a bit less background work: an idle
// connection a server or middlebox dropped is not noticed until the next
// pass (or the next operation routed to it); a connection past its
// lifetime/idle limit on an otherwise idle pool is pruned that much later;
// and a departed server's pool — its connections and breaker — survives up
// to reapAfterMissedPasses of these intervals before being reclaimed. Pick
// the period against how fast your server set churns and how long you can
// tolerate a dropped idle connection lingering; the default is a good
// starting point. A period long enough to matter against a churning set
// (minutes) lets departed pools pile up in the meantime, so prefer the
// default there rather than disabling maintenance by stretching the period.
MaintenanceInterval time.Duration
// IdleConnCheckAfter controls the on-acquire liveness check: when a
// connection that has been idle at least this long is checked out of the
// pool, it is verified with a cheap non-blocking probe and transparently
// replaced if the server, a load balancer, or a middlebox closed it while
// it sat idle. This keeps a rolling restart or an idle-timed-out flow from
// surfacing as a burst of failed operations on the next request.
//
// Freshly established and actively cycling connections (idle less than the
// threshold) are trusted without a syscall, so the hot path is unaffected.
// The probe cannot see through TLS, so TLS connections are always trusted
// and rely on the next operation to detect a dead peer.
//
// The default is DefaultIdleConnCheckAfter (1s); a negative value disables
// the check.
IdleConnCheckAfter time.Duration
// OperationTimeout bounds the read/write of a single memcache operation.
// It acts as an upper bound on every operation: the effective deadline is
// the earlier of the context deadline and now+OperationTimeout. A context
// deadline sooner than OperationTimeout still wins; a later one (or a
// context with no deadline) is capped at OperationTimeout. This ensures a
// long-lived context (e.g. a request- or job-scoped one) cannot leave an
// operation unbounded, so a hung-but-connected server fails fast instead of
// stalling the client.
//
// OperationTimeout bounds socket I/O only. It does not bound waiting for a
// connection from a saturated pool: that wait is bounded solely by the
// caller's context, so pass contexts with deadlines (see the Timeouts
// section in the package documentation).
//
// The cap cannot be disabled, so the client is never left unbounded by a
// configuration mistake. Set a large explicit value for operations that
// legitimately need a long budget; 100ms-1s suits most latency
// requirements.
//
// With Breaker enabled, OperationTimeout must be the binding deadline for
// a hung server's timeouts to be attributed to that server and open its
// breaker; callers whose own deadline is at or below it own the timeout
// instead. See BreakerConfig.
// The default is DefaultOperationTimeout (1s).
OperationTimeout time.Duration
// DialTimeout bounds establishing a new connection, including the TCP
// handshake and the TLS handshake if applicable. Set it higher than
// OperationTimeout when TLS connections take longer to establish.
// The default is the resolved OperationTimeout, so the dial is always
// bounded.
DialTimeout time.Duration
// Dialer is used to create new connections. If nil, a default
// net.Dialer is used.
//
// To connect over TLS (memcached running with --enable-ssl), set a
// *tls.Dialer, which satisfies this interface:
//
// Dialer: &tls.Dialer{Config: &tls.Config{RootCAs: pool}}
//
// A single *tls.Dialer serves a whole multi-server set: crypto/tls
// derives ServerName from each dial address when Config.ServerName is
// empty, so every server is verified against its own hostname. To tune
// the underlying TCP connection (timeouts, keep-alive) as well, set
// tls.Dialer.NetDialer. For per-server TLS settings, supply a Dialer that
// selects its config based on the address.
Dialer Dialer
// ServerSelector picks which server to use for a key.
// It receives the key and current server set and returns the selected server.
// The default implementation uses Rendezvous Hash for membership-stable selection.
ServerSelector ServerSelector
// Breaker configures the circuit breaker guarding each server: when a
// server's recent failure ratio trips it, operations to that server fail
// fast with ErrBreakerOpen instead of tying up connections. Disabled
// unless Breaker.Enabled is true. See BreakerConfig for the policy knobs
// and their defaults.
Breaker BreakerConfig
// Observer is notified around each operation for tracing and metrics.
// If nil, no observation is performed. See the otelmemcache subpackage for
// a ready-made OpenTelemetry adapter.
Observer Observer
}
Config holds the configuration of a memcache client. Every field is optional: the zero value selects the field's documented default.
The client keeps one connection pool and one circuit breaker per server, so every limit below applies per server, not client-wide: against ten servers, a MaxConnsPerServer of 10 allows a hundred connections in total.
"Default" is not one thing, so read each field's own documentation:
- MaxConnsPerServer, OperationTimeout, MaintenanceInterval, IdleConnCheckAfter and the Breaker policy take their Default* constant;
- DialTimeout inherits the resolved OperationTimeout;
- MaxConnLifetime and MaxConnIdleTime mean no limit.
Turning something off is equally field-specific: IdleConnCheckAfter is disabled by a negative value and Breaker by Breaker.Enabled, while OperationTimeout cannot be disabled at all — the client is never left unbounded by a configuration mistake.
type ConnPoolMetrics ¶
type ConnPoolMetrics struct {
// Lifetime counters
AcquireCount int64 // Total acquire attempts
AcquireWaitCount int64 // Acquires that had to wait
AcquireWaitDuration time.Duration // Total time spent waiting to acquire
CreatedConns int64 // Total connections created
DestroyedConns int64 // Total connections destroyed
AcquireErrors int64 // Failed acquire attempts
// Current state gauges
TotalConns int // Total connections in pool (active + idle)
IdleConns int // Idle connections available
ActiveConns int // Connections currently in use
}
ConnPoolMetrics is a point-in-time snapshot of a connection pool's statistics, in the shape of database/sql's DBStats: monotonic lifetime counters plus current-state gauges.
For Prometheus integration, expose these as:
- Gauges: TotalConns, IdleConns, ActiveConns
- Counters: AcquireCount, AcquireWaitCount, CreatedConns, DestroyedConns, AcquireErrors, AcquireWaitDuration
type Connection ¶
type Connection struct {
Reader *bufio.Reader
Writer *bufio.Writer
// contains filtered or unexported fields
}
Connection wraps a network connection with buffered reader and writer for efficient I/O.
func NewConnection ¶
func NewConnection(conn net.Conn, timeout time.Duration) *Connection
NewConnection creates a connection with a per-operation timeout. The timeout is a per-operation upper bound: each operation's deadline is the earlier of the context deadline and now+timeout (see setDeadline). The cap cannot be disabled — a non-positive timeout selects DefaultOperationTimeout, so a Connection is never left unbounded against a hung-but-connected peer; pass a large explicit value when a long budget is genuinely needed.
func (*Connection) Close ¶
func (c *Connection) Close() error
func (*Connection) Execute ¶
func (c *Connection) Execute(ctx context.Context, req *meta.Request, fn ResponseFunc) error
Execute implements the Executor interface. Executes a single request and calls fn with the decoded response. The deadline is the earlier of the context deadline and now+defaultTimeout.
The response passed to fn is owned by the connection and reused by the next Execute call: it and its Data/Flags storage are only valid until fn returns. fn must clone anything it retains.
A reply the command cannot produce (see meta.ValidateResponse) means the connection is out of sync: Execute returns a *meta.ParseError without calling fn, since the reply answers a different request.
func (*Connection) ExecuteBatch ¶
func (c *Connection) ExecuteBatch(ctx context.Context, reqs []*meta.Request) ([]*meta.Response, error)
ExecuteBatch implements the Executor interface. Executes multiple requests in a pipeline using the NoOp marker strategy. Sends all requests followed by a NoOp command, then reads responses until the NoOp response.
Returns responses in the same order as requests. Each response and its buffers are freshly allocated and owned by the caller (the Executor contract); only Execute reuses the connection's response storage. Individual request errors are captured in Response.Error (protocol errors). I/O errors or connection failures are returned as Go errors.
If no request uses the quiet flag, the response count is guaranteed to match the request count, and each response must be a reply its request's command can produce (see meta.ValidateResponse); a mismatch of either is reported as a *meta.ParseError since it means the connection is desynchronized. With quiet requests, nominal responses are suppressed by the server, so fewer responses than requests may be returned and the caller must correlate them (e.g. with opaque tokens). Their statuses are then not checked: response i does not necessarily answer request i.
Deadline handling: The deadline is extended before reading each response to prevent timeout due to cumulative time across multiple responses (inspired by Grafana PR #16).
func (*Connection) ExecuteStats ¶
ExecuteStats implements the StatsExecutor interface. Executes the stats command and returns the stats as a map.
args are the sub-command tokens sent after "stats", space-separated and verbatim: none for the general statistics, one for a named sub-command ("items", "slabs", "settings"), several for the ones that take parameters ("cachedump", "1", "100"). An empty token is rejected, because joining it would send a different command than the caller wrote.
type ConnectionError ¶
type ConnectionError = meta.ConnectionError
ConnectionError wraps an I/O failure on the connection.
type Counter ¶
type Counter struct {
Key string
Value uint64
CAS CAS
// Status is StatusApplied when the counter was read (or created), or the
// condition that stopped it: StatusNotFound (miss without Create) or
// StatusCASMismatch. Value is only set on StatusApplied — a CAS mismatch
// leaves the key in place but returns nothing about it.
Status Status
}
Counter is the result of an arithmetic operation.
type CounterOptions ¶
type CounterOptions struct {
TTL TTL
CAS CAS
Create bool // create the key when it is absent, seeded with Initial
Initial uint64 // value of the created key; ignored unless Create
}
CounterOptions modifies Increment and Decrement. The zero value requires the key to exist; a missing key reports StatusNotFound.
type DeleteOptions ¶
type DeleteOptions struct {
CAS CAS // require the item's CAS to match; zero: delete unconditionally
}
DeleteOptions modifies Delete.
type Dialer ¶
type Dialer interface {
DialContext(ctx context.Context, network, address string) (net.Conn, error)
}
Dialer establishes the network connections used by the client's pools. *net.Dialer satisfies this interface.
type Executor ¶
type Executor interface {
Execute(ctx context.Context, req *meta.Request, fn ResponseFunc) error
ExecuteBatch(ctx context.Context, reqs []*meta.Request) ([]*meta.Response, error)
}
Executor executes memcache requests for the keys they carry. The key is part of the request so an implementation can select a server from it.
Execute runs req and calls fn with the decoded response. It returns transport and pool errors only; the command outcome (a miss, a protocol error, a status the operation does not expect) is in the response, for fn to interpret. A reply the command cannot produce at all means the connection is out of sync: it is a transport error and fn is not called.
ExecuteBatch pipelines reqs and returns the responses by position. Unlike Execute, it hands its responses over: each Response and its Data and Flags storage is freshly allocated and owned by the caller, so batch callers keep them without copying. Implementations must not reuse buffers across the responses of a batch or across batches.
type GenericError ¶
type GenericError = meta.GenericError
GenericError is a bare ERROR reply: an unknown command or a protocol violation. The client closes the connection.
type GetOptions ¶
type GetOptions struct {
TTL TTL // non-zero: touch-on-read, updating the item's expiration (get-and-touch)
}
GetOptions modifies Get. The zero value reads without altering the item.
type InvalidRequestError ¶
type InvalidRequestError = meta.InvalidRequestError
InvalidRequestError is a request rejected by client-side validation, before any byte reached the wire.
type Item ¶
type Item struct {
Key string
Value []byte
Flags uint32 // client flags stored alongside the value
CAS CAS // compare-and-swap token
// Status is StatusApplied when the key was found and StatusNotFound when
// it was not; Value, Flags and CAS are only set on a hit. Test it with
// Status.OK(), as on every other result type.
Status Status
}
Item is what a read returns. Writes take the key and value as arguments (Set, Add, ...) or a SetItem (MultiSet).
type Observer ¶
Observer is notified around each client operation, enabling tracing and metrics without coupling the core to any telemetry backend. Implementations must be safe for concurrent use.
StartOp is called when an operation begins. It returns a context to use for the operation — carrying, for example, a tracing span so downstream work nests under it — and an ActiveOp whose End is called exactly once when the operation finishes. This mirrors OpenTelemetry's own tracer.Start → span.End shape.
type OpError ¶
type OpError struct {
// Op is the operation that failed, as one of the Op* constants
// ("get", "set", "batch", ...).
Op string
// Key is the cache key, when the operation targets a single key.
//
// The key is deliberately NOT part of the Error() message: keys often
// carry user identifiers (PII) that don't belong in logs, and embedding
// them would give error messages unbounded cardinality. Read this field
// explicitly (via errors.AsType) when the key is wanted.
Key string
// Address is the host:port of the server the operation was routed to.
Address string
// Err is the underlying cause: a connection or timeout error,
// ErrBreakerOpen, a meta protocol error, etc.
Err error
}
OpError records an operation that failed against a specific server, following the net.OpError / fs.PathError pattern.
It carries structured context for logging and metrics: which operation, against which server. It is not meant for control flow — branch on the underlying cause with errors.Is/errors.AsType, which traverse the wrapping:
if opErr, ok := errors.AsType[*memcache.OpError](err); ok {
log.Printf("op=%s server=%s: %v", opErr.Op, opErr.Address, err)
}
if errors.Is(err, context.DeadlineExceeded) { ... }
Example ¶
Errors report that an operation did not complete. A miss is not an error: it is a Status, on every result type.
Branch on the cause with errors.Is, and read the OpError for logging and metrics — the key is deliberately kept out of the error message.
package main
import (
"context"
"errors"
"fmt"
"log"
"github.com/pior/memcache"
)
func main() {
client := memcache.NewClient(
memcache.StaticServers("localhost:11211"),
memcache.Config{
MaxConnsPerServer: 20,
Breaker: memcache.BreakerConfig{Enabled: true},
},
)
defer client.Close()
_, err := client.Get(context.Background(), "user:42")
if err == nil {
return
}
switch {
case errors.Is(err, memcache.ErrBreakerOpen):
fmt.Println("server is shedding load; serve from the origin")
case errors.Is(err, context.DeadlineExceeded):
fmt.Println("budget spent; serve from the origin")
case errors.Is(err, memcache.ErrClientClosed):
fmt.Println("client is shut down")
}
if opErr, ok := errors.AsType[*memcache.OpError](err); ok {
log.Printf("memcache %s on %s failed (key %q): %v",
opErr.Op, opErr.Address, opErr.Key, opErr.Unwrap())
}
}
Output:
type OpInfo ¶
type OpInfo struct {
// Op is the operation, one of the Op* constants ("get", "set", "touch",
// ...) and the same vocabulary as OpError.Op. It names what the caller
// asked for, not the wire command carrying it, so a Touch is not reported
// as a get and an Add is not reported as a set.
Op string
Address string // resolved server address ("" if not yet known)
Key string // single-op key ("" for batch/stats)
Requests int // number of pipelined requests for a batch; 0 otherwise
}
OpInfo describes an operation as it begins.
type OpResult ¶
type OpResult struct {
// Status is the operation's outcome, the same value the caller receives:
// StatusApplied, StatusNotFound, StatusExists or StatusCASMismatch. It is
// the zero Status when there is no single outcome to report: the operation
// failed, or it is a batch, a stats or a flush_all.
Status Status
// Code is the raw status the server replied ("HD", "EN", "NS", ...), empty
// when no response was decoded. Status is what that code meant for this
// operation — the same NS is StatusExists after an add and StatusNotFound
// after a replace — so prefer Status and read Code only to report the wire
// verbatim.
Code string
Err error
}
OpResult describes an operation as it completes.
type ParseError ¶
type ParseError = meta.ParseError
ParseError is a server reply the client could not parse.
type PoolMetrics ¶
type PoolMetrics struct {
Address string
Conns ConnPoolMetrics
Breaker BreakerMetrics
}
PoolMetrics contains metrics for a single server's connection pool.
type Querier ¶
type Querier interface {
Get(ctx context.Context, key string, opts ...GetOptions) (Item, error)
Set(ctx context.Context, key string, value []byte, opts ...StoreOptions) (StoreResult, error)
Add(ctx context.Context, key string, value []byte, opts ...StoreOptions) (StoreResult, error)
Replace(ctx context.Context, key string, value []byte, opts ...StoreOptions) (StoreResult, error)
Append(ctx context.Context, key string, value []byte, opts ...ConcatOptions) (StoreResult, error)
Prepend(ctx context.Context, key string, value []byte, opts ...ConcatOptions) (StoreResult, error)
Touch(ctx context.Context, key string, ttl TTL) (Status, error)
Delete(ctx context.Context, key string, opts ...DeleteOptions) (Status, error)
Increment(ctx context.Context, key string, delta uint64, opts ...CounterOptions) (Counter, error)
Decrement(ctx context.Context, key string, delta uint64, opts ...CounterOptions) (Counter, error)
MultiGet(ctx context.Context, keys []string, opts ...GetOptions) ([]Item, error)
MultiSet(ctx context.Context, items []SetItem) ([]StoreResult, error)
MultiDelete(ctx context.Context, keys []string) ([]Status, error)
}
Querier is the command surface of Client: the single-key and pipelined multi-key operations of Commands.
type ResponseFunc ¶
ResponseFunc receives a response while its connection is still checked out. The Response and its Data and Flags storage belong to the connection and are valid only during the call: implementations reuse them for the next response on the same connection. Clone anything retained (e.g. bytes.Clone the value).
type Server ¶
type Server struct {
Address string
}
Server identifies a memcache node.
Address is the host:port used to dial the node and the key under which its connection pool is tracked. It is the stable identity a ServerSelector hashes against, so selection is consistent across processes and survives reordering of the server set. More fields (e.g. weight, zone) may be added in a backward-compatible way; the meaning of Address must not change.
func OrderedServerSelector ¶
OrderedServerSelector maps a key to a server by its position in the list, using Jump Hash. It hashes the key once, so it is the cheaper selector, but the result depends on the order and count of servers: inserting or removing a server anywhere but the end remaps a large fraction of keys. Use it only for a static, stably ordered server list. NewClient defaults to StableServerSelector instead; set Config.ServerSelector explicitly to opt in.
func StableServerSelector ¶
StableServerSelector maps a key to a server by server identity, using Rendezvous (Highest-Random-Weight) hashing. It is membership-stable: the result depends only on the set of Addresses and not on their order, so reordering the list never remaps a key, and adding or removing a single server moves only ~1/N of keys. This costs one hash per server per call. Ties (astronomically unlikely with a 64-bit hash) break on the lower Address to stay order-independent.
type ServerError ¶
type ServerError = meta.ServerError
ServerError is a SERVER_ERROR reply: the server failed the operation (out of memory, internal error). The connection stays usable.
type ServerPool ¶
type ServerPool struct {
// contains filtered or unexported fields
}
ServerPool wraps a pool, a circuit breaker with its server address.
func NewServerPool ¶
func NewServerPool(addr string, config Config) (*ServerPool, error)
NewServerPool creates the connection pool and circuit breaker for one server. Zero-value config fields get the same defaults NewClient applies.
func (*ServerPool) Address ¶
func (sp *ServerPool) Address() string
func (*ServerPool) Close ¶
func (sp *ServerPool) Close()
Close closes the pool, destroying its idle connections. It blocks until checked-out connections are returned.
func (*ServerPool) Execute ¶
func (sp *ServerPool) Execute(ctx context.Context, req *meta.Request, fn ResponseFunc) error
Execute executes a single request-response cycle with proper connection management. It handles acquiring a connection, sending the request, calling fn with the response while the connection is still checked out, and releasing/destroying the connection based on error conditions. The request is wrapped with the server's circuit breaker.
Execution failures are returned as *OpError carrying the operation, key, and server address. Protocol errors carried by the response (resp.Error) are not errors here: they are the command's outcome, left to fn, and do not count as circuit breaker failures. A reply the command cannot produce is an execution failure (a *meta.ParseError, see Connection.Execute): the connection is destroyed and fn is not called.
func (*ServerPool) ExecuteBatch ¶
func (sp *ServerPool) ExecuteBatch(ctx context.Context, reqs []*meta.Request) ([]*meta.Response, error)
ExecuteBatch executes multiple requests in a pipeline using the NoOp marker strategy. Sends all requests followed by a NoOp command, then reads responses until the NoOp response. This leverages memcached's FIFO guarantee for optimal performance.
Returns responses in the same order as requests. Individual request errors are captured in Response.Error (protocol errors). I/O errors or connection failures are returned as Go errors.
The batch execution is wrapped with the circuit breaker to track success/failure.
func (*ServerPool) ExecuteStats ¶
ExecuteStats runs the stats command on one connection and returns the server's reply.
Like Execute and ExecuteBatch, it goes through the server's circuit breaker: a stats call against a server the breaker is shedding fails fast with ErrBreakerOpen instead of tying up a connection, and its own failures count towards the trip condition.
func (*ServerPool) Metrics ¶
func (sp *ServerPool) Metrics() PoolMetrics
type ServerSelector ¶
ServerSelector picks which server owns a given key, from the current set of servers.
It must be deterministic and based on server identity (Address) so that all clients agree on key placement and a change to the server set moves as few keys as possible. The servers slice is never empty: the single-server case is handled before the selector is consulted.
The slice is read-only. It comes straight from Servers.List and may be that implementation's own backing array, shared with every concurrent operation, so a selector that sorts or reorders it in place is a data race.
type ServerStats ¶
type ServerStats struct {
Address string // Server address
Stats map[string]string // Server statistics (name -> value)
Error error // Error if stats request failed
}
ServerStats contains statistics from a single memcache server.
type Servers ¶
type Servers interface {
// List returns the current set of servers, read-only. The client calls it
// on every operation and hands the slice straight to the ServerSelector,
// so neither may modify it: an implementation is free to return its own
// backing array, shared with every concurrent caller, and StaticServers
// does exactly that.
List() []Server
}
Servers provides the current set of memcache servers. Implementations must be safe for concurrent use.
func ServersFromEnv ¶
ServersFromEnv creates a Servers instance from a comma-separated list of server addresses stored in the specified environment variable.
func StaticServers ¶
StaticServers returns an immutable Servers built from the given host:port addresses, in the order provided. List hands out the backing array itself, so callers must treat it as read-only (see Servers.List).
type SetItem ¶
type SetItem struct {
Key string
Value []byte
Options StoreOptions
}
SetItem is one item to store with MultiSet, the write-side counterpart of Item. Options apply to that item only: the zero value is a plain set with no expiration.
type StatsExecutor ¶
type StatsExecutor interface {
ExecuteStats(ctx context.Context, args ...string) (map[string]string, error)
}
StatsExecutor is an optional interface for executing the stats command. The stats command has a different response format than regular meta commands. args are the sub-command tokens sent after "stats", space-separated.
type Status ¶
type Status int
Status reports how an operation ended: it took effect, or the reason it did not. Every result type carries one — Item, StoreResult and Counter in their Status field, Delete and Touch as their return value — and Status.OK is the one way to ask whether an operation took effect.
It is the interpreted, public counterpart to the wire-level meta.StatusType: it preserves the distinction the raw status alone would lose (a store "not stored" could mean the key was missing, already existed, or the CAS mismatched).
The zero value is not a status: it means none was reported, which happens only on the zero value of a result type.
func (Status) OK ¶
OK reports whether the operation took effect. It is the single spelling of that question across the API: item.Status.OK(), result.Status.OK(), counter.Status.OK(), and the Status returned by Delete and Touch.
A failed precondition is not OK, whether the key was missing, already present, or carried another CAS; read the Status itself to tell those apart.
type StoreOptions ¶
type StoreOptions struct {
TTL TTL
Flags uint32 // client flags stored with the item; zero: none
CAS CAS // require the item's CAS to match; zero: store unconditionally
}
StoreOptions modifies Set, Add, and Replace. The zero value stores with no expiration, no client flags, and no CAS precondition.
type StoreResult ¶
type StoreResult struct {
// Status is StatusApplied when the value was stored, or the condition
// that stopped it: StatusExists (add on an existing key), StatusNotFound
// (replace, append or prepend on a missing key) or StatusCASMismatch.
Status Status
CAS CAS
}
StoreResult reports the result of a store. CAS is the item's token after a successful store, for chaining a follow-up conditional write without a reread.
type TTL ¶
type TTL struct {
// contains filtered or unexported fields
}
TTL specifies when an item expires. The zero value (NoTTL) means the item never expires (it persists until evicted). Use ExpiresIn for an expiration relative to now, ExpiresAt for an absolute point in time.
func ExpiresAt ¶
ExpiresAt returns a TTL expiring at time t. A t in the past expires the item immediately. A zero t is NoTTL.
func ExpiresIn ¶
ExpiresIn returns a TTL expiring d after the request is sent. Sub-second durations are rounded up to one second, memcached's resolution. Durations longer than 30 days are encoded on the wire as an absolute unix timestamp, as the memcached protocol requires; the meaning is unchanged. A non-positive d is NoTTL.
func (TTL) Expiration ¶
Expiration encodes the TTL as memcached's exptime value: 0 for no expiration, relative seconds up to 30 days, and an absolute unix timestamp beyond that. Relative durations longer than 30 days are converted to an absolute timestamp against the current time; only that branch reads the clock, so the common cases never call time.Now.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
consistenthash
Package consistenthash implements the consistent hashing algorithms that map a key to one of a set of servers.
|
Package consistenthash implements the consistent hashing algorithms that map a key to one of a set of servers. |
|
netconn
Package netconn holds protocol-agnostic helpers for net.Conn: idle connection liveness checks and I/O timeout attribution.
|
Package netconn holds protocol-agnostic helpers for net.Conn: idle connection liveness checks and I/O timeout attribution. |
|
Package meta provides low-level request serialization and response parsing for the memcached meta protocol (version 1.6+).
|
Package meta provides low-level request serialization and response parsing for the memcached meta protocol (version 1.6+). |
|
otelmemcache
module
|
|
|
spec
|
|
|
experiments
command
|