@@ -48,16 +48,25 @@ import (
4848// here so operators see one canonical reference and so the magic-number
4949// linter doesn't flag repeated literals at the env-parse sites.
5050const (
51- defaultReplication = 3
52- defaultCapacity = 100_000
53- defaultVirtualNodes = 64
54- defaultIndirectK = 2
55- suspectMultiplier = 3 // suspect after = N × heartbeat interval
56- deadMultiplier = 6 // dead after = N × heartbeat interval
57- defaultHintTTL = 30 * time .Second
58- defaultHintReplay = 200 * time .Millisecond
59- defaultHeartbeat = 1 * time .Second
60- defaultRebalance = 250 * time .Millisecond
51+ defaultReplication = 3
52+ defaultCapacity = 100_000
53+ defaultVirtualNodes = 64
54+ defaultIndirectK = 2
55+ suspectMultiplier = 3 // suspect after = N × heartbeat interval
56+ deadMultiplier = 6 // dead after = N × heartbeat interval
57+ defaultHintTTL = 30 * time .Second
58+ defaultHintReplay = 200 * time .Millisecond
59+ defaultHeartbeat = 1 * time .Second
60+ defaultRebalance = 250 * time .Millisecond
61+ // Membership gossip cadence. Without an enabled gossip loop the
62+ // cluster has no path to re-introduce a previously-removed node:
63+ // peers' heartbeats only probe nodes already in their membership
64+ // list, and the Health endpoint is one-way. A graceful drain →
65+ // restart (the canonical operator workflow) leaves the restarted
66+ // node invisible to the rest of the cluster forever. Default 1s
67+ // matches the heartbeat cadence — gossip+heartbeat together
68+ // disseminate membership changes within a couple of ticks.
69+ defaultGossip = 1 * time .Second
6170 clientAPIReadTimeout = 5 * time .Second
6271 clientAPIWriteTimeout = 5 * time .Second
6372 clientAPIIdleTimeout = 60 * time .Second
@@ -85,6 +94,7 @@ type envConfig struct {
8594 Heartbeat time.Duration
8695 IndirectK int
8796 RebalanceInt time.Duration
97+ GossipInt time.Duration
8898}
8999
90100// loadConfig pulls every knob from the environment and applies sane
@@ -122,6 +132,7 @@ func loadConfig() (envConfig, error) {
122132 Heartbeat : envDuration ("HYPERCACHE_HEARTBEAT" , defaultHeartbeat ),
123133 IndirectK : envInt ("HYPERCACHE_INDIRECT_PROBE_K" , defaultIndirectK ),
124134 RebalanceInt : envDuration ("HYPERCACHE_REBALANCE_INTERVAL" , defaultRebalance ),
135+ GossipInt : envDuration ("HYPERCACHE_GOSSIP_INTERVAL" , defaultGossip ),
125136 }
126137
127138 return cfg , nil
@@ -235,6 +246,7 @@ func buildHyperCache(ctx context.Context, cfg envConfig, logger *slog.Logger) (*
235246 backend .WithDistWriteConsistency (backend .ConsistencyQuorum ),
236247 backend .WithDistHeartbeat (cfg .Heartbeat , suspectMultiplier * cfg .Heartbeat , deadMultiplier * cfg .Heartbeat ),
237248 backend .WithDistIndirectProbes (cfg .IndirectK , cfg .Heartbeat / 2 ),
249+ backend .WithDistGossipInterval (cfg .GossipInt ),
238250 backend .WithDistHintTTL (cfg .HintTTL ),
239251 backend .WithDistHintReplayInterval (cfg .HintReplay ),
240252 backend .WithDistRebalanceInterval (cfg .RebalanceInt ),
0 commit comments