Help us shape the future of Go! Take the 2026 Go Developer Survey

memcache

package module
v0.0.0-...-2f0df27 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 21, 2026 License: MIT Imports: 19 Imported by: 1

README

memcache

Go Reference

A modern memcache client for Go implementing the meta protocol.

The library provides a high-level Client with multi-server support, circuit breakers, and connection pooling, built on top of low-level building blocks (a meta protocol codec, connections, and command helpers) that you can compose into a custom client.

Its runtime dependencies are sony/gobreaker for circuit breakers, jackc/puddle for connection pooling, and zeebo/xxh3 for key hashing.

Why This Client?

bradfitz/gomemcache is the de-facto Go memcache client and is battle-tested; if a plain client speaking the legacy text protocol is all you need, it remains a fine choice. This client exists for services that need more from their cache path:

pior/memcache bradfitz/gomemcache
Protocol meta (memcached 1.6+) legacy text
Timeouts per-call context.Context, plus a per-operation I/O bound that cannot be disabled client-wide Timeout
Connection pooling bounded pool with connection lifetime/idle limits, health checks, and pool metrics free-list of idle connections, unbounded when busy
Key distribution rendezvous hashing by default: reordering the server list never remaps keys, adding or removing one server moves ~1/N of keys — plus a cheaper jump-hash selector, or your own CRC32 modulo: any change to the server list remaps most keys
Failure isolation per-server circuit breakers with per-server error attribution errors surface to the caller
Batching MultiGet/MultiSet/MultiDelete for the common cases, plus pipelined batches of arbitrary mixed commands (get + set + increment in one round trip) GetMulti (reads only)
Observability Observer hook with a ready-made OpenTelemetry adapter —
Performance

Single-key operations are bounded by the server round-trip in both clients — switching does not cost you throughput (cmd/bench, 8 workers against memcached 1.6 on loopback, AMD Ryzen 7 8845HS, 200k ops per operation, trimmed mean of 5 runs):

operation pior/memcache bradfitz/gomemcache
get (hit) 151k ops/s 151k ops/s
get (miss) 155k ops/s 141k ops/s
set 151k ops/s 150k ops/s
get 10 KB 151k ops/s 150k ops/s
set 10 KB 106k ops/s 105k ops/s
delete 149k ops/s 151k ops/s
increment 149k ops/s 151k ops/s

Pipelining is where the meta protocol pays off: on the same setup, a MultiGet of 10 keys delivers 1.10M items/s versus 151k items/s issuing them one at a time — and batches extend to writes and mixed commands, which the legacy protocol's GetMulti cannot express.

Reproduce with ./bench -count 200000 -concurrency 8 -runs 5 (add -bradfitz for the gomemcache side); every pull request also runs a paired benchmark against main to catch regressions.

Features

  • Multi-server support with consistent key distribution
  • Circuit breakers using gobreaker for fault tolerance
  • Connection pooling with health checks and lifecycle management, backed by jackc/puddle
  • Pool statistics for monitoring connection health and usage
  • Context deadlines honored throughout; cancellation during I/O is bounded by the per-operation timeout
  • Type-safe operations
  • Low-level building blocks (meta protocol codec, connections, command helpers) for custom clients

Installation

go get github.com/pior/memcache

Quick Start

import (
    "context"
    "fmt"
    "time"

    "github.com/pior/memcache"
)

// Every Config field is optional: the zero value selects a documented default.
// The one worth sizing on day one is MaxConnsPerServer, the per-server
// connection limit — it caps how many operations can be in flight to a server
// at once, and callers queue (bounded only by their context) once it is full.
servers := memcache.StaticServers("localhost:11211", "localhost:11212")
client := memcache.NewClient(servers, memcache.Config{
    MaxConnsPerServer: 20,
})
defer client.Close()

ctx := context.Background()

// Set with TTL (memcache.ExpiresAt for an absolute expiration time,
// omit StoreOptions to never expire). Set reports the outcome and the new
// CAS token; the error is reserved for transport failures.
_, _ = client.Set(ctx, "mykey", []byte("hello world"),
    memcache.StoreOptions{TTL: memcache.ExpiresIn(1 * time.Hour)})

// Get. Every result — Item, StoreResult, Counter, and the Status returned by
// Delete and Touch — reports the outcome as a Status; Status.OK() is the one
// way to ask whether the operation took effect.
item, _ := client.Get(ctx, "mykey")
if item.Status.OK() {
    fmt.Printf("Value: %s\n", item.Value)
}

