{"id":47944566,"url":"https://github.com/catatsuy/mcturbo","last_synced_at":"2026-04-04T08:22:11.290Z","repository":{"id":338535365,"uuid":"1157696866","full_name":"catatsuy/mcturbo","owner":"catatsuy","description":"memcached (ASCII) client with fast-path \u0026 context APIs, clustering (modula/ketama), partial GetMulti, and clear failover rules.","archived":false,"fork":false,"pushed_at":"2026-03-06T04:42:31.000Z","size":83,"stargazers_count":1,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-28T15:58:31.698Z","etag":null,"topics":["cache","consistent-hashing","go","memchached"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/catatsuy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-02-14T06:34:43.000Z","updated_at":"2026-02-16T09:23:54.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/catatsuy/mcturbo","commit_stats":null,"previous_names":["catatsuy/mcturbo"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/catatsuy/mcturbo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catatsuy%2Fmcturbo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catatsuy%2Fmcturbo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catatsuy%2Fmcturbo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catatsuy%2Fmcturbo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/catatsuy","download_url":"https://codeload.github.com/catatsuy/mcturbo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catatsuy%2Fmcturbo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31392853,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T04:26:24.776Z","status":"ssl_error","status_checked_at":"2026-04-04T04:23:34.147Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cache","consistent-hashing","go","memchached"],"created_at":"2026-04-04T08:22:10.637Z","updated_at":"2026-04-04T08:22:11.280Z","avatar_url":"https://github.com/catatsuy.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# mcturbo\n\n`mcturbo` is a memcached ASCII (text protocol) client for Go 1.26.\n\nIt provides two clients:\n- `mcturbo.Client` for a single memcached server\n- `cluster.Cluster` for multi-server routing\n\n## What This Project Supports\n\n- Protocol: memcached ASCII only\n- Commands: `get`, `gets`, `set`, `add`, `replace`, `cas`, `append`, `prepend`, `delete`, `touch`, `gat`, `incr`, `decr`, `flush_all`, `version`\n- Both API styles:\n  - Fast path (no `context` argument)\n  - Context-aware path (`*WithContext`)\n\nNot supported:\n- binary protocol, SASL, compression, serializer\n\n## Example (Single Server)\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/catatsuy/mcturbo\"\n)\n\nfunc main() {\n\tc, err := mcturbo.New(\"127.0.0.1:11211\", mcturbo.WithWorkers(4))\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer c.Close()\n\n\t// Fast path: no context argument.\n\tif err := c.Set(\"user:1\", []byte(\"alice\"), 1, 60); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\t// Context-aware path for deadline/cancel.\n\tctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)\n\tdefer cancel()\n\n\tit, err := c.GetWithContext(ctx, \"user:1\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tlog.Printf(\"get: value=%q flags=%d\", string(it.Value), it.Flags)\n\n\t// CAS update flow.\n\tcurrent, err := c.GetsWithContext(ctx, \"user:1\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\terr = c.CASWithContext(ctx, \"user:1\", []byte(\"alice-updated\"), current.Flags, 60, current.CAS)\n\tif errors.Is(err, mcturbo.ErrCASConflict) {\n\t\tlog.Printf(\"cas conflict: retry with latest value\")\n\t} else if err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\t// GetMulti may return both result and error on partial success.\n\titems, err := c.GetMultiWithContext(ctx, []string{\"user:1\", \"user:2\", \"user:3\"})\n\tif err != nil {\n\t\tif me, ok := errors.AsType[*mcturbo.MultiError](err); ok {\n\t\t\tlog.Printf(\"getmulti partial failure: %d servers failed\", len(me.PerServer))\n\t\t} else {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t}\n\tfor k, v := range items {\n\t\tlog.Printf(\"getmulti: key=%s value=%q flags=%d\", k, string(v.Value), v.Flags)\n\t}\n}\n```\n\n## Single-Server API Summary\n\nFast path (no context):\n- `Get`, `Gets`, `GetMulti`\n- `Set`, `Add`, `Replace`, `CAS`\n- `Append`, `Prepend`, `Delete`, `Touch`, `GetAndTouch`\n- `Incr`, `Decr`, `FlushAll`, `Ping`\n\nContext-aware path:\n- `GetWithContext`, `GetsWithContext`, `GetMultiWithContext`\n- `SetWithContext`, `AddWithContext`, `ReplaceWithContext`, `CASWithContext`\n- `AppendWithContext`, `PrependWithContext`, `DeleteWithContext`, `TouchWithContext`, `GetAndTouchWithContext`\n- `IncrWithContext`, `DecrWithContext`, `FlushAllWithContext`, `PingWithContext`\n\nLifecycle:\n- `Close()`\n\n### Timeout and Cancellation\n\n- `*WithContext` methods use `context` as the source of truth.\n- Fast-path methods do not accept `context`.\n- `WithDefaultDeadline(d)` sets a fallback socket deadline when you do not pass context.\n- `WithMaxSlots(n)` limits per-worker concurrency (`0` = unlimited).\n\n### Performance Tips\n\n- Prefer fast-path methods when you do not need per-call cancellation/deadline.\n- Reuse one client instance; do not create/close clients per request.\n- Tune `WithWorkers(n)` based on your CPU and request concurrency.\n- Start with `WithMaxSlots(0)` (unlimited), then set a limit only when protecting backend load.\n- Keep value sizes moderate and avoid very large hot keys.\n- Use `GetMulti` for multi-key reads to reduce network round trips.\n- Use context deadlines only where needed; overly short deadlines can increase retries and error handling cost.\n- Benchmark with your real key/value size distribution before changing defaults.\n\n## Cluster Client\n\n`cluster.Cluster` routes each key to one shard and calls the existing `mcturbo.Client` methods internally.\nThe routing layer is extensible: you can pass any `RouterFactory`, and you can also use built-in router factories.\n\n### Routing Options\n\n- Distribution:\n  - `DistributionModula` (default)\n  - `DistributionConsistent` (Ketama-style)\n- Hash:\n  - `HashDefault` (default)\n  - `HashMD5`\n  - `HashCRC32`\n- Libketama-compatible mode:\n  - `WithLibketamaCompatible(true)` forces:\n    - distribution = consistent\n    - hash = MD5\n- Custom router:\n  - `WithRouterFactory(cluster.RouterFactory)`\n  - You can inject your own `Router` implementation.\n  - Built-ins are also exposed as factories:\n    - `cluster.DefaultRouterFactory`\n    - `cluster.ModulaRouterFactory(hash)`\n    - `cluster.ConsistentRouterFactory(hash, vnodeFactor)`\n\n### Built-in Router Behavior\n\n- `DistributionModula`:\n  - `idx = hash(key) % serverCount`\n  - Fast O(1) lookup\n  - More key movement when server count changes\n- `DistributionConsistent` (Ketama):\n  - Builds a hash ring with virtual nodes (`vnodeFactor * weight * 4`)\n  - Uses binary search lookup on the ring (O(log M), `M` = ring points)\n  - Smaller key movement when servers are added/removed\n- `WithLibketamaCompatible(true)` forces consistent routing with MD5 hash.\n\n### Custom Router Example\n\n```go\ntype stickyRouter struct{}\n\nfunc (r *stickyRouter) Pick(key string) int {\n\t_ = key\n\treturn 0 // always shard 0 (example only)\n}\n\nclusterClient, err := cluster.NewCluster(\n\t[]cluster.Server{\n\t\t{Addr: \"127.0.0.1:11211\", Weight: 1},\n\t\t{Addr: \"127.0.0.1:11212\", Weight: 1},\n\t},\n\tcluster.WithRouterFactory(func(\n\t\tservers []cluster.Server,\n\t\tdist cluster.Distribution,\n\t\thash cluster.Hash,\n\t\tvnode int,\n\t) (cluster.Router, error) {\n\t\t_ = servers\n\t\t_ = dist\n\t\t_ = hash\n\t\t_ = vnode\n\t\treturn \u0026stickyRouter{}, nil\n\t}),\n)\nif err != nil {\n\tlog.Fatal(err)\n}\ndefer clusterClient.Close()\n```\n\nNotes:\n- Your factory is called on `NewCluster` and `UpdateServers`.\n- `Router.Pick` must return an index in `[0, len(servers)-1]`.\n- Cluster operation flow stays the same: `Pick(key)` -\u003e target shard client call.\n\n### Server Update Behavior\n\n- `UpdateServers` rebuilds routing.\n- Existing shard clients are reused when `Addr` is unchanged.\n- Removed shard clients are closed.\n- Key movement can happen after server updates.\n\n### Failover Behavior (Optional)\n\nDefault:\n- no failover\n\nEnable temporary auto-eject:\n- `WithRemoveFailedServers(true)`\n- `WithServerFailureLimit(n)` (default: `2`)\n- `WithRetryTimeout(d)` (default: `2s`)\n\nWhen enabled:\n- Retry to next shard only for communication failures:\n  - `io.EOF`, `net.ErrClosed`\n  - timeout/non-temporary `net.Error`\n  - protocol parse errors (`mcturbo.IsProtocolError(err)`)\n- No failover for semantic errors:\n  - `ErrNotFound`, `ErrNotStored`, `ErrCASConflict`\n- If all shards are temporarily ejected, the cluster falls back to trying all shards.\n\n`GetMulti` note:\n- It keeps partial-success semantics (`result` and `error` can both be non-nil).\n\n### Cluster Performance Tips\n\n- Keep server weights close to actual capacity to avoid shard hotspots.\n- Use `DistributionConsistent` for smoother key movement during `UpdateServers`.\n- Enable failover only when needed; each retry can add latency on failure paths.\n- Use `GetMulti` for read-heavy fan-out access patterns.\n\n## Cluster API Summary\n\nContext-aware path:\n- `GetWithContext`, `GetsWithContext`, `GetMultiWithContext`\n- `SetWithContext`, `AddWithContext`, `ReplaceWithContext`, `CASWithContext`\n- `AppendWithContext`, `PrependWithContext`, `DeleteWithContext`, `TouchWithContext`, `GetAndTouchWithContext`\n- `IncrWithContext`, `DecrWithContext`, `FlushAllWithContext`, `PingWithContext`\n\nFast path:\n- `Get`, `Gets`, `GetMulti`\n- `Set`, `Add`, `Replace`, `CAS`\n- `Append`, `Prepend`, `Delete`, `Touch`, `GetAndTouch`\n- `Incr`, `Decr`, `FlushAll`, `Ping`\n\nNo-context aliases:\n- `GetNoContext`, `GetsNoContext`, `GetMulti`\n- `SetNoContext`, `AddNoContext`, `ReplaceNoContext`, `CASNoContext`\n- `AppendNoContext`, `PrependNoContext`, `DeleteNoContext`, `TouchNoContext`, `GetAndTouchNoContext`\n- `IncrNoContext`, `DecrNoContext`, `FlushAllNoContext`, `PingNoContext`\n\nManagement:\n- `UpdateServers([]Server)`\n- `Close()`\n\n## Example (Cluster)\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/catatsuy/mcturbo\"\n\t\"github.com/catatsuy/mcturbo/cluster\"\n)\n\nfunc main() {\n\tc, err := cluster.NewCluster(\n\t\t[]cluster.Server{\n\t\t\t{Addr: \"127.0.0.1:11211\", Weight: 1},\n\t\t\t{Addr: \"127.0.0.1:11212\", Weight: 1},\n\t\t},\n\t\tcluster.WithDistribution(cluster.DistributionConsistent), // explicit ketama\n\t\tcluster.WithHash(cluster.HashMD5),\n\t\tcluster.WithBaseClientOptions(mcturbo.WithWorkers(4)),\n\t\tcluster.WithRemoveFailedServers(true), // optional failover\n\t\tcluster.WithServerFailureLimit(2),\n\t\tcluster.WithRetryTimeout(2*time.Second),\n\t)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer c.Close()\n\n\t// No-context API.\n\tif err := c.SetNoContext(\"session:42\", []byte(\"token\"), 0, 120); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif err := c.PingNoContext(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\t// Context-aware API.\n\tctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)\n\tdefer cancel()\n\n\tit, err := c.GetWithContext(ctx, \"session:42\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tlog.Printf(\"cluster get: value=%q flags=%d\", string(it.Value), it.Flags)\n\n\t// Cluster GetMulti also allows partial success.\n\titems, err := c.GetMultiWithContext(ctx, []string{\"session:42\", \"session:43\"})\n\tif err != nil {\n\t\tif me, ok := errors.AsType[*mcturbo.MultiError](err); ok {\n\t\t\tlog.Printf(\"cluster getmulti partial failure: %d servers failed\", len(me.PerServer))\n\t\t} else {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t}\n\tfor k, v := range items {\n\t\tlog.Printf(\"cluster getmulti: key=%s value=%q\", k, string(v.Value))\n\t}\n}\n```\n\n## Test\n\nUnit tests:\n\n```bash\ngo test ./...\n```\n\nIntegration tests (requires `memcached` command):\n\n```bash\ngo test -tags=integration ./...\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcatatsuy%2Fmcturbo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcatatsuy%2Fmcturbo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcatatsuy%2Fmcturbo/lists"}