{"id":51497013,"url":"https://github.com/avifenesh/ocaml-valkey","last_synced_at":"2026-07-07T16:30:30.831Z","repository":{"id":352388393,"uuid":"1214962255","full_name":"avifenesh/ocaml-valkey","owner":"avifenesh","description":"Modern Valkey client for OCaml 5 + Eio (RESP3-only).","archived":false,"fork":false,"pushed_at":"2026-05-17T19:26:44.000Z","size":2155,"stargazers_count":9,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-17T19:32:48.237Z","etag":null,"topics":["cache","client","database","distributed-systems","dune","dx","eio","ergonomics","functional-programming","high-availability","high-performance","key-value","library","ocaml","ocaml5","opam","performance","redis","reliability","valkey"],"latest_commit_sha":null,"homepage":"","language":"OCaml","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/avifenesh.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":"AUDIT.md","citation":null,"codeowners":null,"security":"docs/security.md","support":null,"governance":null,"roadmap":"ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-19T09:39:43.000Z","updated_at":"2026-05-17T19:23:27.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/avifenesh/ocaml-valkey","commit_stats":null,"previous_names":["avifenesh/valkey-ocaml","avifenesh/ocaml-valkey"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/avifenesh/ocaml-valkey","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/avifenesh%2Focaml-valkey","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/avifenesh%2Focaml-valkey/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/avifenesh%2Focaml-valkey/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/avifenesh%2Focaml-valkey/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/avifenesh","download_url":"https://codeload.github.com/avifenesh/ocaml-valkey/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/avifenesh%2Focaml-valkey/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35235814,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-07T02:00:07.222Z","response_time":90,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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","client","database","distributed-systems","dune","dx","eio","ergonomics","functional-programming","high-availability","high-performance","key-value","library","ocaml","ocaml5","opam","performance","redis","reliability","valkey"],"created_at":"2026-07-07T16:30:29.947Z","updated_at":"2026-07-07T16:30:30.817Z","avatar_url":"https://github.com/avifenesh.png","language":"OCaml","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ocaml-valkey\n\nA modern Valkey client for OCaml 5 + [Eio](https://github.com/ocaml-multicore/eio).\n\n**Status: alpha, approaching a stable API audit.** v0.3.1 is on\nopam. v0.4.0 is prepared as the Valkey Bundle module release:\nSearch, JSON, and Bloom now have typed wrappers, bundle-backed\nintegration tests, and runnable examples. Full core + cluster +\nbatch (incl. WATCH guards + cross-slot `pfcount_cluster`) +\nclient-side caching\n(Default / BCAST / OPTIN, standalone and cluster) + blocking\npool (BLPOP / BRPOP / BLMOVE / XREAD BLOCK via a per-node lease\npool) + AWS IAM auth (pure-OCaml SigV4 signer +\nauto-refreshing token provider for ElastiCache) + mTLS\n(client-cert constructor) + fuzz + CI + docs.\n\n## Why\n\nExisting OCaml Redis clients predate Valkey, target RESP2, and use\nLwt or Async. This project targets the current era of both stacks:\n\n- **OCaml 5.3+**, Eio-native (effects-based, direct-style concurrency)\n- **RESP3 only** — no RESP2 fallback\n- **Valkey 7.2+**, with first-class support for Valkey 8/9 features\n  (HELLO `availability_zone`, `SET IFEQ`, `DELIFEQ`, hash field\n  TTL, sharded pub/sub)\n\nNo Lwt compat layer. No legacy Redis support.\n\n## Docs\n\n- **New to the library?** Start with [docs/getting-started.md](docs/getting-started.md).\n- **API reference** (odoc) — built by the docs workflow and\n  deployed to GitHub Pages. Run locally with `dune build @doc`;\n  open `_build/default/_doc/_html/valkey/Valkey/index.html`.\n- **Guides** for deeper topics:\n  - [docs/cluster.md](docs/cluster.md) — topology, MOVED/ASK,\n    Read_from, hashtags, failover walkthrough.\n  - [docs/batch.md](docs/batch.md) — scatter-gather and atomic\n    batches, `mget_cluster` / `mset_cluster` / etc., timeout\n    semantics.\n  - [docs/client-side-caching.md](docs/client-side-caching.md) —\n    `CLIENT TRACKING` modes (Default / BCAST / OPTIN), wire shape,\n    invalidation pipeline, cluster behaviour.\n  - [docs/transactions.md](docs/transactions.md) — MULTI/EXEC,\n    WATCH, when *not* to use transactions.\n  - [docs/pubsub.md](docs/pubsub.md) — regular + sharded pub/sub,\n    auto-resubscribe, delivery guarantees.\n  - [docs/blocking-pool.md](docs/blocking-pool.md) — per-node lease\n    pool for `BLPOP` / `BRPOP` / `XREAD BLOCK` etc.; config knobs,\n    typed errors, topology-change behaviour.\n  - [docs/tls.md](docs/tls.md) — system CAs, dev certs,\n    observed overhead.\n  - [docs/performance.md](docs/performance.md) — one-client model,\n    tuning knobs, profiling.\n  - [docs/troubleshooting.md](docs/troubleshooting.md) — every\n    `Connection.Error.t` case, failover symptoms.\n  - [docs/security.md](docs/security.md) — AUTH, ACLs, TLS, audit\n    of what goes on the wire.\n  - [docs/migration-from-ocaml-redis.md](docs/migration-from-ocaml-redis.md) —\n    side-by-side port guide.\n\n## What you get\n\n### Connection spine\n\n- Auto-reconnect with configurable backoff + jitter; HELLO / AUTH\n  / SETNAME / SELECT replayed on every reconnect.\n- Byte-budget backpressure (not count-based).\n- Always-on circuit breaker with a conservative default.\n- App-level keepalive fibre.\n- Full TLS support (self-signed or system CA bundle).\n- Optional cross-domain split: parser stays on the user's domain;\n  socket I/O runs on a dedicated `Eio.Domain_manager` thread so\n  long parses can't stall the pipeline.\n- Contracts: user command timeouts honoured; commands never\n  silently dropped.\n- `?on_connected` hook — fires after every successful handshake\n  (used by `Pubsub` to replay subscriptions).\n\n### Cluster router\n\n- CRC16-XMODEM slot hashing with hashtag support.\n- `CLUSTER SHARDS` parser (Valkey 9 rich format).\n- Quorum-based topology discovery from seed addresses;\n  canonical-SHA change detection.\n- MOVED / ASK redirect retry (bounded, `ASKING` sent before the\n  retried command on `ASK`).\n- CLUSTERDOWN / TRYAGAIN retry with exponential back-off (up to\n  ~3 s).\n- Interrupted / Closed retry so callers don't see transient\n  tear-downs.\n- Periodic background refresh fibre, wakes early on\n  unknown-address redirects.\n- Seed fallback when the live pool can no longer reach quorum.\n- Typed `Read_from` — Primary, Prefer_replica (random), AZ-affinity\n  with 3-tier fallback (in-AZ replica → any replica → primary).\n- `Target` types for `By_slot` / `By_node` / `Random`;\n  `Fan_target` for `All_nodes` / `All_primaries` / `All_replicas`.\n- **Per-primary atomic mutex** (`Router.atomic_lock_for_slot`) —\n  serialises concurrent MULTI/EXEC blocks (from `Batch ~atomic` or\n  `Transaction`) on the shared pinned connection. Non-atomic\n  traffic bypasses the lock and multiplexes freely.\n- **Standalone = one-shard cluster** — single-node connections go\n  through the same router behind a synthetic topology; dispatch\n  is unified.\n\n### Typed commands\n\n- ~140 typed helpers across strings, counters, TTL, hashes\n  (incl. field TTL), sets, lists, **sorted sets** (ZADD with 6\n  mode variants / ZINCRBY / ZRANK / ZSCORE / ZPOPMIN/MAX /\n  WITHSCORES variants), **bitmaps**, **HLL**, **geo**, **generic\n  keyspace**, **CLIENT admin**, **FUNCTION + FCALL**,\n  **CLUSTER introspection**, **LATENCY**, **MEMORY**, streams\n  (non-blocking + consumer groups + admin), scripting (with\n  automatic `EVALSHA → EVAL` fallback on `NOSCRIPT`), blocking\n  commands.\n- **Per-command default routing** (`Command_spec`): ~230 command\n  + sub-command entries, cross-checked against live\n  `COMMAND INFO` in a test.\n- **Cluster-aware admin**: `SCRIPT LOAD/FLUSH/EXISTS`, `KEYS`,\n  `FUNCTION LOAD/DELETE/FLUSH/LIST` fan to every primary and\n  aggregate so they behave identically in standalone and cluster.\n- **Custom commands** via `Client.custom` / `custom_multi` — any\n  Valkey command (including ones we don't wrap typed-side) routes\n  correctly.\n- **Named / registered commands** via `Named_commands`: register\n  a template once (`[| \"HSET\"; \"$1\"; \"$2\"; \"$3\" |]`) and invoke\n  by name; same for named transactions.\n- **Valkey modules**: `Valkey.Search` wraps Search indexes and\n  queries; `Valkey.Json` wraps production JSON document commands\n  while leaving JSON parsing to the caller's preferred codec;\n  `Valkey.Bloom` wraps Bloom filter add/check/insert/info\n  workflows from Valkey Bundle.\n\n### Batch (scatter-gather + atomic)\n\n- **`Valkey.Batch.t`** — one primitive, two modes:\n  - **Non-atomic** (default): queue heterogeneous commands,\n    `Batch.run` splits by slot, runs a per-slot pipeline in\n    parallel, merges replies in input order. Partial success is\n    the norm; each command gets its own `result`.\n  - **Atomic** (`~atomic:true`): in cluster mode, all keys must\n    hash to one slot\n    (client-side CROSSSLOT validation). Single\n    `WATCH`/`MULTI`/cmds/`EXEC` burst on the slot's primary;\n    returns `Ok (Some results)` on commit, `Ok None` on WATCH\n    abort.\n- **Fan-out commands** (`SCRIPT LOAD`, `FLUSHALL`, `CLUSTER\n  NODES`, …) route through `exec_multi` in non-atomic mode and\n  return `Many (node_id, reply) list` entries; they're rejected\n  at `queue` time in atomic mode.\n- **Wall-clock `?timeout`** applies to the whole batch; completed\n  commands keep their replies, stragglers come back as\n  `One (Error Timeout)`.\n- **Typed cluster helpers**: `Batch.mget_cluster`,\n  `mset_cluster`, `del_cluster`, `unlink_cluster`,\n  `exists_cluster`, `touch_cluster`, `pfcount_cluster` (HLL union\n  across slots via `DUMP` / `RESTORE` / `PFMERGE`).\n- **WATCH guards** for read-modify-write CAS —\n  `Batch.with_watch client [\"k\"] (fun guard -\u003e …)` holds `WATCH`\n  across the closure (and the watched primary's atomic mutex),\n  commits via `Batch.run_with_guard`, guarantees `UNWATCH` on any\n  exit. Matches the classic \"read, decide, maybe commit\" pattern.\n- **Concurrent atomic batches are safe** — `Router.atomic_lock_for_slot`\n  serialises MULTI/EXEC blocks on the shared connection; ops on\n  different primaries run in parallel.\n\nSee [docs/batch.md](docs/batch.md).\n\n### Transactions\n\n- `Valkey.Transaction.begin_ / queue / exec / discard` +\n  `with_transaction` scope helper — thin façade over atomic\n  `Batch` as of 0.2.0 (one primitive, one mental model).\n- Buffered model: bad-arity / unknown-command errors surface\n  inside `exec`'s reply array, not at `queue` time. Fan-out\n  commands are still rejected at `queue` with a `Terminal` error.\n- `~watch` opens a `Batch.guard`, so the watched primary's atomic\n  mutex is held for the whole read-modify-exec window.\n- `exec` returns `(Resp3.t list option, Error.t) result` —\n  `Ok None` on WATCH abort; a fresh retry loop is the expected\n  response.\n\n### Client-side caching\n\nServer-invalidated CSC, on standalone **and** cluster, in all\nthree tracking modes Valkey supports. Configure via a\n`Client_cache.t` on the connection config:\n\n```ocaml\nlet cache = Valkey.Cache.create ~byte_budget:(64 * 1024 * 1024) in\nlet ccfg = Valkey.Client_cache.make ~cache ~mode:Default () in\nlet cfg =\n  { Valkey.Client.Config.default with\n    connection =\n      { Valkey.Client.Config.default.connection with\n        client_cache = Some ccfg } }\nin\nlet client = Valkey.Client.connect ~sw ~net ~clock ~config:cfg ~host ~port () in\nmatch Valkey.Client.get client \"user:42\" with ...\n```\n\n- **`mode = Default`** (recommended) — server-side per-connection\n  tracking table; `Client.get` / `mget` / `hgetall` / `smembers`\n  populate the cache on miss, return from cache on hit, evict\n  on the server's invalidation push.\n- **`mode = Bcast { prefixes }`** — prefix-broadcast tracking.\n  Smaller server-side state at the cost of broader invalidation\n  fan-in.\n- **`mode = Optin`** — pipelined per-read tracking\n  (`CLIENT CACHING YES` + read as one wire-atomic submit). Even\n  smaller server-side table; one extra wire frame per cached\n  read. On cluster, the pair retries the whole submit on the\n  new owner across MOVED.\n\nInvalidations land on a dedicated RESP3 push stream; an\ninvalidator fiber drains them per-connection. Single-flight\ndedups concurrent fetches; an in-flight invalidation flips a\ndirty flag so the post-fetch put is skipped. Cache flushes on\nevery per-connection reconnect AND on topology refresh\n(coarse but correct, matches redis-py).\n\nSee [docs/client-side-caching.md](docs/client-side-caching.md)\nfor the wire-level shape, edge-case behaviour, and the empirical\nfindings against Valkey 9 that the implementation is grounded in.\n\n### Pub/sub\n\nTwo handles that cover the whole pub/sub surface:\n\n- **`Pubsub.t`** — dedicated subscriber connection. Typed\n  `message` variants (Channel / Pattern / Shard). Tracks the\n  subscription set under a mutex; on every reconnect, the\n  `on_connected` hook replays `SUBSCRIBE` / `PSUBSCRIBE` /\n  `SSUBSCRIBE`.\n- **`Cluster_pubsub.t`** — cluster-aware. One handle covers\n  regular pub/sub (global connection, broadcast across shards on\n  Valkey 7+) and sharded pub/sub (one pinned connection per\n  subscribed slot). A watchdog fibre polls\n  `Router.endpoint_for_slot` every second; when a primary\n  changes (failover), the shard connection is closed, reopened at\n  the new address, and `on_connected` replays the slot's\n  `SSUBSCRIBE` set. Verified by an integration test that\n  `docker restart`s every primary in sequence and asserts\n  post-failover delivery.\n\nPublish side has typed `Client.publish` (cluster-wide broadcast)\nand `Client.spublish` (slot-pinned).\n\n### Testing, fuzzing, chaos\n\n- **Release gate split**:\n  - `EIO_BACKEND=posix dune runtest` covers pure units with no\n    server dependency.\n  - `EIO_BACKEND=posix dune exec test/run_tests.exe` runs the full\n    live suite when the standalone, cluster, and Valkey Bundle\n    services are up.\n    The current v0.4.0 prep run passed 391 tests, including\n    Search, JSON, and Bloom module integrations against\n    `valkey-bundle`.\n- **Parser fuzzer** (`bin/fuzz_parser/`) — byte-level + tree\n  mutation + length-field poisoning + shrinker. 10 M strict\n  clean at ~145 k inputs/s.\n- **Stability fuzzer** (`bin/fuzz/`) — live-server soak with 48\n  commands, optional docker-restart chaos, zero-error thresholds.\n- **Soak tool** (`bin/soak/`) — long-running stability with\n  `Gc.quick_stat` + `/proc/self/fd` sampling + OLS slope\n  detection.\n- **TCP chaos** via toxiproxy (`docker-compose.toxiproxy.yml` +\n  `scripts/chaos/chaos.sh`) — latency / loss / bandwidth / reset\n  / close toxics on demand.\n- **Retry state-machine tests** — every branch of\n  `handle_retries` covered with scripted dispatch + wall-clock\n  verification of the CLUSTERDOWN exponential back-off schedule.\n- **Round-trip proptest** — `∀ v, parse (encode v) = v` over 10 k\n  random leaves + 10 k random nested trees + edge cases.\n\n### CI / CD\n\n- **`ci.yml`** — Ubuntu × OCaml {5.3, 5.4} full integration\n  (docker cluster + tests + parser fuzz + 30 s standalone fuzz +\n  30 s cluster fuzz); macOS × {5.3, 5.4} docker-free portability\n  subset.\n- **`coverage.yml`** — bisect_ppx instrumented tests, HTML\n  artifact, 60 % floor (baseline 63 %), gh-pages deploy on main.\n- **`fuzz-nightly.yml`** — 02:00 UTC: 200 M parser fuzz strict +\n  15 min cluster stability with docker-restart chaos. Auto-files\n  an issue on failure.\n- **`bench.yml`** — per-PR delta vs `main` with 10 % regression\n  gate; main pushes stash the baseline on `bench-history` branch.\n- **`docs.yml`** — odoc HTML + guides under `/guides/`; gh-pages\n  deploy on main.\n\n### Benchmarks\n\nApples-to-apples with [ocaml-redis](https://github.com/0xffea/ocaml-redis)\n(RESP2, blocking) and `valkey-benchmark` (the C client, as a\nreference ceiling):\n\n| Scenario            |       Ours | ocaml-redis |        C | Ours/C | Ours/ocaml-redis |\n|---------------------|-----------:|------------:|---------:|-------:|-----------------:|\n| SET 100 B conc=1    |  7.3 k r/s |   8.5 k r/s |  8.8 k   |  83 %  |           0.86x  |\n| GET 100 B conc=100  |  199 k r/s |    60 k r/s |  202 k   |  99 %  |         **3.3x** |\n| MIX 1 KiB conc=100  |  110 k r/s |    47 k r/s |    —     |   —    |         **2.3x** |\n| SET 16 KiB conc=10  |   49 k r/s |    26 k r/s |   55 k   |  91 %  |           1.9x   |\n\nAt concurrency ≥ 10 we are **3–3.5× faster than ocaml-redis and\nwithin 85–96 % of the C reference**. Full matrix + methodology +\noptimisation history in [BENCHMARKS.md](BENCHMARKS.md). Run\nlocally with `bash scripts/run-bench.sh`. Batch paths add a\nfurther ≈20× speedup vs per-key loops on 1000-key bulk operations\nin cluster mode (`examples/10-batch/bulk.ml`).\n\n## Installation\n\nRequires OCaml 5.3+ and opam 2.2+.\n\n```sh\nopam update\nopam install valkey eio_main\n```\n\nOr to build from a checkout:\n\n```sh\nopam install . --deps-only --with-test\ndune build\n```\n\n## Quick start\n\n```ocaml\nlet () =\n  Eio_main.run @@ fun env -\u003e\n  Eio.Switch.run @@ fun sw -\u003e\n  let net = Eio.Stdenv.net env in\n  let clock = Eio.Stdenv.clock env in\n  let client =\n    Valkey.Client.connect\n      ~sw ~net ~clock\n      ~host:\"localhost\" ~port:6379 ()\n  in\n\n  let _ = Valkey.Client.set client \"greeting\" \"hello\" in\n  (match Valkey.Client.get client \"greeting\" with\n   | Ok (Some v) -\u003e Printf.printf \"got: %s\\n\" v\n   | Ok None     -\u003e print_endline \"no value\"\n   | Error e     -\u003e\n       Format.printf \"error: %a\\n\" Valkey.Connection.Error.pp e);\n\n  Valkey.Client.close client\n```\n\n### Connecting to a cluster\n\n```ocaml\nlet config =\n  Valkey.Cluster_router.Config.default\n    ~seeds:[ \"node-a.example.com\", 6379;\n             \"node-b.example.com\", 6379;\n             \"node-c.example.com\", 6379 ]\nin\nmatch Valkey.Cluster_router.create ~sw ~net ~clock ~config () with\n| Error m -\u003e failwith m\n| Ok router -\u003e\n    let client =\n      Valkey.Client.from_router ~config:Valkey.Client.Config.default router\n    in\n    let _ = Valkey.Client.set client \"user:42\" \"ada\" in\n    ...\n```\n\nSee [docs/cluster.md](docs/cluster.md).\n\n### Bulk ops across cluster slots\n\n```ocaml\n(* MGET that spans slots — splits by slot, parallel pipelines,\n   merges in input order. *)\nmatch Valkey.Batch.mget_cluster client\n        [ \"user:1\"; \"user:2\"; \"user:3\"; (* ...1000 more... *) ]\nwith\n| Ok pairs -\u003e List.iter (fun (k, v_opt) -\u003e ...) pairs\n| Error e  -\u003e ...\n```\n\nOr a heterogeneous batch:\n\n```ocaml\nlet b = Valkey.Batch.create () in\nlet _ = Valkey.Batch.queue b [| \"SET\"; \"a\"; \"1\" |] in\nlet _ = Valkey.Batch.queue b [| \"INCR\"; \"ctr\" |] in\nlet _ = Valkey.Batch.queue b [| \"HSET\"; \"h\"; \"k\"; \"v\" |] in\nlet _ = Valkey.Batch.queue b [| \"GET\"; \"a\" |] in\nmatch Valkey.Batch.run ~timeout:2.0 client b with\n| Ok (Some results) -\u003e Array.iter decode results\n| _ -\u003e ...\n```\n\nSee [docs/batch.md](docs/batch.md).\n\n### Transactions\n\n```ocaml\nmatch\n  Valkey.Transaction.with_transaction client ~hint_key:\"user:42\" @@ fun tx -\u003e\n  let _ = Valkey.Transaction.queue tx [| \"HSET\"; \"user:42\"; \"seen\"; \"now\" |] in\n  let _ = Valkey.Transaction.queue tx [| \"EXPIRE\"; \"user:42\"; \"3600\" |] in\n  ()\nwith\n| Ok (Some replies) -\u003e (* committed; replies.[i] = i-th queued reply *)\n| Ok None           -\u003e (* WATCH abort — caller decides whether to retry *)\n| Error e           -\u003e (* transport / protocol failure *)\n```\n\nSee [docs/transactions.md](docs/transactions.md).\n\n### Pub/sub (cluster-aware)\n\n```ocaml\nlet cp =\n  Valkey.Cluster_pubsub.create ~sw ~net ~clock ~router ()\nin\nlet _ = Valkey.Cluster_pubsub.ssubscribe cp [ \"orders:created\" ] in\n\nlet rec loop () =\n  match Valkey.Cluster_pubsub.next_message ~timeout:30.0 cp with\n  | Ok (Shard { channel; payload }) -\u003e\n      Printf.printf \"[%s] %s\\n%!\" channel payload;\n      loop ()\n  | Error `Timeout -\u003e loop ()\n  | Error `Closed  -\u003e ()\nin\nloop ()\n```\n\nOn primary failover the watchdog re-pins the slot's connection\nand replays `SSUBSCRIBE` automatically. See\n[docs/pubsub.md](docs/pubsub.md).\n\n### With TLS against a managed service\n\n```ocaml\nlet tls =\n  match Valkey.Tls_config.with_system_cas\n          ~server_name:\"your.redis.amazonaws.com\" () with\n  | Ok t -\u003e t | Error m -\u003e failwith m\nin\nlet config =\n  { Valkey.Client.Config.default with\n    connection = { Valkey.Connection.Config.default with tls = Some tls } }\nin\nlet client = Valkey.Client.connect ~sw ~net ~clock ~config\n               ~host:\"your.redis.amazonaws.com\" ~port:6379 () in\n...\n```\n\nSee [docs/tls.md](docs/tls.md).\n\n## Development setup\n\nRequires: Docker, OCaml 5.3+, opam 2.2+.\n\n```sh\n# One-time: generate self-signed certs for the TLS integration tests\nbash scripts/gen-tls-certs.sh\n\n# Start a local Valkey 9 on :6379 (plain) and :6390 (TLS)\ndocker compose up -d\n\n# Optional: spin up a 3-primary / 3-replica cluster for integration tests\nsudo bash scripts/cluster-hosts-setup.sh     # one-time: /etc/hosts entries\ndocker compose -f docker-compose.cluster.yml up -d\n\n# Start Valkey Bundle modules on :6381 for Search/JSON/Bloom integration tests\ndocker compose -f docker-compose.search.yml up -d\n\n# Build everything + pure-unit tests (no server needed)\ndune build\nEIO_BACKEND=posix dune runtest\n\n# Full integration suite (needs standalone + module services; exercises cluster if started)\nEIO_BACKEND=posix \\\n  VALKEY_SEARCH_PORT=6381 VALKEY_JSON_PORT=6381 VALKEY_BLOOM_PORT=6381 \\\n  dune exec test/run_tests.exe\n```\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for the full developer\nworkflow — fuzzers, bench, coverage, pre-push gate, style rules,\nPR checklist.\n\n## Architecture\n\nFour layers, bottom up:\n\n- **`Connection`** owns one socket and the protocol state\n  machine (`Connecting → Alive → Recovering → Dead`). Pipelines\n  commands through a write fibre and drains replies through a\n  parser fibre; optionally splits I/O across domains. Provides\n  `request` (reply-matched) and `send_fire_and_forget` (no reply\n  expected, used by `Pubsub`).\n- **`Cluster_router`** owns the fleet: topology discovery, node\n  pool, slot dispatch, redirect retry, periodic refresh, seed\n  fallback, typed `Read_from` / `Target` / `Fan_target`,\n  per-primary atomic mutex. Standalone is wrapped as a synthetic\n  single-shard cluster through the same dispatch path.\n- **`Client`** is the typed command surface built on any\n  `Router.t`. Handles `Command_spec`-driven routing, fan-out\n  aggregation, and the `Client.custom` escape hatch.\n- **`Batch`** / **`Transaction`** / **`Pubsub`** /\n  **`Cluster_pubsub`** / **`Named_commands`** sit beside `Client`\n  and use its primitives. Each is a small, focused module with\n  its own integration tests.\n\n## Pre-push gate\n\n`scripts/git-hooks/pre-push` runs `dune build`, the pure test\nsuite with `EIO_BACKEND=posix`, the parser fuzz at 100 k\niterations (strict), the blocking-pool stress test (1000\nconcurrent `BLPOP` callers, `max_per_node=100`), and a\n30-second stability fuzz (both standalone and, if up, the\ncluster) with a **zero-error threshold**. The full live\nstandalone/cluster/Valkey Bundle suite remains a release/CI gate.\nSet it up once:\n\n```sh\nbash scripts/install-git-hooks.sh\n```\n\nOverride knobs:\n- `SKIP_FUZZ=1 git push` — skip fuzz steps (still runs build +\n  tests + parser fuzz + stress).\n- `SKIP_STRESS=1 git push` — skip blocking-pool stress (still\n  runs everything else).\n- `SKIP_PRE_PUSH=1 git push` — emergency escape.\n- `FUZZ_SECONDS=60 git push` — longer fuzz window.\n\n## Roadmap\n\nSee [ROADMAP.md](ROADMAP.md) for the full 12-phase plan. Current\nstate:\n\n- ✅ Phase 0 — core (connection, RESP3, typed client, routing)\n- ✅ Phase 1 — command surface completion\n- ✅ Phase 2 — testing rigour + internal audit ([AUDIT.md](AUDIT.md))\n- ✅ Phase 3 — CI / CD + coverage + bench + nightly fuzz + docs\n- ✅ Phase 4 — documentation (9 guides + CONTRIBUTING + CHANGELOG)\n- ✅ Phase 5 — initial 9 examples + standing rule \"ship features with their example\"\n- ✅ Phase 6 — publishing (v0.2.0 live on opam)\n- ✅ Phase 7 — Batch primitive (atomic + scatter-gather + WATCH\n  guards + cross-slot `pfcount_cluster`) + cluster typed helpers\n- ✅ Phase 8 — client-side caching (`CLIENT TRACKING` + LRU,\n  Default / BCAST / OPTIN, standalone + cluster, server-invalidated)\n- ✅ Phase 9 — blocking pool (narrowed; Client_pool dropped — see\n  [ROADMAP.md](ROADMAP.md))\n- ✅ Phase 10 — IAM (SigV4 signer + 10-min refresh provider) +\n  mTLS (`Tls_config.with_client_cert`)\n- ✅ Phase 11 — module support (`Valkey.Search`, `Valkey.Json`,\n  and `Valkey.Bloom`)\n- ⏳ Phase 12 — deep audit → 1.0.0 stable\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Favifenesh%2Focaml-valkey","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Favifenesh%2Focaml-valkey","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Favifenesh%2Focaml-valkey/lists"}