Skip to content

Commit 2525876

Browse files
committed
clusterer_controller: announce a join to the whole cluster, not just the joiner
A node that restarts resumes its sequence counters at zero. Its peers still hold the replay window from the previous incarnation, so every packet it sends is 'older than the replay window' and is dropped - silently, because the consumer-plane drop is LM_DBG and production runs at INFO. cl_ctr_handle_member_list() already resets those windows on receipt; the master simply never told anyone. On JOIN_REQ it sent NODE_ASSIGN + KEY_GRANT + a UNICAST MEMBER_LIST to the joiner alone, so only the master itself recovered - it re-creates the peer entry while handling the join, which is why the failure presents as 'only the master can hear the restarted node'. Measured in production: a billing gateway was restarted at 02:07:59; the master logged one '(unicast)' MEMBER_LIST and nothing else all day; the third node accepted ZERO packets from it over the following ten hours (cachedb_perf per-peer replies_from_it=0, last_reply_ago=36154) while happily serving it 106k answers of its own. Its cross-node cache pulls then waited the full 50 ms timeout on every miss - ~8.5/s - which is the 20-25% per-process 'load' the operator saw and reported as high CPU. The master now announces the join to the whole group. The announcement is the existing full-state broadcast (NODE_ASSIGNs + MEMBER_LIST), so receivers need no change; it is coalesced per CL_CTR_RESYNC_MIN_US like RESYNC, but an announcement suppressed by that window is remembered and emitted by the next keepalive tick instead of dropped - coalescing away a second node's join would reproduce the bug for that node. Note the departure signal cannot be relied on to prevent this: a graceful stop sends GOODBYE, and peers then drop the entry and re-create it clean on return - but production recorded no GOODBYE at all from the restarted node (zero lines on both peers), and a crash or a lost packet never sends one. Fail-first rig (three nodes, cluster 777, /dn/run_join2.sh): pump 1400 cluster messages so the sender's sequence passes the 1024-wide window, SIGKILL the node so no GOODBYE is emitted, restart, send 25 more. Unpatched master: the other backup delivered 0 of 25. Patched master: 25 of 25, with 3 'joined - broadcasting full state' announcements. Only the master's binary differs between the arms.
1 parent 600d40d commit 2525876

1 file changed

Lines changed: 75 additions & 12 deletions

File tree

