Skip to content

Commit 0f71df1

Browse files
feat(cluster): profile-aware cluster-replica-validity-factor
Cache renders `cluster-replica-validity-factor 0`, disabling the replica-validity gate so a replica failovers regardless of how stale its last primary contact was (availability-first — avoids a stuck shard under a partition/chaos window where node-timeout*factor elapses and no replica is "valid"). Durable keeps Valkey's default gate (factor 10) so it never promotes an arbitrarily-stale replica and silently loses acked writes. Old, stable directive — no version gate. Overridable via spec.config. Confirmed upstream (valkey-io/valkey-operator #216 / PR #222).
1 parent 779379d commit 0f71df1

3 files changed

Lines changed: 63 additions & 3 deletions

File tree

docs/valkey-compatibility.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ operator does not bundle Valkey.
1515
Probed on k3d. On **9.1+** the operator **uses Atomic Slot Migration**
1616
for all rebalance/reshard operations (version-gated `asmDetectSnippet`,
1717
`--cluster-use-atomic-slot-migration`; on <9.1 — classic key-by-key reshard;
18-
see [adr/0001](adr/0001-atomic-slot-migration.md)). On 9.x it also renders
19-
version-gated resilience directives: `cluster-allow-replica-migration no`,
20-
`shutdown-on-sigterm failover` (≥9.0), `tls-auto-reload-interval` (TLS+≥9.1).
18+
see [adr/0001](adr/0001-atomic-slot-migration.md)). The operator also renders
19+
Cluster resilience directives. Version-agnostic (all versions):
20+
`cluster-allow-replica-migration no`, and — for the **Cache** profile only —
21+
`cluster-replica-validity-factor 0` (availability-first: a stale replica can
22+
still win an election, avoiding a stuck shard; **Durable** keeps Valkey's
23+
default gate so it never promotes a replica that would silently lose acked
24+
writes). Version-gated: `shutdown-on-sigterm failover` (≥9.0) and
25+
`tls-auto-reload-interval` (TLS+≥9.1).
2126
- **7.x** works (the cluster-specific features of 8/9 are not required), but is legacy.
2227

2328
## What to check when bumping the Valkey major

internal/controller/resources.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,21 @@ func renderValkeyConf(vc *cachev1beta1.ValkeyCluster, password string) string {
382382
// on all versions. Confirmed by upstream maintainer (valkey-operator #216).
383383
// Overridable via spec.config.
384384
fmt.Fprintf(&b, "cluster-allow-replica-migration no\n")
385+
// Cache profile is availability-first: disable the replica-validity gate
386+
// (factor 0) so a replica will failover regardless of how stale its last
387+
// contact with the primary was — the same trade-off as
388+
// cluster-require-full-coverage no. The default gate (factor 10) refuses to
389+
// promote a replica once (now - last_primary_contact) exceeds
390+
// cluster-node-timeout * factor + repl-ping-replica-period, which under a
391+
// partition/chaos window can leave NO valid replica and a stuck shard with
392+
// no election. Durable keeps the default gate: promoting an arbitrarily
393+
// stale replica would silently lose acknowledged writes, violating the
394+
// Durable contract. Old, stable directive (accepted on all versions, no
395+
// gate). Overridable via spec.config. Confirmed upstream (valkey-operator
396+
// #216 / PR #222).
397+
if vc.Spec.Profile != cachev1beta1.ProfileDurable {
398+
fmt.Fprintf(&b, "cluster-replica-validity-factor 0\n")
399+
}
385400
// Valkey 9.0+: on SIGTERM a cluster primary does a graceful manual failover
386401
// to an up-to-date replica before shutting down — a node-local safety net
387402
// for OUT-OF-BAND descheduling (node drain / eviction / preemption /

internal/controller/resources_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,46 @@ func TestRenderValkeyConfVersionGatedDirectives(t *testing.T) {
11121112
}
11131113
}
11141114

1115+
func TestRenderValkeyConfReplicaValidityFactor(t *testing.T) {
1116+
mk := func(profile cachev1beta1.Profile, topo cachev1beta1.Topology, image string) *cachev1beta1.ValkeyCluster {
1117+
vc := minimalCR()
1118+
vc.Spec.Topology = topo
1119+
vc.Spec.Shards = ptr.To[int32](3)
1120+
vc.Spec.Profile = profile
1121+
if image != "" {
1122+
vc.Spec.Image = image
1123+
}
1124+
return vc
1125+
}
1126+
1127+
// Cache is availability-first: disable the replica-validity gate so a stale
1128+
// replica can still win an election (avoids a stuck shard). Any Valkey
1129+
// version — old, stable directive, no version gate.
1130+
for _, img := range []string{"valkey/valkey:8.0", "valkey/valkey:9.1"} {
1131+
if c := renderValkeyConf(mk(cachev1beta1.ProfileCache, cachev1beta1.TopologyCluster, img), ""); !strings.Contains(c, "cluster-replica-validity-factor 0") {
1132+
t.Errorf("%s Cache/Cluster must set cluster-replica-validity-factor 0\n%s", img, c)
1133+
}
1134+
}
1135+
1136+
// Durable keeps the default gate (nothing emitted): promoting an
1137+
// arbitrarily-stale replica would silently lose acknowledged writes.
1138+
if c := renderValkeyConf(mk(cachev1beta1.ProfileDurable, cachev1beta1.TopologyCluster, ""), ""); strings.Contains(c, "cluster-replica-validity-factor") {
1139+
t.Errorf("Durable/Cluster must NOT override the validity gate\n%s", c)
1140+
}
1141+
1142+
// Only Cluster topology gets the directive.
1143+
if c := renderValkeyConf(mk(cachev1beta1.ProfileCache, cachev1beta1.TopologyReplication, ""), ""); strings.Contains(c, "cluster-replica-validity-factor") {
1144+
t.Errorf("non-Cluster must not get cluster-replica-validity-factor\n%s", c)
1145+
}
1146+
1147+
// spec.config overrides the operator default (rendered last → wins).
1148+
over := mk(cachev1beta1.ProfileCache, cachev1beta1.TopologyCluster, "")
1149+
over.Spec.Config = map[string]string{"cluster-replica-validity-factor": "10"}
1150+
if c := renderValkeyConf(over, ""); !strings.Contains(c, "cluster-replica-validity-factor 10") {
1151+
t.Errorf("spec.config must be able to override the validity factor\n%s", c)
1152+
}
1153+
}
1154+
11151155
func TestReshardScriptsUseASMFlag(t *testing.T) {
11161156
mkVC := func(perShard bool) *cachev1beta1.ValkeyCluster {
11171157
vc := &cachev1beta1.ValkeyCluster{

0 commit comments

Comments
 (0)