feat(cluster): CLUSTER REPLICATE + role propagation via gossip - #186
Merged
Conversation
…gossip - add `NodeUpdate::RoleChanged` gossip message variant (tag 6) with binary encode/decode; carries node id, incarnation, is_primary, and optional replicates field - add `GossipEvent::RoleChanged(NodeId, bool, Option<NodeId>)` emitted by apply_updates() when a peer's role changes - add `GossipEngine::queue_role_update()` to piggyback role changes on outgoing pings and acks - add `replicates: Option<NodeId>` to `MemberState` to track which primary each replica mirrors - add `ClusterCoordinator::cluster_replicate()` — validates target, updates local state, queues gossip announcement, saves nodes.conf - add `ClusterCoordinator::is_replica()` and `primary_addr_for_slot()` for replica-aware routing - handle `GossipEvent::RoleChanged` in spawn_gossip to update ClusterState - add `Command::is_write()` to identify mutation commands - add `Command::primary_key()` to extract the first key for slot routing - replace REPLICATE and FAILOVER stubs in connection.rs; REPLICATE now dispatches to cluster_replicate(), FAILOVER returns a clear message - add replica write rejection in execute(): writes on replicas are redirected to the primary via MOVED; reads are served locally - update cluster_slot_check() to skip slot routing on replicas - update integration tests: replicate_stub → targeted error-path tests; failover_stub updated to check new message text
kacy
added a commit
that referenced
this pull request
Feb 19, 2026
…gossip (#186) - add `NodeUpdate::RoleChanged` gossip message variant (tag 6) with binary encode/decode; carries node id, incarnation, is_primary, and optional replicates field - add `GossipEvent::RoleChanged(NodeId, bool, Option<NodeId>)` emitted by apply_updates() when a peer's role changes - add `GossipEngine::queue_role_update()` to piggyback role changes on outgoing pings and acks - add `replicates: Option<NodeId>` to `MemberState` to track which primary each replica mirrors - add `ClusterCoordinator::cluster_replicate()` — validates target, updates local state, queues gossip announcement, saves nodes.conf - add `ClusterCoordinator::is_replica()` and `primary_addr_for_slot()` for replica-aware routing - handle `GossipEvent::RoleChanged` in spawn_gossip to update ClusterState - add `Command::is_write()` to identify mutation commands - add `Command::primary_key()` to extract the first key for slot routing - replace REPLICATE and FAILOVER stubs in connection.rs; REPLICATE now dispatches to cluster_replicate(), FAILOVER returns a clear message - add replica write rejection in execute(): writes on replicas are redirected to the primary via MOVED; reads are served locally - update cluster_slot_check() to skip slot routing on replicas - update integration tests: replicate_stub → targeted error-path tests; failover_stub updated to check new message text
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
summary
implements
CLUSTER REPLICATEand the gossip machinery needed to propagate role changes across the cluster.gossip layer (
ember-cluster)NodeUpdate::RoleChangedvariant (binary tag 6) — carries node id, incarnation,is_primaryflag, and optional primary id for replicas. fully serialized/deserialized alongside the existing update types.GossipEvent::RoleChanged(NodeId, bool, Option<NodeId>)emitted when a peer's role changes and the incarnation is newer than what we've seen.GossipEngine::queue_role_update()queues the update for epidemic dissemination via piggybacking on Ping/Ack messages.replicates: Option<NodeId>added toMemberState.server cluster layer (
ember-server)ClusterCoordinator::cluster_replicate(&str)— validates target exists and is a primary, updates local node role + replicates field, registers self in primary's replica list, queues gossip announcement, saves nodes.conf.ClusterCoordinator::is_replica()andprimary_addr_for_slot(u16)for replica-aware routing.GossipEvent::RoleChangedhandled in the event consumer to keepClusterStatein sync with gossip.CLUSTER REPLICATEstub replaced with the real implementation;CLUSTER FAILOVERstub updated to a clearer "not yet implemented" message.protocol layer (
ember-protocol)Command::is_write()— predicate covering all mutation commands (SET, DEL, INCR, LPUSH, ZADD, HSET, SADD, FLUSHDB, BGSAVE, BGREWRITEAOF, RESTORE, vector and protobuf writes, etc.)Command::primary_key()— returns the first key argument for slot calculation.replica read/write policy
MOVED <slot> <primary_addr>orREADONLY …if no primary found.cluster_slot_check).what was tested
cargo test -p ember-cluster -p ember-protocol -p ember-server)cargo clippy --workspace -- -D warningscleanCLUSTER REPLICATE: unknown node, self-replicate rejection, invalid idapply_role_changed_updates_member,stale_role_changed_ignoredis_write_returns_true_for_mutations,is_write_returns_false_for_reads,primary_key_returns_first_keydesign notes
role changes use incarnation numbers (same mechanism as suspicion refutation) to prevent stale gossip from overwriting newer state. replicas serve reads locally because in most deployments read-scaling is the primary motivation for replication; write redirection via MOVED maintains protocol compatibility with redis cluster clients.