mcturbo is a memcached ASCII (text protocol) client for Go 1.26.
It provides two clients:
mcturbo.Clientfor a single memcached servercluster.Clusterfor multi-server routing
- Protocol: memcached ASCII only
- Commands:
get,gets,set,add,replace,cas,append,prepend,delete,touch,gat,incr,decr,flush_all,version - Both API styles:
- Fast path (no
contextargument) - Context-aware path (
*WithContext)
- Fast path (no
Not supported:
- binary protocol, SASL, compression, serializer
package main
import (
"context"
"errors"
"log"
"time"
"github.com/catatsuy/mcturbo"
)
func main() {
c, err := mcturbo.New("127.0.0.1:11211", mcturbo.WithWorkers(4))
if err != nil {
log.Fatal(err)
}
defer c.Close()
// Fast path: no context argument.
if err := c.Set("user:1", []byte("alice"), 1, 60); err != nil {
log.Fatal(err)
}
// Context-aware path for deadline/cancel.
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
it, err := c.GetWithContext(ctx, "user:1")
if err != nil {
log.Fatal(err)
}
log.Printf("get: value=%q flags=%d", string(it.Value), it.Flags)
// CAS update flow.
current, err := c.GetsWithContext(ctx, "user:1")
if err != nil {
log.Fatal(err)
}
err = c.CASWithContext(ctx, "user:1", []byte("alice-updated"), current.Flags, 60, current.CAS)
if errors.Is(err, mcturbo.ErrCASConflict) {
log.Printf("cas conflict: retry with latest value")
} else if err != nil {
log.Fatal(err)
}
// GetMulti may return both result and error on partial success.
items, err := c.GetMultiWithContext(ctx, []string{"user:1", "user:2", "user:3"})
if err != nil {
if me, ok := errors.AsType[*mcturbo.MultiError](err); ok {
log.Printf("getmulti partial failure: %d servers failed", len(me.PerServer))
} else {
log.Fatal(err)
}
}
for k, v := range items {
log.Printf("getmulti: key=%s value=%q flags=%d", k, string(v.Value), v.Flags)
}
}Fast path (no context):
Get,Gets,GetMultiSet,Add,Replace,CASAppend,Prepend,Delete,Touch,GetAndTouchIncr,Decr,FlushAll,Ping
Context-aware path:
GetWithContext,GetsWithContext,GetMultiWithContextSetWithContext,AddWithContext,ReplaceWithContext,CASWithContextAppendWithContext,PrependWithContext,DeleteWithContext,TouchWithContext,GetAndTouchWithContextIncrWithContext,DecrWithContext,FlushAllWithContext,PingWithContext
Lifecycle:
Close()
*WithContextmethods usecontextas the source of truth.- Fast-path methods do not accept
context. WithDefaultDeadline(d)sets a fallback socket deadline when you do not pass context.WithMaxSlots(n)limits per-worker concurrency (0= unlimited).
- Prefer fast-path methods when you do not need per-call cancellation/deadline.
- Reuse one client instance; do not create/close clients per request.
- Tune
WithWorkers(n)based on your CPU and request concurrency. - Start with
WithMaxSlots(0)(unlimited), then set a limit only when protecting backend load. - Keep value sizes moderate and avoid very large hot keys.
- Use
GetMultifor multi-key reads to reduce network round trips. - Use context deadlines only where needed; overly short deadlines can increase retries and error handling cost.
- Benchmark with your real key/value size distribution before changing defaults.
cluster.Cluster routes each key to one shard and calls the existing mcturbo.Client methods internally.
The routing layer is extensible: you can pass any RouterFactory, and you can also use built-in router factories.
- Distribution:
DistributionModula(default)DistributionConsistent(Ketama-style)
- Hash:
HashDefault(default)HashMD5HashCRC32
- Libketama-compatible mode:
WithLibketamaCompatible(true)forces:- distribution = consistent
- hash = MD5
- Custom router:
WithRouterFactory(cluster.RouterFactory)- You can inject your own
Routerimplementation. - Built-ins are also exposed as factories:
cluster.DefaultRouterFactorycluster.ModulaRouterFactory(hash)cluster.ConsistentRouterFactory(hash, vnodeFactor)
DistributionModula:idx = hash(key) % serverCount- Fast O(1) lookup
- More key movement when server count changes
DistributionConsistent(Ketama):- Builds a hash ring with virtual nodes (
vnodeFactor * weight * 4) - Uses binary search lookup on the ring (O(log M),
M= ring points) - Smaller key movement when servers are added/removed
- Builds a hash ring with virtual nodes (
WithLibketamaCompatible(true)forces consistent routing with MD5 hash.
type stickyRouter struct{}
func (r *stickyRouter) Pick(key string) int {
_ = key
return 0 // always shard 0 (example only)
}
clusterClient, err := cluster.NewCluster(
[]cluster.Server{
{Addr: "127.0.0.1:11211", Weight: 1},
{Addr: "127.0.0.1:11212", Weight: 1},
},
cluster.WithRouterFactory(func(
servers []cluster.Server,
dist cluster.Distribution,
hash cluster.Hash,
vnode int,
) (cluster.Router, error) {
_ = servers
_ = dist
_ = hash
_ = vnode
return &stickyRouter{}, nil
}),
)
if err != nil {
log.Fatal(err)
}
defer clusterClient.Close()Notes:
- Your factory is called on
NewClusterandUpdateServers. Router.Pickmust return an index in[0, len(servers)-1].- Cluster operation flow stays the same:
Pick(key)-> target shard client call.
UpdateServersrebuilds routing.- Existing shard clients are reused when
Addris unchanged. - Removed shard clients are closed.
- Key movement can happen after server updates.
Default:
- no failover
Enable temporary auto-eject:
WithRemoveFailedServers(true)WithServerFailureLimit(n)(default:2)WithRetryTimeout(d)(default:2s)
When enabled:
- Retry to next shard only for communication failures:
io.EOF,net.ErrClosed- timeout/non-temporary
net.Error - protocol parse errors (
mcturbo.IsProtocolError(err))
- No failover for semantic errors:
ErrNotFound,ErrNotStored,ErrCASConflict
- If all shards are temporarily ejected, the cluster falls back to trying all shards.
GetMulti note:
- It keeps partial-success semantics (
resultanderrorcan both be non-nil).
- Keep server weights close to actual capacity to avoid shard hotspots.
- Use
DistributionConsistentfor smoother key movement duringUpdateServers. - Enable failover only when needed; each retry can add latency on failure paths.
- Use
GetMultifor read-heavy fan-out access patterns.
Context-aware path:
GetWithContext,GetsWithContext,GetMultiWithContextSetWithContext,AddWithContext,ReplaceWithContext,CASWithContextAppendWithContext,PrependWithContext,DeleteWithContext,TouchWithContext,GetAndTouchWithContextIncrWithContext,DecrWithContext,FlushAllWithContext,PingWithContext
Fast path:
Get,Gets,GetMultiSet,Add,Replace,CASAppend,Prepend,Delete,Touch,GetAndTouchIncr,Decr,FlushAll,Ping
No-context aliases:
GetNoContext,GetsNoContext,GetMultiSetNoContext,AddNoContext,ReplaceNoContext,CASNoContextAppendNoContext,PrependNoContext,DeleteNoContext,TouchNoContext,GetAndTouchNoContextIncrNoContext,DecrNoContext,FlushAllNoContext,PingNoContext
Management:
UpdateServers([]Server)Close()
package main
import (
"context"
"errors"
"log"
"time"
"github.com/catatsuy/mcturbo"
"github.com/catatsuy/mcturbo/cluster"
)
func main() {
c, err := cluster.NewCluster(
[]cluster.Server{
{Addr: "127.0.0.1:11211", Weight: 1},
{Addr: "127.0.0.1:11212", Weight: 1},
},
cluster.WithDistribution(cluster.DistributionConsistent), // explicit ketama
cluster.WithHash(cluster.HashMD5),
cluster.WithBaseClientOptions(mcturbo.WithWorkers(4)),
cluster.WithRemoveFailedServers(true), // optional failover
cluster.WithServerFailureLimit(2),
cluster.WithRetryTimeout(2*time.Second),
)
if err != nil {
log.Fatal(err)
}
defer c.Close()
// No-context API.
if err := c.SetNoContext("session:42", []byte("token"), 0, 120); err != nil {
log.Fatal(err)
}
if err := c.PingNoContext(); err != nil {
log.Fatal(err)
}
// Context-aware API.
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
it, err := c.GetWithContext(ctx, "session:42")
if err != nil {
log.Fatal(err)
}
log.Printf("cluster get: value=%q flags=%d", string(it.Value), it.Flags)
// Cluster GetMulti also allows partial success.
items, err := c.GetMultiWithContext(ctx, []string{"session:42", "session:43"})
if err != nil {
if me, ok := errors.AsType[*mcturbo.MultiError](err); ok {
log.Printf("cluster getmulti partial failure: %d servers failed", len(me.PerServer))
} else {
log.Fatal(err)
}
}
for k, v := range items {
log.Printf("cluster getmulti: key=%s value=%q", k, string(v.Value))
}
}Unit tests:
go test ./...Integration tests (requires memcached command):
go test -tags=integration ./...