|
| 1 | +# Redis one-phase txn dedup — flip default to on |
| 2 | + |
| 3 | +Status: Proposed |
| 4 | +Author: bootjp |
| 5 | +Date: 2026-06-10 |
| 6 | + |
| 7 | +> **Status: proposed.** Flip |
| 8 | +> `adapter.RedisServer.onePhaseTxnDedup` from default-off to default-on, |
| 9 | +> closing the rollout of the option-2 idempotency design landed by |
| 10 | +> [`2026_05_21_proposed_txn_secondary_idempotency.md`][parent]. No new |
| 11 | +> mechanism — the FSM probe (`dedupProbeOnePhase` in `kv/fsm.go`), the |
| 12 | +> wire change (`TxnMeta.PrevCommitTS`, V2-only when non-zero), and every |
| 13 | +> adapter-side reuse path (RPUSH/LPUSH, MULTI/EXEC, standalone SET, etc.) |
| 14 | +> already ship and have been exercised by the dedup-mode Jepsen workflow |
| 15 | +> for **12 consecutive green runs (2026-05-31 → 2026-06-10)**, exceeding |
| 16 | +> the parent design's `M4` 7-day criterion. The rolling-upgrade reader |
| 17 | +> (probe code) has been on every node in production for months; only the |
| 18 | +> writer (emission default) remains. |
| 19 | +> |
| 20 | +> Triggered by Jepsen Scheduled Stress run |
| 21 | +> [27033231956](https://github.com/bootjp/elastickv/actions/runs/27033231956/job/79790587770) |
| 22 | +> on 2026-06-05 — the **dedup-OFF control baseline workflow** |
| 23 | +> ([`jepsen-test-scheduled.yml`](../../.github/workflows/jepsen-test-scheduled.yml)) |
| 24 | +> surfaced `:duplicate-elements`, `:future-read`, `:G-single-item-realtime`, |
| 25 | +> and `:G2-item-realtime` on the Redis list-append workload, exactly the |
| 26 | +> anomaly shape the parent design was written to prevent. Issue |
| 27 | +> [#937](https://github.com/bootjp/elastickv/issues/937) tracks that |
| 28 | +> failure; this proposal closes it by retiring the unprotected default. |
| 29 | +
|
| 30 | +[parent]: 2026_05_21_proposed_txn_secondary_idempotency.md |
| 31 | + |
| 32 | +## Background |
| 33 | + |
| 34 | +The parent design landed an FSM-side dedup probe and an adapter-side |
| 35 | +write-set reuse path keyed on a stale `commit_ts` ridden through |
| 36 | +`OperationGroup.PrevCommitTS` into a V2 `TxnMeta`. The probe is always |
| 37 | +present; emission of `prev_commit_ts != 0` is gated by |
| 38 | +`RedisServer.onePhaseTxnDedup` (constructor: `WithOnePhaseTxnDedup`, env: |
| 39 | +`ELASTICKV_REDIS_ONEPHASE_DEDUP=1`). The gate stays default-off until |
| 40 | +the cluster has uniformly upgraded — the parent's R5 "ship the reader |
| 41 | +before the writer" sequencing. |
| 42 | + |
| 43 | +Two Jepsen workflows run the same stress profile (`--time-limit 150 |
| 44 | +--rate 10 --concurrency 8 --key-count 16 --max-writes-per-key 250 |
| 45 | +--max-txn-length 4`) every day against `main`: |
| 46 | + |
| 47 | +| Workflow | Env | Purpose | |
| 48 | +|---|---|---| |
| 49 | +| [`jepsen-test-scheduled.yml`][off] | `ELASTICKV_REDIS_ONEPHASE_DEDUP` unset (off) | Legacy-path baseline — expected to surface the parent's anomaly class until default-on lands. | |
| 50 | +| [`jepsen-test-scheduled-dedup.yml`][on] | `ELASTICKV_REDIS_ONEPHASE_DEDUP=1` (on) | M4 validation — must stay green to authorize default-on. | |
| 51 | + |
| 52 | +[off]: ../../.github/workflows/jepsen-test-scheduled.yml |
| 53 | +[on]: ../../.github/workflows/jepsen-test-scheduled-dedup.yml |
| 54 | + |
| 55 | +## M4 evidence |
| 56 | + |
| 57 | +The parent design's `M4` criterion is *"7 consecutive days without |
| 58 | +`:duplicate-elements` / `:G-single-item-realtime` in the dedup-mode |
| 59 | +workflow."* |
| 60 | + |
| 61 | +Dedup-mode (`jepsen-test-scheduled-dedup.yml`) run history on `main`: |
| 62 | + |
| 63 | +| Date (UTC) | Run | Conclusion | |
| 64 | +|---|---|---| |
| 65 | +| 2026-06-10 04:50 | [27253991270](https://github.com/bootjp/elastickv/actions/runs/27253991270) | success | |
| 66 | +| 2026-06-09 04:44 | [27184402445](https://github.com/bootjp/elastickv/actions/runs/27184402445) | success | |
| 67 | +| 2026-06-08 04:57 | [27116904871](https://github.com/bootjp/elastickv/actions/runs/27116904871) | success | |
| 68 | +| 2026-06-07 04:54 | [27083142341](https://github.com/bootjp/elastickv/actions/runs/27083142341) | success | |
| 69 | +| 2026-06-06 04:40 | [27052820868](https://github.com/bootjp/elastickv/actions/runs/27052820868) | success | |
| 70 | +| 2026-06-05 04:49 | [26996058409](https://github.com/bootjp/elastickv/actions/runs/26996058409) | success | |
| 71 | +| 2026-06-04 04:57 | [26931749014](https://github.com/bootjp/elastickv/actions/runs/26931749014) | success | |
| 72 | +| 2026-06-04 04:16 | [26930308744](https://github.com/bootjp/elastickv/actions/runs/26930308744) | success | |
| 73 | +| 2026-06-03 04:58 | [26864667493](https://github.com/bootjp/elastickv/actions/runs/26864667493) | success | |
| 74 | +| 2026-06-02 04:56 | [26799333263](https://github.com/bootjp/elastickv/actions/runs/26799333263) | success | |
| 75 | +| 2026-06-01 04:58 | [26736041841](https://github.com/bootjp/elastickv/actions/runs/26736041841) | success | |
| 76 | +| 2026-05-31 04:52 | [26703624971](https://github.com/bootjp/elastickv/actions/runs/26703624971) | success | |
| 77 | + |
| 78 | +12 consecutive green runs over 10 calendar days, on the exact stress |
| 79 | +profile that produced the parent design's trigger anomaly. M4 is met. |
| 80 | + |
| 81 | +The control workflow (`jepsen-test-scheduled.yml`, gate off) continues |
| 82 | +to surface `:duplicate-elements` and the related real-time cycle |
| 83 | +anomalies as expected; the 2026-06-05 18:37 failure ([#937]) is the |
| 84 | +most recent observation. The control's purpose ends when default-on |
| 85 | +lands — see *§Control workflow disposition* below. |
| 86 | + |
| 87 | +## Proposal |
| 88 | + |
| 89 | +### M1 — Flip the default |
| 90 | + |
| 91 | +`adapter/redis.go` constructs the server with: |
| 92 | + |
| 93 | +```go |
| 94 | +onePhaseTxnDedup: os.Getenv("ELASTICKV_REDIS_ONEPHASE_DEDUP") == "1", |
| 95 | +``` |
| 96 | + |
| 97 | +After this PR: |
| 98 | + |
| 99 | +```go |
| 100 | +// Default on; set ELASTICKV_REDIS_ONEPHASE_DEDUP=0 to opt out. |
| 101 | +onePhaseTxnDedup: os.Getenv("ELASTICKV_REDIS_ONEPHASE_DEDUP") != "0", |
| 102 | +``` |
| 103 | + |
| 104 | +The env-var sense inverts from opt-in to opt-out. `=1` (the dedup-mode |
| 105 | +workflow's existing setting) and unset both resolve to `true`; only the |
| 106 | +explicit `=0` opts out. `WithOnePhaseTxnDedup(bool)` continues to be the |
| 107 | +constructor-level override and trumps the env var either way. |
| 108 | + |
| 109 | +Comment and `WithOnePhaseTxnDedup`'s godoc are updated to describe the |
| 110 | +new default; the in-file pointer to R5 stays (now reframed as "the |
| 111 | +ordering constraint that authorized this default flip"). |
| 112 | + |
| 113 | +### M2 — Control workflow disposition |
| 114 | + |
| 115 | +After default-on, `jepsen-test-scheduled.yml` would silently exercise |
| 116 | +the same path as `jepsen-test-scheduled-dedup.yml` (unset env → true), |
| 117 | +so the two workflows would collapse to the same coverage. Two options: |
| 118 | + |
| 119 | +| Option | Effect | Recommendation | |
| 120 | +|---|---|---| |
| 121 | +| **A** Keep the control by setting `ELASTICKV_REDIS_ONEPHASE_DEDUP=0` in `jepsen-test-scheduled.yml`'s job env. | Continues to publish the unprotected-path baseline; lets us notice if anomalies on the off-path ever vanish (suggesting the workload regressed in coverage). | Yes for the first 30 days post-flip. After that, retire if signal stays unchanged. | |
| 122 | +| **B** Delete `jepsen-test-scheduled.yml` entirely. | One fewer cron consuming runners. Loses the legacy-path baseline. | No, until 30 days of post-flip data shows the dedup-mode workflow is the only signal we read. | |
| 123 | + |
| 124 | +This PR picks **A**: add explicit `ELASTICKV_REDIS_ONEPHASE_DEDUP: "0"` |
| 125 | +to `jepsen-test-scheduled.yml`'s top-level `env:` so the control retains |
| 126 | +its meaning across the default flip. The 30-day retirement decision |
| 127 | +becomes a follow-up issue. |
| 128 | + |
| 129 | +### M3 — Issue #937 closure |
| 130 | + |
| 131 | +Update [#937](https://github.com/bootjp/elastickv/issues/937) with the |
| 132 | +M4 evidence + this proposal link, then close as "expected baseline; the |
| 133 | +dedup default flip in this PR is the fix." The audit hypothesis from |
| 134 | +the issue body ("client-retry across connection-close bypasses the |
| 135 | +probe") is **incorrect**: `PrevCommitTS` rides on the request envelope, |
| 136 | +not on the client TCP connection, so any retry that re-enters the |
| 137 | +adapter's `retryRedisWrite` loop (the path Jepsen exercises) carries |
| 138 | +the probe regardless of whether the underlying connection closed. The |
| 139 | +12-day green dedup-on streak on the identical workload demonstrates |
| 140 | +the existing mechanism closes the gap; no new design surface is |
| 141 | +required. |
| 142 | + |
| 143 | +## R-references reused from the parent |
| 144 | + |
| 145 | +This proposal does not introduce new risks; every risk from the parent |
| 146 | +applies unchanged. The two that change posture under default-on: |
| 147 | + |
| 148 | +- **R5 (FSM determinism across a rolling upgrade) — discharged.** The |
| 149 | + probe code shipped on every node in production months ago. A node |
| 150 | + receiving a V2 `TxnMeta` with `prev_commit_ts != 0` is now uniformly |
| 151 | + capable of running the probe and producing the same apply outcome as |
| 152 | + every peer. The ordering constraint that gated this default flip is |
| 153 | + satisfied; the constraint stays documented so the same sequencing is |
| 154 | + reused for future probe extensions (e.g. extending dedup to a new |
| 155 | + command family). |
| 156 | +- **R6 (Operator rollback) — preserved.** Setting |
| 157 | + `ELASTICKV_REDIS_ONEPHASE_DEDUP=0` reverts to the pre-flip behaviour. |
| 158 | + This is a single-env-var change; no binary rollback required. The |
| 159 | + rollback contract is documented in the godoc on |
| 160 | + `WithOnePhaseTxnDedup` and in the comment at the env-var read site. |
| 161 | + |
| 162 | +The parent's R1–R4 (result reconstruction on dedup no-op, retry-window |
| 163 | +window, MVCC visibility, store-side compaction) are unaffected by the |
| 164 | +default flip and do not need re-evaluation; the same code paths now |
| 165 | +just run in production by default. |
| 166 | + |
| 167 | +## Test plan |
| 168 | + |
| 169 | +- Unit: keep the existing `adapter/redis_*_dedup_test.go` suites green |
| 170 | + unchanged. Confirm that tests which currently rely on the gate being |
| 171 | + off either pass the explicit `WithOnePhaseTxnDedup(false)` option or |
| 172 | + `t.Setenv("ELASTICKV_REDIS_ONEPHASE_DEDUP", "0")` — audit and fix in |
| 173 | + the implementation commit. |
| 174 | +- Lint: `golangci-lint run` clean. |
| 175 | +- CI: the per-PR Jepsen run (`jepsen-test.yml`) and both scheduled |
| 176 | + stress workflows must stay green for the first run after merge. |
| 177 | +- Manual operator-check: a smoke run of the local Jepsen script |
| 178 | + (`./scripts/run-jepsen-local.sh`) without any env tweaks, confirming |
| 179 | + the gate is now on by default (look for the |
| 180 | + `r.onePhaseTxnDedup == true` branch in the adapter's startup log |
| 181 | + line, or inspect via the `RedisServer.onePhaseTxnDedup` field through |
| 182 | + a probe metric — if no metric exists today, log the active gate at |
| 183 | + `NewRedisServer` start; this is an optional sub-task). |
| 184 | + |
| 185 | +## Out of scope |
| 186 | + |
| 187 | +- **DynamoDB default-on.** Parallel work tracked by |
| 188 | + [`2026_06_03_partial_dynamodb_onephase_dedup.md`][dyn-doc]'s `M2`. |
| 189 | + The DynamoDB dedup-mode workflow has its own 7-day criterion that |
| 190 | + this proposal does not certify. A separate proposal will land the |
| 191 | + DynamoDB flip once its own M2 is met. |
| 192 | +- **S3, SQS dedup paths.** Not yet routed through one-phase reuse; the |
| 193 | + parent design's option-2 mechanism does not cover them. Out of scope |
| 194 | + for this PR; revisit when those adapters add reuse paths. |
| 195 | +- **Removing the env var.** Keep the opt-out env var indefinitely as a |
| 196 | + fast operator rollback. A future cleanup may remove it once |
| 197 | + operational confidence is high; not in this PR. |
| 198 | + |
| 199 | +[dyn-doc]: 2026_06_03_partial_dynamodb_onephase_dedup.md |
| 200 | + |
| 201 | +## Rollout |
| 202 | + |
| 203 | +One PR, two commits: |
| 204 | + |
| 205 | +1. **Doc commit**: add this file. Reviewable on its own — no behavior |
| 206 | + change. |
| 207 | +2. **Implementation commit**: flip the env-var sense in |
| 208 | + `adapter/redis.go`, update the comment and godoc, add the explicit |
| 209 | + `ELASTICKV_REDIS_ONEPHASE_DEDUP=0` to `jepsen-test-scheduled.yml`'s |
| 210 | + job env, fix any test that previously relied on the default-off |
| 211 | + posture (audit during the commit). |
| 212 | + |
| 213 | +After merge: monitor the next 2–3 daily runs of both scheduled |
| 214 | +workflows. The dedup-mode workflow must stay green; the control |
| 215 | +workflow may or may not surface anomalies — both outcomes are |
| 216 | +informative. Roll back via env var (no binary revert) if anything |
| 217 | +unexpected appears. |
0 commit comments