diff --git a/Dockerfile b/Dockerfile index 853c2544..9a4a077f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,4 +41,7 @@ LABEL org.opencontainers.image.title="ember" \ org.opencontainers.image.description="low-latency distributed cache" \ org.opencontainers.image.source="https://github.com/kacy/ember" +HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -qO- http://localhost:9100/health || exit 1 + ENTRYPOINT ["ember-server"] diff --git a/crates/ember-cluster/src/topology.rs b/crates/ember-cluster/src/topology.rs index b309d6da..51846bed 100644 --- a/crates/ember-cluster/src/topology.rs +++ b/crates/ember-cluster/src/topology.rs @@ -350,7 +350,8 @@ impl std::fmt::Display for ClusterHealth { impl ClusterState { /// Creates a new cluster state for a single-node cluster. - pub fn single_node(local_node: ClusterNode) -> Self { + pub fn single_node(mut local_node: ClusterNode) -> Self { + local_node.config_epoch = 1; let local_id = local_node.id; let slot_map = SlotMap::single_node(local_id); let mut nodes = HashMap::new(); diff --git a/crates/ember-server/src/cluster.rs b/crates/ember-server/src/cluster.rs index 63d0e86e..c91b235f 100644 --- a/crates/ember-server/src/cluster.rs +++ b/crates/ember-server/src/cluster.rs @@ -123,12 +123,16 @@ impl ClusterCoordinator { })?; let gossip_addr = SocketAddr::new(bind_addr.ip(), gossip_port); - let gossip = GossipEngine::new(local_id, gossip_addr, gossip_config, event_tx); + let mut gossip = GossipEngine::new(local_id, gossip_addr, gossip_config, event_tx); let state = if bootstrap { let mut node = ClusterNode::new_primary_with_offset(local_id, bind_addr, port_offset); node.set_myself(); - ClusterState::single_node(node) + let cs = ClusterState::single_node(node); + // Populate local_slots so Welcome replies correctly advertise all owned slots + // instead of sending an empty list and triggering a stale SlotsChanged event. + gossip.set_local_slots(cs.slot_map.slots_for_node(local_id)); + cs } else { let mut cs = ClusterState::new(local_id); let mut node = ClusterNode::new_primary_with_offset(local_id, bind_addr, port_offset); @@ -431,6 +435,16 @@ impl ClusterCoordinator { } }; + // Insert placeholder before sending UDP so the gossip receive task always + // finds an entry during MemberJoined resolution. Without this, the Welcome + // reply can arrive and be processed before we add the placeholder, leaving + // both a real entry and a stale placeholder in state.nodes. + { + let mut state = self.state.write().await; + let node = ClusterNode::new_primary_with_offset(new_id, addr, self.gossip_port_offset); + state.add_node(node); + } + // send join message via UDP { let socket = self.udp_socket.lock().await; @@ -444,14 +458,6 @@ impl ClusterCoordinator { } } - // add a placeholder to the routing table so MOVED redirects work - // immediately — the real node ID arrives via gossip and replaces this - { - let mut state = self.state.write().await; - let node = ClusterNode::new_primary_with_offset(new_id, addr, self.gossip_port_offset); - state.add_node(node); - } - self.save_config().await; Frame::Simple("OK".into()) @@ -1617,6 +1623,11 @@ impl ClusterCoordinator { false } GossipEvent::SlotsChanged(id, slots) => { + // The local node is authoritative for its own slot ownership; + // external gossip about it must never overwrite canonical state. + if id == coordinator.local_id { + false + } else { debug!( "cluster: node {} slots changed ({} ranges)", id, @@ -1640,6 +1651,7 @@ impl ClusterCoordinator { } state.update_health(); true + } } GossipEvent::RoleChanged(id, is_primary, replicates) => { debug!( diff --git a/scripts/dev-cluster.sh b/scripts/dev-cluster.sh index 0fd3d4f5..449aa502 100755 --- a/scripts/dev-cluster.sh +++ b/scripts/dev-cluster.sh @@ -103,12 +103,12 @@ cmd_start() { "$CLI" -p "$p1" cluster meet 127.0.0.1 "$p3" >/dev/null echo "assigning slots..." - "$CLI" -p "$p1" cluster addslotsrange 0 5460 >/dev/null + # node-1 already owns slots 0-5460 via bootstrap; nodes 2 and 3 take their ranges "$CLI" -p "$p2" cluster addslotsrange 5461 10922 >/dev/null "$CLI" -p "$p3" cluster addslotsrange 10923 16383 >/dev/null - # give gossip a moment to converge - sleep 0.5 + # give gossip time to fully converge before printing status + sleep 1.5 echo "" "$CLI" -p "$p1" cluster info