{
  "markdown": "# Helix\n\n[![Go Reference](https://pkg.go.dev/badge/github.com/arloliu/helix.svg)](https://pkg.go.dev/github.com/arloliu/helix)\n[![Go Report Card](https://goreportcard.com/badge/github.com/arloliu/helix)](https://goreportcard.com/report/github.com/arloliu/helix)\n\n<div align=\"center\">\n  <img src=\"docs/logo.png\" alt=\"Helix Logo\" height=\"150\" />\n</div>\n\n**Helix** is a high-availability dual-database client library for Go, designed to support \"Shared Nothing\" architecture with active-active dual writes, sticky reads, and asynchronous reconciliation.\n\n## Why \"Helix\"?\n\n**Biomimetic Fault Tolerance.**\n\nHelix is named after the DNA double helix: **two independent strands carrying the same genetic code.**\n\nIn this architecture, your database clusters are the strands. They share nothing—no state, no gossip, no master-slave tether. They exist in parallel universes.\n*   **Dual Writes** replicate the code to both strands simultaneously.\n*   **Sticky Reads** latch onto a single strand for maximum locality.\n*   **Replay** acts as the **repair enzyme**, asynchronously healing \"mutations\" (inconsistencies) when a strand temporarily fails.\n\nIf one strand snaps, the other keeps the organism alive. It's 4 billion years of evolution applied to high-availability engineering. 🧬\n\n## Features\n\n- **Dual Active-Active Writes** - Concurrent writes to two independent clusters for maximum availability\n- **Sticky Read Routing** - Per-client sticky reads to maximize cache hits across clusters\n- **Active Failover** - Immediate failover to secondary cluster on read failures\n- **Replay System** - Asynchronous reconciliation via in-memory queue or NATS JetStream\n- **Strict Writes** - Per-statement opt-in for replay-unsafe writes (counters, list/set append) that surfaces partial failures immediately — see [Strict Write Guide](docs/strict-write.md)\n- **Async Mirror Writes** - Per-statement `Mirror()` opt-in for seamless cluster migrations; async fire-and-forget with durable replay retry and optional out-of-process publisher mode — see [Mirror Guide](docs/mirror.md)\n- **Session Refresh** - Manual or automatic recovery from permanently-dead sessions (cluster restart with port reassignment, DNS rotation) without rebuilding the client — see [Session Refresh Guide](docs/session-refresh.md)\n- **Cluster Event Notification** - Single `WithOnClusterEvent` hook delivers typed alerts for failover, circuit breaker trips, adaptive-write degrade/recover, drain transitions, replay drops, and session refresh — see [Cluster Events Guide](docs/cluster-events.md)\n- **Drop-in Replacement** - Interface-based design mirrors `gocql` API for minimal migration effort\n\n> **CAS/LWT Warning:** Lightweight Transactions (`INSERT ... IF NOT EXISTS`, `ScanCAS`, etc.) are **not safe** in a shared-nothing dual-cluster architecture. Each cluster has an independent Paxos state, so CAS conditions cannot be coordinated across clusters. Do not use Helix for CAS/LWT operations.\n\n## Installation\n\n```bash\ngo get github.com/arloliu/helix\n```\n\n## Quick Start\n\n### CQL (Cassandra/ScyllaDB)\n\nThe examples below use the **v2 adapter**, which is the recommended path for new\ncode. Helix builds it against the `arloliu/cassandra-gocql-driver` fork, and Go\nignores a `replace` directive that lives in a dependency, so your own `go.mod`\nneeds the same line before this compiles:\n\n```\nrequire github.com/apache/cassandra-gocql-driver/v2 v2.1.2\n\nreplace github.com/apache/cassandra-gocql-driver/v2 => github.com/arloliu/cassandra-gocql-driver/v2 v2.6.2-otter\n```\n\nThe v1 adapter (`adapter/cql/v1`, over `github.com/gocql/gocql`) is still\nsupported and needs no `replace` line. It is the older of the two: the fork's\nfault-tolerance work lands in the v2 driver, while v1 follows upstream gocql's\nown pace. Prefer v2 unless you are already on gocql v1.\n\n```go\npackage main\n\nimport (\n    \"log\"\n\n    gocql \"github.com/apache/cassandra-gocql-driver/v2\"\n    \"github.com/arloliu/helix\"\n    v2 \"github.com/arloliu/helix/adapter/cql/v2\"\n    \"github.com/arloliu/helix/policy\"\n    \"github.com/arloliu/helix/replay\"\n)\n\nfunc main() {\n    // Create gocql sessions for both clusters\n    clusterA := gocql.NewCluster(\"cluster-a.example.com\")\n    clusterA.Keyspace = \"myapp\"\n    sessionA, _ := clusterA.CreateSession()\n    defer sessionA.Close()\n\n    clusterB := gocql.NewCluster(\"cluster-b.example.com\")\n    clusterB.Keyspace = \"myapp\"\n    sessionB, _ := clusterB.CreateSession()\n    defer sessionB.Close()\n\n    // Create Helix client\n    client, err := helix.NewCQLClient(\n        v2.NewSession(sessionA),\n        v2.NewSession(sessionB),\n        helix.WithReplayer(replay.NewMemoryReplayer()),\n        helix.WithReadStrategy(policy.NewStickyRead()),\n        helix.WithWriteStrategy(policy.NewConcurrentDualWrite()),\n        helix.WithFailoverPolicy(policy.NewActiveFailover()),\n    )\n    if err != nil {\n        log.Fatal(err)\n    }\n    defer client.Close()\n\n    // Dual-write to both clusters\n    userID := gocql.TimeUUID()\n    err = client.Query(\n        \"INSERT INTO users (id, name, email) VALUES (?, ?, ?)\",\n        userID, \"Alice\", \"alice@example.com\",\n    ).Exec()\n    if err != nil {\n        log.Printf(\"Both clusters failed: %v\", err)\n    }\n    // If only one cluster failed, it's automatically queued for replay\n\n    // Read with sticky routing and failover\n    var name, email string\n    err = client.Query(\n        \"SELECT name, email FROM users WHERE id = ?\",\n        userID,\n    ).Scan(&name, &email)\n    if err != nil {\n        log.Printf(\"Read failed on both clusters: %v\", err)\n    }\n}\n```\n\n## Architecture\n\n```mermaid\n%%{init:{'theme':'neutral'}}%%\nflowchart TD\n    Client[Dual-Session Client]\n\n    subgraph Clusters [Cassandra Clusters]\n        CA[(Cassandra Cluster A)]\n        CB[(Cassandra Cluster B)]\n    end\n\n    subgraph ReplaySys [Replay System]\n        NATS[\"NATS JetStream<br/>(DLQ / Replay Log)\"]\n        Worker[Background Replay Worker]\n    end\n\n    %% Dual Write Path\n    Client -- \"1. Dual Write (Concurrent)\" --> CA\n    Client -- \"1. Dual Write (Concurrent)\" --> CB\n\n    %% Failure Path\n    Client -- \"2. On Failure (e.g., B fails)\" --> NATS\n\n    %% Replay Path\n    NATS -- \"3. Consume Failed Write\" --> Worker\n    Worker -- \"4. Replay Write (Idempotent)\" --> CB\n\n    classDef db fill:#e1f5fe,stroke:#01579b,stroke-width:2px;\n    classDef component fill:#fff9c4,stroke:#fbc02d,stroke-width:2px;\n    class CA,CB db;\n    class NATS,Worker component;\n    %% --- Stylesheet ---\n    classDef app fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#000;\n    classDef db fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:#000;\n    classDef infra fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#000;\n\n    class Client app;\n    class CA,CB db;\n    class NATS,Worker infra;\n```\n\n## Strategies & Policies\n\n### Write Strategies\n\n| Strategy | Description |\n|----------|-------------|\n| `ConcurrentDualWrite` | Writes to both clusters concurrently (default) |\n| `SyncDualWrite` | Writes sequentially (A then B, or B then A) |\n| `AdaptiveDualWrite` | Latency-aware: healthy clusters wait, degraded clusters fire-and-forget |\n\n### Read Strategies\n\n| Strategy | Description |\n|----------|-------------|\n| `StickyRead` | Sticks to one cluster per client instance (default) |\n| `PrimaryOnlyRead` | Always reads from Cluster A |\n| `RoundRobinRead` | Alternates between clusters |\n\n### Failover Policies\n\n| Policy | Description |\n|--------|-------------|\n| `ActiveFailover` | Immediately tries secondary on failure (default) |\n| `CircuitBreaker` | Switches after N consecutive failures |\n| `LatencyCircuitBreaker` | CircuitBreaker + treats slow responses as soft failures |\n\nSee [Strategy & Policy Documentation](docs/strategy-policy.md) for detailed configuration and interaction patterns.\n\n### FallbackRead\n\nWhen a dual-write partially fails and replay hasn't converged yet, a read may return \"not found\" on one cluster even though the data exists on the other. FallbackRead silently checks both clusters before returning not-found.\n\n```go\n// Per-query: critical data only\nerr := client.Query(\"SELECT * FROM users WHERE id = ?\", id).\n    FallbackRead().Scan(&name)\n\n// Per-context: all queries in a request handler\nctx := helix.WithFallbackRead(r.Context())\nerr = client.Query(\"SELECT ...\").ScanContext(ctx, &dest)\n\n// Per-client: all queries on this client\nclient, _ := helix.NewCQLClient(sessionA, sessionB,\n    helix.WithDefaultFallbackRead(true),\n)\n```\n\nUse `helix.IsNotFound(err)` to check results. See [FallbackRead Guide](docs/fallback-read.md) for availability semantics, activation levels, and best practices.\n\n**Multi-row reads** — `SliceMap`, `SliceScan`, and `SliceScanAs[T]` collect all rows into memory and also participate in FallbackRead:\n\n```go\nrows, err := client.Query(\"SELECT * FROM orders WHERE user = ?\", userID).\n    FallbackRead().MaxRows(1_000).SliceMapContext(ctx)\n```\n\nSee [Slice Read Guide](docs/slice-read.md) for all methods, `MaxRows` configuration, the typed `SliceScanAs[T]` helper, and performance notes.\n\n## Cluster Event Notification\n\nRegister one handler to receive typed `types.ClusterEvent` notifications for\noperationally significant transitions — failover, circuit breaker open/close,\nadaptive-write degrade/recover, drain enter/exit, replay drops, and session\nrefresh:\n\n```go\nclient, err := helix.NewCQLClient(sessionA, sessionB,\n    // circuit_breaker_open comes from the failover policy, which is unset by\n    // default — without this option the handler below is never called.\n    helix.WithFailoverPolicy(policy.NewCircuitBreaker()),\n\n    helix.WithOnClusterEvent(func(ev types.ClusterEvent) {\n        if ev.Kind == types.EventCircuitBreakerOpen {\n            alerting.Page(\"cluster degraded\", \"cluster\", string(ev.Cluster))\n        }\n    }),\n)\n```\n\nWhich kinds you receive depends on what else you configure: most are produced\nby an optional component and stay silent when it is absent. The constructor\nlogs one Info line listing any kinds left unreachable by the configuration.\nThe [Cluster Events Guide](docs/cluster-events.md) has a per-kind\nprerequisites table, plus the full event reference, delivery/shutdown\nsemantics, and standalone policy usage.\n\n## Structured Logging (`contrib/log/slog`)\n\n`*log/slog.Logger` already matches `types.Logger` for Debug, Info, Warn and\nError; the bundled adapter adds the one method it lacks, `Fatal`, which logs at\nError level with a `fatal=true` attribute and returns rather than ending the\nprocess — a record your handler filters by level like any other:\n\n```go\nimport (\n    \"log/slog\"\n\n    helixslog \"github.com/arloliu/helix/contrib/log/slog\"\n)\n\nclient, err := helix.NewCQLClient(sessionA, sessionB,\n    helix.WithLogger(helixslog.New(slog.Default())),\n)\n```\n\nWithout `WithLogger` Helix uses a no-op logger, so every startup warning,\ncircuit breaker transition and replay drop is silent. `types.Logger`'s Godoc\nhas the wrapper for zap and other loggers whose signatures differ.\n\n## Duration Histograms (`contrib/metrics/vm`)\n\n**Breaking — dashboard migration required.** The bundled collector now exposes\n`*_duration_seconds` metrics as classic Prometheus histograms\n(`_bucket{le=...}`, `_sum`, `_count`) instead of VictoriaMetrics-native\n`vmrange` histograms. `histogram_quantile()` now works in vanilla Prometheus,\nbut existing `vmrange`-based quantile queries return no data and must be\nrewritten against `le` buckets:\n\n```promql\nhistogram_quantile(0.99, sum(rate(helix_read_duration_seconds_bucket[5m])) by (le))\n```\n\nQueries built on `_sum` and `_count` — average latency, throughput — are\nunaffected; only quantile queries break. See the\n[CHANGELOG](CHANGELOG.md) for the full entry.\n\n## Replay System\n\nHelix provides two replay implementations for handling partial write failures:\n\n| Implementation | Durability | Use Case |\n|---------------|------------|----------|\n| `MemoryReplayer` | Volatile | Development, testing |\n| `NATSReplayer` | Durable | Production (requires NATS JetStream) |\n\nSee [Replay System Documentation](docs/replay-system.md) for detailed usage patterns.\n\n## Configuration Options\n\n### Production Recommendations\n\nFor production dual-cluster deployments, always configure:\n\n| Component | Why It Matters |\n|-----------|----------------|\n| `Replayer` | **Critical**: Without a replayer, partial write failures are lost permanently. Use `NATSReplayer` for durability. |\n| `ReadStrategy` | Improves read performance. `StickyRead` maximizes cache hits by routing reads to the same cluster. |\n| `WriteStrategy` | Controls write behavior. `AdaptiveDualWrite` handles degraded clusters gracefully. |\n| `FailoverPolicy` | Enables automatic read failover. `ActiveFailover` immediately retries on the secondary cluster. |\n\n> **Warning**: A warning is logged if you create a dual-cluster client without a Replayer configured.\n\n### Minimal Production Example\n\n```go\nclient, err := helix.NewCQLClient(\n    v2.NewSession(sessionA),\n    v2.NewSession(sessionB),\n    // REQUIRED for production: enables failure recovery (in-memory, auto-started)\n    helix.WithAutoMemoryWorker(10000),\n\n    // RECOMMENDED: optimizes read/write behavior\n    helix.WithReadStrategy(policy.NewStickyRead()),\n    helix.WithWriteStrategy(policy.NewAdaptiveDualWrite()),\n    helix.WithFailoverPolicy(policy.NewActiveFailover()),\n)\n```\n\nFor durable replay across restarts, use a NATS-backed replayer instead:\n\n```go\nnatsReplayer, err := replay.NewNATSReplayer(js) // js is jetstream.JetStream\nif err != nil {\n    log.Fatal(err)\n}\nclient, err := helix.NewCQLClient(\n    v2.NewSession(sessionA),\n    v2.NewSession(sessionB),\n    helix.WithReplayer(natsReplayer),\n    helix.WithReplayWorker(replay.NewNATSWorker(natsReplayer, executorFunc)),\n    helix.WithReadStrategy(policy.NewStickyRead()),\n    helix.WithWriteStrategy(policy.NewAdaptiveDualWrite()),\n    helix.WithFailoverPolicy(policy.NewActiveFailover()),\n)\n```\n\n### Commonly Used Options\n\n`NewCQLClient` validates root options before starting background components or\nmutating caller-owned strategies, policies, and workers. Invalid root options\nreturn joined `*types.OptionError` values that can be checked with\n`types.IsOptionError` or `errors.As`; mirror mode conflicts also wrap the\nrelevant sentinel error such as `types.ErrMirrorModeConflict`.\n\nThe block below shows the options most deployments set. The complete list of root options,\nwith defaults and the rules `NewCQLClient` enforces, is in the\n[Configuration Reference](docs/configuration.md).\n\n```go\nhelix.NewCQLClient(sessionA, sessionB,\n    // Strategies\n    helix.WithReadStrategy(policy.NewStickyRead(\n        policy.WithStickyReadCooldown(5*time.Minute), // Prevent rapid cluster switching\n    )),\n    helix.WithWriteStrategy(policy.NewConcurrentDualWrite()),\n    helix.WithFailoverPolicy(policy.NewActiveFailover()),\n\n    // Replay\n    helix.WithReplayer(replayer),\n    helix.WithReplayWorker(worker),  // Optional: auto-start worker\n\n    // Observability — wired into every component that accepts one\n    helix.WithLogger(helixslog.New(slog.Default())), // contrib/log/slog adapter; omit the option for a no-op logger\n    helix.WithMetrics(vm.New()),                     // contrib/metrics/vm collector\n    helix.WithClusterNames(\"us_east\", \"us_west\"),    // labels used in metrics and logs\n\n    // Timestamps (critical for idempotency)\n    helix.WithTimestampProvider(func() int64 {\n        return time.Now().UnixMicro()\n    }),\n\n    // Mirror — async per-statement mirroring to a second cluster pair (cluster migrations)\n    helix.WithMirror(mirrorClient),\n    helix.WithMirrorReplayer(replayer),           // durable retry for failed mirror writes\n    // helix.WithMirrorPublisher(natsReplayer),   // out-of-process publisher mode\n\n    // Recovery probe — auto-heal degraded clusters (default-on with AdaptiveDualWrite)\n    helix.WithRecoveryProbe(helix.RecoveryProbe{\n        Interval: 5 * time.Second,\n        Timeout:  2 * time.Second,\n    }),\n    // helix.WithRecoveryProbeDisabled(),  // opt out; use ForceRecover() manually\n\n    // Session refresh — recover from permanently-dead sessions\n    // (cluster restart with port reassignment, DNS rotation) without\n    // rebuilding the client. See docs/session-refresh.md.\n    helix.WithSessionRefresher(func(ctx context.Context, cluster helix.ClusterID, lastErr error) (cql.Session, error) {\n        // Caller code: rebuild gocql session against the cluster's\n        // current endpoint, wrapped with the v2 adapter.\n        return v2.NewSession(rebuildGocqlSession(cluster)), nil\n    }),\n    helix.WithAutoRefresh(),  // Helix-driven refresh on observed dead session\n)\n```\n\n## Examples\n\nSee the [examples](examples/) directory:\n\n- [basic](examples/basic/) - Simple dual-write and read operations\n- [failover](examples/failover/) - Failover behavior demonstration\n- [custom-strategy](examples/custom-strategy/) - Creating custom strategies\n- [replay](examples/replay/) - Replay system usage\n- [mirror](examples/mirror/) - Async mirror write wiring for cluster migrations\n\n## Documentation\n\n- [Configuration Reference](docs/configuration.md) — Every root option with its default and validation rule\n- [Strategy & Policy](docs/strategy-policy.md) — Read/write strategies, failover policies, and `AllowedClusters` operator override\n- [Replay System](docs/replay-system.md) — Queue implementations, replay patterns, and worker configuration\n- [AdaptiveDualWrite Guide](docs/adaptive-dual-write.md) — Latency-aware write strategy: degradation thresholds, fire-and-forget, and recovery probe\n- [Slice Read Guide](docs/slice-read.md) — Bounded multi-row reads: `SliceMap`, `SliceScan`, `MaxRows`, and `SliceScanAs[T]`\n- [FallbackRead Guide](docs/fallback-read.md) — Best-effort dual-cluster reads for critical read-after-write scenarios\n- [Strict Write Guide](docs/strict-write.md) — Replay-unsafe writes: counters, list/set append, tombstone races\n- [Mirror Guide](docs/mirror.md) — Async per-statement mirroring for seamless cluster migrations\n- [Auto-Recovery Guide](docs/auto-recovery.md) — Recovery lifecycle, coordinated drain / re-enable workflow, and operator best practices\n- [Session Refresh Guide](docs/session-refresh.md) — Recover from permanently-dead sessions without rebuilding the client\n- [Cluster Events Guide](docs/cluster-events.md) — `WithOnClusterEvent` notification hook: event reference, delivery/shutdown semantics, standalone policy usage\n- [Simulation Guide](docs/simulation_guide.md) — Behavioral test harness for multi-cluster failure scenarios\n\n## Requirements\n\n- Go 1.26+\n- For CQL: v2 (recommended): `github.com/apache/cassandra-gocql-driver`, or v1: `github.com/gocql/gocql`\n- Helix builds the v2 adapter against the `arloliu/cassandra-gocql-driver` fork (tag `v2.6.2-otter`)\n  through a `replace` directive.\n  Go ignores `replace` in dependencies, so a module that uses the v2 adapter must add the same\n  line to its own `go.mod`:\n\n  ```\n  replace github.com/apache/cassandra-gocql-driver/v2 => github.com/arloliu/cassandra-gocql-driver/v2 v2.6.2-otter\n  ```\n\n  The fork lets a caller's context deadline override the connection-level request timeout, so a read\n  leg is no longer capped by `Session.Timeout`. Set `helix.WithClusterReadTimeout(d)` to bound each\n  leg yourself; without it the first cluster can consume the caller's whole budget and read\n  failover never reaches the second cluster. Size `d` by how long a healthy cluster may take to\n  answer, then give callers at least `2*d` so both legs can have their full allowance. Where a leg\n  may hit the driver's reconnect path it returns on the driver's request timeout `r` instead of on\n  `d`, so a caller that must survive that case needs about `r + d`, not `max(2*d, r)`.\n\n  Give writes a caller deadline longer than the driver's own request timeout as well. A write to an\n  unreachable node returns on that timeout even when the context is already cancelled, and a leg\n  that finishes after the caller's deadline is attributed to the caller rather than to the cluster:\n  no health failure is recorded, so `AdaptiveDualWrite` does not degrade the cluster. That is\n  enough for the strategies that run both legs together; `policy.SyncDualWrite` runs them one\n  after another and skips the second once the context has ended, so budget it like a read: about\n  `r + d`.\n- For NATS Replay: `github.com/nats-io/nats.go`\n\n## License\n\nMIT License - see [LICENSE](LICENSE) for details.\n",
  "bytes": 20396,
  "sha": "43ec3c5dd2788ec39d4bbfc02e99de4ec327d5c62a14f24b829356a73ab2f68e",
  "repo_slug": "arloliu/helix",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/okf_arloliu_helix_knowledges_index_md_b6e9d4b3/readme"
}