// Increment a counter. By default a missing key is reported as not found;
// set CounterOptions.Create to create it on first use, seeded with Initial.
count, _ := client.Increment(ctx, "counter", 1, memcache.CounterOptions{Create: true, Initial: 1})
fmt.Printf("Count: %d\n", count.Value)

// Counter deltas and values use memcached's native uint64 representation.
count, _ = client.Decrement(ctx, "counter", 1)
if count.Status == memcache.StatusNotFound {
    fmt.Println("Counter does not exist")
}

// Delete
_, _ = client.Delete(ctx, "mykey")

// Batches pipeline the requests to each server concurrently and return one
// result per key, in order. A failure on any server fails the whole batch.
_, _ = client.MultiSet(ctx, []memcache.SetItem{
    {Key: "a", Value: []byte("1")},
    {Key: "b", Value: []byte("2"), Options: memcache.StoreOptions{TTL: memcache.ExpiresIn(time.Minute)}},
})
items, _ := client.MultiGet(ctx, []string{"a", "b", "c"}) // items[2].Status == memcache.StatusNotFound

See the package documentation for runnable examples of the usual patterns: cache-aside, read-through with a CAS-guarded write back, counters, and mixed-command batches.

Multi-Server Support

The client supports multiple memcache servers with consistent key distribution:

servers := memcache.StaticServers(
    "cache1.example.com:11211",
    "cache2.example.com:11211",
    "cache3.example.com:11211",
)

client := memcache.NewClient(servers, memcache.Config{
    MaxConnsPerServer: 10,
})

Keys are distributed with memcache.StableServerSelector (Rendezvous Hash) by default. Selection depends only on the set of server addresses, so reordering the list never remaps keys and adding or removing a single server moves only ~1/N of keys. For a static, ordered server list, memcache.OrderedServerSelector provides the lower fixed cost of Jump Hash. You can also supply a custom selector of the form func(key string, servers []memcache.Server) memcache.Server via Config.ServerSelector (for example, a weighted or zone-aware policy).

Batching

Batching is two things.

Convenience operations — MultiGet, MultiSet and MultiDelete cover the common case. Keys are grouped by server, each group is pipelined as one round trip, groups run concurrently, and results come back in the order the keys were passed:

items, err := client.MultiGet(ctx, []string{"a", "b", "c"})

Batches of arbitrary operations — ExecuteBatch pipelines any mix of meta commands, which is what the legacy protocol's GetMulti cannot express. Build meta.Request values and read the meta.Response values back by position:

reqs := []*meta.Request{
    meta.NewRequest(meta.CmdGet, "profile:42", nil).AddReturnValue().AddReturnCAS(),
    meta.NewRequest(meta.CmdSet, "session:42", []byte("live")).AddTTL(300),
    meta.NewRequest(meta.CmdArithmetic, "hits:42", nil).
        AddModeIncrement().AddDelta(1).AddInitialValue(0).AddVivify(3600).AddReturnValue(),
}
resps, err := client.ExecuteBatch(ctx, reqs) // resps[i] answers reqs[i]

ExecuteBatch 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 by-position matching.

OperationTimeout bounds each response read, not the whole batch, so pass a context with a deadline to cap the total.

Circuit Breakers

Each server gets its own circuit breaker: when a server's recent failure ratio trips it, operations to that server fail fast with memcache.ErrBreakerOpen instead of tying up connections. The defaults trip at 60% failures over the last 10 seconds (with at least 10 operations observed) and retest the server after 5 seconds:

client := memcache.NewClient(servers, memcache.Config{
    MaxConnsPerServer: 10,
    Breaker: memcache.BreakerConfig{Enabled: true},
})

Every knob can be tuned; see BreakerConfig for the full documentation:

Breaker: memcache.BreakerConfig{
    Enabled:          true,
    TripMinRequests:  10,               // don't trip below this volume
    TripFailureRatio: 0.6,              // trip when 60% of recent operations failed
    TripWindow:       10 * time.Second, // "recent" means the last 10s
    OpenDuration:     5 * time.Second,  // fail fast for 5s, then probe the server
    OnStateChange: func(server, from, to string) {
        log.Printf("breaker %s: %s -> %s", server, from, to)
    },
},

Only transport-level errors count as failures (dial errors, socket I/O errors, operation timeouts); cache misses and caller-caused errors (canceled contexts, invalid keys) do not.

Give callers a budget looser than OperationTimeout. A timeout counts against the server only when 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, so the breaker never opens and every operation keeps paying the full timeout. A caller context with no deadline is capped at OperationTimeout and works too.

Detect rejected operations with errors.Is(err, memcache.ErrBreakerOpen), and monitor the breakers through client.PoolMetrics():