‎modules/clusterer_controller/clusterer_controller.c‎

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,14 @@ typedef struct cl_ctr_cluster_ {
744744
* CL_CTR_RESYNC_MIN_US. */
745745
utime_t last_resync_us;
746746
utime_t last_full_state_us;
747+
/* A full-state announcement that cl_ctr_announce_full_state() had to
748+
* coalesce away, still owed to the cluster. Only the master sets it; the
749+
* keepalive tick flushes it once the window has passed. A join must never
750+
* be merely dropped the way a RESYNC can be: RESYNC is a repair request
751+
* that its sender will repeat, while a join is a one-shot event whose
752+
* whole point is telling the OTHER members that this node restarted. */
753+
int full_state_pending;
754+
char full_state_why[64];
747755
/* Master-side per-IP table tracking bootstrap-decrypt failures.
748756
* Worker-local (no shm, no lock needed). After CL_CTR_JOIN_FAIL_LIMIT
749757
* failures from the same source IP the master sends JOIN_REJECT. */
@@ -1438,6 +1446,7 @@ static void cl_ctr_membership_digest(cl_ctr_cluster_t *cl, uint16_t *count, uint
14381446
static void cl_ctr_send_resync(int sock, cl_ctr_cluster_t *cl,
14391447
const struct sockaddr *dest, socklen_t destlen);
14401448
static void cl_ctr_broadcast_full_state(cl_ctr_cluster_t *cl);
1449+
static void cl_ctr_announce_full_state(cl_ctr_cluster_t *cl, const char *why);
14411450
static int cl_ctr_on_alive_tfd(int fd, void *param, int was_timeout);
14421451
static void cl_ctr_arm_master_timers(cl_ctr_cluster_t *cl, int i_am_master);
14431452
static int cl_ctr_on_join_tfd(int fd, void *param, int was_timeout);
@@ -4100,27 +4109,56 @@ static void cl_ctr_broadcast_full_state(cl_ctr_cluster_t *cl)
41004109
* master answers, and coalesces a burst of RESYNCs into one full-state
41014110
* re-broadcast per CL_CTR_RESYNC_MIN_US.
41024111
*/
4112+
/**
4113+
* cl_ctr_announce_full_state() - master: publish cluster state to EVERYONE.
4114+
*
4115+
* Master path only. Coalesced to one broadcast per CL_CTR_RESYNC_MIN_US, and
4116+
* an announcement suppressed by that window is REMEMBERED (full_state_pending)
4117+
* so the keepalive tick emits it a moment later instead of losing it.
4118+
*
4119+
* Why the pending flag matters: a MEMBER_LIST is what makes every receiver
4120+
* reset that peer's replay windows (see cl_ctr_handle_member_list()). A node
4121+
* that restarts resumes its sequence counters at zero, so until the others
4122+
* reset, every packet it sends is judged "older than the replay window" and
4123+
* dropped - silently, because the consumer-plane drop is LM_DBG. Measured in
4124+
* production: a gateway restarted, the master unicast the MEMBER_LIST to the
4125+
* joiner ALONE, and the third node black-holed 100% of the restarted node's
4126+
* consumer traffic for ten hours. Its cross-node cache pulls then waited the
4127+
* full timeout on every miss. Coalescing away a second join inside the same
4128+
* window would reproduce exactly that for the second node, which is why a
4129+
* suppressed announcement is deferred rather than dropped.
4130+
*/
4131+
static void cl_ctr_announce_full_state(cl_ctr_cluster_t *cl, const char *why)
4132+
{
4133+
utime_t now = get_uticks();
4134+
4135+
if ((utime_t)(now - cl->last_full_state_us) < CL_CTR_RESYNC_MIN_US) {
4136+
cl->full_state_pending = 1;
4137+
snprintf(cl->full_state_why, sizeof cl->full_state_why, "%s", why);
4138+
LM_DBG("clusterer_controller: [cluster %d] %s - full-state broadcast "
4139+
"coalesced, owed to the cluster\n", cl->cluster_id, why);
4140+
return;
4141+
}
4142+
cl->last_full_state_us = now;
4143+
cl->full_state_pending = 0;
4144+
LM_INFO("clusterer_controller: [cluster %d] %s - broadcasting full state "
4145+
"to the cluster\n", cl->cluster_id, why);
4146+
cl_ctr_broadcast_full_state(cl);
4147+
}
4148+
41034149
static void cl_ctr_handle_resync(const char *sender_ip, cl_ctr_cluster_t *cl)
41044150
{
4105-
int i_am_master;
4106-
utime_t now;
4151+
int i_am_master;
4152+
char why[64];
41074153

41084154
lock_start_read(cl->peers->lock);
41094155
i_am_master = cl_ctr_i_am_master_locked(cl);
41104156
lock_stop_read(cl->peers->lock);
41114157
if (!i_am_master)
41124158
return;
41134159

4114-
now = get_uticks();
4115-
if ((utime_t)(now - cl->last_full_state_us) < CL_CTR_RESYNC_MIN_US) {
4116-
LM_DBG("clusterer_controller: [cluster %d] RESYNC from %s coalesced\n",
4117-
cl->cluster_id, sender_ip);
4118-
return;
4119-
}
4120-
cl->last_full_state_us = now;
4121-
LM_INFO("clusterer_controller: [cluster %d] RESYNC from %s - re-broadcasting "
4122-
"full state\n", cl->cluster_id, sender_ip);
4123-
cl_ctr_broadcast_full_state(cl);
4160+
snprintf(why, sizeof why, "RESYNC from %s", sender_ip);
4161+
cl_ctr_announce_full_state(cl, why);
41244162
}
41254163

41264164
/**
@@ -4968,6 +5006,21 @@ static void cl_ctr_handle_join_req(int sock, const char *payload, int payload_le
49685006
/* the joiner's authoritative snapshot - unicast to it (a member that misses
49695007
* it self-heals via the membership digest; the joiner, via its JOIN_REQ retry). */
49705008
cl_ctr_send_list_pkt(sock, CL_CTR_PKT_MEMBER_LIST, cl, src, src_len);
5009+
5010+
/* ...and tell the REST of the cluster that this node joined. The unicast
5011+
* above serves the joiner only; every other member still holds the state it
5012+
* had before this node restarted - including that node's replay windows,
5013+
* which cl_ctr_handle_member_list() resets when (and only when) a
5014+
* MEMBER_LIST arrives. Without this broadcast a rejoining node's traffic
5015+
* is dropped by everyone except the master, silently and indefinitely; the
5016+
* master itself is unaffected because it re-creates the peer entry here,
5017+
* which is why the failure looks like "only the master can hear it".
5018+
* Coalesced (and deferred, never dropped) inside cl_ctr_announce_full_state. */
5019+
{
5020+
char why[64];
5021+
snprintf(why, sizeof why, "node %s joined", src_ip);
5022+
cl_ctr_announce_full_state(cl, why);
5023+
}
49715024
}
49725025

49735026
/**
@@ -6949,6 +7002,16 @@ static int cl_ctr_on_master_alive_tfd(int fd, void *param, int was_timeout)
69497002
cl_ctr_cluster_t *cl = (cl_ctr_cluster_t *)param;
69507003
cl_ctr_drain_tfd(fd);
69517004
cl_ctr_send_master_alive(cl->sock, cl);
7005+
/* An announcement the coalescing window swallowed is still owed - emit it
7006+
* as soon as the window allows. This timer only runs on the master, which
7007+
* is the only node that announces. */
7008+
if (cl->full_state_pending &&
7009+
(utime_t)(get_uticks() - cl->last_full_state_us) >= CL_CTR_RESYNC_MIN_US) {
7010+
char why[64];
7011+
snprintf(why, sizeof why, "%s", cl->full_state_why);
7012+
cl->full_state_pending = 0;
7013+
cl_ctr_announce_full_state(cl, why);
7014+
}
69527015
/* Emit a bootstrap-key beacon every CL_CTR_MASTER_BEACON_EVERY ticks so any
69537016
* peer master holding a different session key can find us and merge. */
69547017
if (++cl->beacon_tick >= CL_CTR_MASTER_BEACON_EVERY) {

0 commit comments

Comments
 (0)