for _, m := range client.PoolMetrics() {
    fmt.Printf("Server: %s, Circuit: %s\n", m.Address, m.Breaker.State)
    fmt.Printf("  Requests: %d, Failures: %d\n",
        m.Breaker.Requests,
        m.Breaker.TotalFailures)
}

Connection Pooling

The client pools connections per server (backed by jackc/puddle), up to MaxConnsPerServer connections per pool. Connection lifecycle is controlled by MaxConnLifetime, MaxConnIdleTime, and MaintenanceInterval.

Pool Statistics

Monitor connection pool health and usage:

for _, m := range client.PoolMetrics() {
    fmt.Printf("Server: %s\n", m.Address)
    fmt.Printf("  Total Connections: %d\n", m.Conns.TotalConns)
    fmt.Printf("  Idle Connections: %d\n", m.Conns.IdleConns)
    fmt.Printf("  Active Connections: %d\n", m.Conns.ActiveConns)
    fmt.Printf("  Connections Created: %d\n", m.Conns.CreatedConns)
    fmt.Printf("  Acquire Errors: %d\n", m.Conns.AcquireErrors)

    // Circuit breaker state
    fmt.Printf("  Circuit State: %s\n", m.Breaker.State)
}

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 — bounded by DialTimeout (defaults to OperationTimeout).
  • I/O — bounded by the earlier of the context deadline and now + 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.

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 ambiguous (the server may or may not have applied it), so callers that need certainty must verify or accept the ambiguity.

Checkout waits show up in the pool metrics (AcquireWaitCount, AcquireWaitDuration), and errors during checkout are prefixed with acquire:.

Observability

Config.Observer is invoked around every operation, enabling tracing and metrics without coupling the core to any telemetry backend. It is off by default and has no impact when unset. StartOp returns a context (so a span propagates to nested work) and an ActiveOp; the client calls ActiveOp.End once with the outcome and any error — the same tracer.Start → span.End shape OpenTelemetry uses. Keys are never passed to telemetry by the shipped adapter — only counts — matching the client's policy of keeping keys out of errors.

The operation is named, not coded: OpInfo.Op is "get", "touch", "add", … (the Op* constants), never the mg/ms command that carries it, and the same names appear in OpError.Op. OpResult.Status is the interpreted outcome the caller sees, so an add that found the key (StatusExists) and a replace that did not (StatusNotFound) are distinct although the server answered NS to both; OpResult.Code carries that raw code.

A ready-made OpenTelemetry tracing adapter ships as a separate module, so the OTel dependency tree never leaks into consumers of the core package:

import "github.com/pior/memcache/otelmemcache"

client := memcache.NewClient(servers, memcache.Config{
    Observer: otelmemcache.New(tracerProvider), // one client span per operation
})

Keys are excluded from spans by default. If your keys are safe to export to your tracing backend, opt in with otelmemcache.New(tracerProvider, otelmemcache.Options{RecordKeys: true}) to record them as the db.operation.parameter.key attribute.

To wire metrics, implement memcache.Observer against your metrics backend (the same hook carries everything needed for per-op latency, error, and hit/miss counters).

Low-Level Building Blocks

The high-level client is assembled from smaller pieces you can use on their own to build a custom client:

  • meta package — request serialization and response parsing for the memcached meta protocol.
  • Connection — a single pooled connection that implements Executor.
  • Commands — the command logic (Get, Set, Delete, Increment, MultiGet, …) on top of any Executor.

See the package documentation for runnable examples.

How It's Tested

A cache client's worst failure is returning the wrong value, so every stress and chaos harness in this repo enforces the same invariant: stored values embed their key, and any response carrying data for a different key counts as a protocol desynchronization. Under failure injection, errors are expected — wrong data never is.

Beyond the unit and integration tests (run in CI against a real memcached, with the race detector, plus a paired benchmark that flags performance regressions on every PR), the client is validated with:

  • In-process failure injection (stress/) — connections killed mid-stream, injected latency and jitter, per-request server errors, connection churn against a saturated pool, latency spikes past the timeout, and full server outages.
  • Endurance soak (loadtest/stress/) — a 56-hour saturation soak against three servers, ~35 billion operations, including chaos phases (server kills, a hung server): zero desyncs.
  • Chaos replay — container-level faults (freeze, kill one node, kill a majority) against a live 3-node fleet at ~230k ops/s: errors attributed only to the faulted node, breakers open and re-close per node, a frozen node degrades throughput instead of stalling the client — zero desyncs across 188M ops. Run record.
  • Fleet churn at scale — 48 real memcached servers under rolling server churn, breaker flapping, a mass-freeze of half the fleet, and a total outage: 182M ops with pool, goroutine, and heap usage flat throughout; a full run built with the race detector reported zero data races; a service-discovery probe (142 server addresses rolled through a 24-node live set) caught a departed-pool reaping leak, fixed in #129. Run record.
  • Cloud chaos on GCP (loadtest/) — orchestrated VM fleets with a fault timeline (SIGSTOP freeze, iptables blackhole, netem latency+loss, SIGKILL+restart), run same-zone (~475M ops across two clients) and cross-region through a ~180ms-RTT shard: zero desyncs in every phase, per-shard error attribution, breakers open during each fault and re-close on heal, full throughput recovery. Run record.

Requirements

  • Go 1.26+
  • Memcached 1.6+ (with meta protocol support)

License

MIT License - See LICENSE file for details.

Status

This project is under active development. The meta protocol implementation and core client features are production-ready — see How It's Tested for the validation record — but the API is still pre-v1.0 and may change before the first stable release.

Contributions and feedback are welcome!

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)
	}
}

Index

Examples

Constants

View Source
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.

View Source
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.

View Source
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

View Source
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.

View Source
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

func NewClient(servers Servers, config Config) *Client

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"))
}
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"))
}
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)
	}
}
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"))
}

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

func (c *Client) Execute(ctx context.Context, req *meta.Request, fn ResponseFunc) (err error)

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

func (c *Client) ExecuteBatch(ctx context.Context, reqs []*meta.Request) ([]*meta.Response, error)

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)
	}
}

func (*Client) FlushAll

func (c *Client) FlushAll(ctx context.Context) error

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

func (c *Client) Stats(ctx context.Context, args ...string) ([]ServerStats, error)

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

func NewCommands(executor Executor) *Commands

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)
	}
}

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

func (c *Commands) Delete(ctx context.Context, key string, opts ...DeleteOptions) (Status, error)

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

func (c *Commands) Get(ctx context.Context, key string, opts ...GetOptions) (Item, error)

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

func (c *Commands) MultiDelete(ctx context.Context, keys []string) ([]Status, error)

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

func (c *Commands) MultiGet(ctx context.Context, keys []string, opts ...GetOptions) ([]Item, error)

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

func (c *Commands) MultiSet(ctx context.Context, items []SetItem) ([]StoreResult, error)

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.

func (*Commands) Touch

func (c *Commands) Touch(ctx context.Context, key string, ttl TTL) (Status, error)

Touch updates the expiration of an existing key without transferring its value. It reports StatusApplied when the key existed, StatusNotFound when it did not. The TTL is the point of the call, so it is a plain argument, like the delta of Increment.

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

func (c *Connection) ExecuteStats(ctx context.Context, args ...string) (map[string]string, error)

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.

func (*Connection) Ping

func (c *Connection) Ping(ctx context.Context) error

Ping performs a simple health check on a connection using the noop command. The check is bounded by the earlier of the context deadline and the connection's default timeout.

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

type Observer interface {
	StartOp(ctx context.Context, info OpInfo) (context.Context, ActiveOp)
}

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())
	}
}

func (*OpError) Error

func (e *OpError) Error() string

func (*OpError) Unwrap

func (e *OpError) Unwrap() error

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

type ResponseFunc func(*meta.Response)

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

func OrderedServerSelector(key string, servers []Server) Server

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

func StableServerSelector(key string, servers []Server) Server

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

func (sp *ServerPool) ExecuteStats(ctx context.Context, args ...string) (map[string]string, error)

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

type ServerSelector func(key string, servers []Server) Server

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

func ServersFromEnv(envVar string) (Servers, error)

ServersFromEnv creates a Servers instance from a comma-separated list of server addresses stored in the specified environment variable.

func StaticServers

func StaticServers(addrs ...string) Servers

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.

const (
	StatusApplied     Status = iota + 1 // the operation took effect: a read found the key, a write or delete applied
	StatusNotFound                      // the key did not exist
	StatusExists                        // the key already existed (add on existing key)
	StatusCASMismatch                   // the CAS precondition did not match
)

func (Status) OK

func (s Status) OK() bool

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.

func (Status) String

func (s Status) String() string

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

func ExpiresAt(t time.Time) TTL

ExpiresAt returns a TTL expiring at time t. A t in the past expires the item immediately. A zero t is NoTTL.

func ExpiresIn

func ExpiresIn(d time.Duration) TTL

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

func (t TTL) Expiration() int

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.

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL