Skip to content

Commit 148e066

Browse files
committed
clusterer_controller: clusterer's side of the controller, as plain code
The changes PR OpenSIPS#4074 makes to modules/clusterer behind CLUSTERER_CTRL_SUPPORT, with that switch resolved as defined: the controller API (clusterer_ctrl.c/h), per-cluster node ids, the managed sharing tags and BIN messages that do not override the controller. Log and MI strings are ASCII where the PR had em dashes.
1 parent b62e863 commit 148e066

10 files changed

Lines changed: 1106 additions & 106 deletions

File tree

‎modules-contrib/clusterer_controller/clusterer.c‎

Lines changed: 166 additions & 44 deletions
Large diffs are not rendered by default.

‎modules-contrib/clusterer_controller/clusterer_ctrl.c‎

Lines changed: 443 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* clusterer_ctrl.h — Controller API for the clusterer module
3+
*
4+
* Allows an external module (clusterer_controller) to drive the clusterer
5+
* topology at runtime without any DB or static configuration.
6+
*
7+
* Copyright (C) 2026 Yury Kirsanov
8+
* VoIPLine Telecom
9+
*
10+
* This file is part of opensips, a free SIP server.
11+
*
12+
* opensips is free software; you can redistribute it and/or modify
13+
* it under the terms of the GNU General Public License as published by
14+
* the Free Software Foundation; either version 2 of the License, or
15+
* (at your option) any later version.
16+
*/
17+
18+
#ifndef CLUSTERER_CTRL_H
19+
#define CLUSTERER_CTRL_H
20+
21+
#include "../../str.h"
22+
23+
/* clusterer's side of the controller: what the controller calls in it */
24+
typedef struct clusterer_ctrl_binds {
25+
/* register this node in a cluster (created if new) as its current_node;
26+
* call before add_node(), @bin_url is "bin:IP:PORT" */
27+
int (*set_my_identity)(int cluster_id, int node_id, str *bin_url);
28+
29+
/* add a peer at runtime (0, no-op, if present); the ping timer brings up
30+
* its BIN link within one ping interval */
31+
int (*add_node)(int cluster_id, int node_id, str *bin_url);
32+
33+
/* remove a peer at runtime and close its BIN connection;
34+
* -1 if the cluster or node is unknown */
35+
int (*remove_node)(int cluster_id, int node_id);
36+
37+
/* close a node's BIN connection but keep it in the topology */
38+
int (*close_node_conn)(int cluster_id, int node_id);
39+
/* replace this node's optimistic node_id with the master-assigned one
40+
* (0, no-op, if unchanged) */
41+
int (*update_identity)(int cluster_id, int new_node_id, str *bin_url);
42+
43+
/* call in child_init() of every process: re-reads the process-local
44+
* current_id from shm so BIN packets carry the right source node_id */
45+
int (*sync_current_id)(void);
46+
47+
/* activate all BACKUP sharing tags: master only, when a peer departs or
48+
* this node becomes master */
49+
int (*activate_backup_shtags)(int cluster_id);
50+
51+
/* force all local sharing tags to BACKUP regardless of =active,
52+
* at startup when the controller manages tags */
53+
int (*force_backup_shtags)(int cluster_id);
54+
55+
/* block MI clusterer_set_tag_active and $shtag() writes for the cluster so
56+
* they cannot fight controller failover (manage_shtags=1) */
57+
int (*set_shtag_managed)(int cluster_id);
58+
59+
/* re-allow MI/script shtag changes, on adopting a running cluster's
60+
* manage_shtags=0 (on_config_mismatch=adopt) */
61+
int (*unset_shtag_managed)(int cluster_id);
62+
63+
/* cluster_ids with cluster_options use_controller=1: the count, and
64+
* clusterer's own array (valid for the process lifetime) */
65+
int managed_count;
66+
int *managed_ids;
67+
} clusterer_ctrl_binds_t;
68+
69+
/* Set to 1 by load_clusterer_ctrl_binds() when a controller module binds the
70+
* API, so clusterer can warn if use_controller=1 yet no controller registered. */
71+
extern int clusterer_ctrl_bound;
72+
73+
/* called from the controller's mod_init(); -1 if clusterer is not loaded
74+
* or lacks use_controller support */
75+
typedef int (*load_clusterer_ctrl_binds_f)(clusterer_ctrl_binds_t *binds);
76+
77+
int load_clusterer_ctrl_binds(clusterer_ctrl_binds_t *binds);
78+
79+
#endif /* CLUSTERER_CTRL_H */

‎modules-contrib/clusterer_controller/clusterer_mod.c‎

Lines changed: 184 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
#include "clusterer.h"
4040
#include "sync.h"
4141
#include "sharing_tags.h"
42+
#include "clusterer_ctrl.h"
4243
#include "clusterer_evi.h"
4344

4445
int ping_interval = DEFAULT_PING_INTERVAL;
@@ -50,6 +51,105 @@ int current_id = -1;
5051
int db_mode = 1;
5152
int clusterer_enable_rerouting = 1;
5253

54+
int *_current_id_shm = NULL;
55+
int use_controller = 0;
56+
57+
int cl_ctr_stub_ids[64];
58+
int cl_ctr_stub_count = 0;
59+
60+
/* extract "<name>=<int>" from a "key=value, key=value" cluster_options string.
61+
* Returns 0 and sets *out on success, 1 if the key is absent, -1 if malformed. */
62+
static int cl_ctr_opt_int(str *descr, str *name, int *out)
63+
{
64+
char *p, *pe;
65+
str aux;
66+
67+
p = str_strstr(descr, name);
68+
if (!p)
69+
return 1;
70+
p += name->len;
71+
p = q_memchr(p, '=', descr->s + descr->len - p);
72+
if (!p) {
73+
LM_ERR("clusterer: expected '=' after '%.*s' in cluster_options\n",
74+
name->len, name->s);
75+
return -1;
76+
}
77+
p++;
78+
pe = q_memchr(p, ',', descr->s + descr->len - p);
79+
aux.s = p;
80+
aux.len = pe ? pe - p : descr->s + descr->len - p;
81+
str_trim_spaces_lr(aux);
82+
if (aux.len == 0 || str2int(&aux, (unsigned int *)out)) {
83+
LM_ERR("clusterer: bad value for '%.*s' in cluster_options\n",
84+
name->len, name->s);
85+
return -1;
86+
}
87+
return 0;
88+
}
89+
90+
/* "cluster_id=N, use_controller=0|1" (default 0 = native); only use_controller=1
91+
* registers the cluster, whose identity and peers the controller then drives */
92+
static int cl_ctr_add_cluster_options(modparam_t type, void *val)
93+
{
94+
static str cid_prop = str_init("cluster_id");
95+
static str uc_prop = str_init("use_controller");
96+
str descr;
97+
int cid = -1, uc = 0, rc, i;
98+
99+
if (!val || !*(char *)val) {
100+
LM_ERR("clusterer: empty 'cluster_options'\n");
101+
return -1;
102+
}
103+
descr.s = (char *)val;
104+
descr.len = strlen(descr.s);
105+
106+
rc = cl_ctr_opt_int(&descr, &cid_prop, &cid);
107+
if (rc < 0)
108+
return -1;
109+
if (rc == 1 || cid < 1) {
110+
LM_ERR("clusterer: 'cluster_options' requires cluster_id >= 1, "
111+
"e.g. \"cluster_id=1, use_controller=1\"\n");
112+
return -1;
113+
}
114+
115+
rc = cl_ctr_opt_int(&descr, &uc_prop, &uc); /* absent (rc==1) -> uc stays 0 */
116+
if (rc < 0)
117+
return -1;
118+
if (uc != 0 && uc != 1) {
119+
LM_ERR("clusterer: use_controller in 'cluster_options' must be 0 or 1 "
120+
"(cluster_id %d)\n", cid);
121+
return -1;
122+
}
123+
124+
if (uc == 0)
125+
return 0; /* native (the default): nothing to register */
126+
127+
for (i = 0; i < cl_ctr_stub_count; i++)
128+
if (cl_ctr_stub_ids[i] == cid) {
129+
LM_ERR("clusterer: cluster_options declares cluster_id %d as "
130+
"controller-managed more than once\n", cid);
131+
return -1;
132+
}
133+
if (cl_ctr_stub_count >= (int)(sizeof cl_ctr_stub_ids / sizeof cl_ctr_stub_ids[0])) {
134+
LM_ERR("clusterer: too many controller-managed clusters\n");
135+
return -1;
136+
}
137+
cl_ctr_stub_ids[cl_ctr_stub_count++] = cid;
138+
use_controller = 1;
139+
return 0;
140+
}
141+
142+
/* retired interim modparams (never released): kept only to fail with a
143+
* migration hint instead of "unknown parameter" */
144+
static int cl_ctr_retired_param(modparam_t type, void *val)
145+
{
146+
LM_ERR("clusterer: the 'use_controller'/'cluster_id' modparams have been "
147+
"replaced; declare each controller-managed cluster with "
148+
"modparam(\"clusterer\", \"cluster_options\", "
149+
"\"cluster_id=N, use_controller=1\")\n");
150+
return -1;
151+
}
152+
53153
str clusterer_db_url = {NULL, 0};
54154

55155
extern db_con_t *db_hdl;
@@ -106,6 +206,7 @@ int cmd_check_addr(struct sip_msg *msg, int *cluster_id, str *ip_str,
106206
*/
107207

108208
static const cmd_export_t cmds[] = {
209+
{"load_clusterer_ctrl_binds", (cmd_function)load_clusterer_ctrl_binds, {{0,0,0}}, 0},
109210
{"load_clusterer", (cmd_function)load_clusterer, {{0,0,0}}, 0},
110211
{"cluster_broadcast_req", (cmd_function)cmd_broadcast_req, {
111212
{CMD_PARAM_INT,0,0},
@@ -158,6 +259,9 @@ static const param_export_t params[] = {
158259
{"flags_col", STR_PARAM, &flags_col.s },
159260
{"description_col", STR_PARAM, &description_col.s },
160261
{"db_mode", INT_PARAM, &db_mode },
262+
{"cluster_options", STR_PARAM|USE_FUNC_PARAM, (void*)cl_ctr_add_cluster_options},
263+
{"use_controller", INT_PARAM|USE_FUNC_PARAM, (void*)cl_ctr_retired_param},
264+
{"cluster_id", INT_PARAM|USE_FUNC_PARAM, (void*)cl_ctr_retired_param},
161265
{"neighbor_node_info", STR_PARAM|USE_FUNC_PARAM,
162266
(void*)&provision_neighbor},
163267
{"my_node_info", STR_PARAM|USE_FUNC_PARAM,
@@ -389,11 +493,18 @@ static int mod_init(void)
389493
flags_col.len = strlen(flags_col.s);
390494
description_col.len = strlen(description_col.s);
391495

496+
/* no DB URL means no DB-native clusters: db_mode 0, so controller-only and
497+
* static (my_node_info) setups need no explicit db_mode=0 */
498+
if (!clusterer_db_url.s && !db_default_url)
499+
db_mode = 0;
500+
392501
/* only allow the DB URL to be skipped in "P2P discovery" mode */
393502
init_db_url(clusterer_db_url, db_mode == 0);
394503

395-
if (current_id < 1) {
396-
LM_CRIT("Invalid 'my_node_id' parameter\n");
504+
/* controller clusters get their id at runtime: my_node_id is required only
505+
* for native (DB or static) clusters */
506+
if (current_id < 1 && (db_mode != 0 || (cluster_list && *cluster_list != NULL))) {
507+
LM_CRIT("Invalid 'my_node_id' parameter (required for DB or static native clusters)\n");
397508
return -1;
398509
}
399510
if (ping_interval <= 0) {
@@ -421,6 +532,17 @@ static int mod_init(void)
421532
LM_CRIT("Failed to init lock\n");
422533
return -1;
423534
}
535+
{
536+
int *_shm_id = shm_malloc(sizeof(int));
537+
if (!_shm_id) {
538+
LM_CRIT("No shm memory for GET_CURRENT_ID\n");
539+
return -1;
540+
}
541+
/* my_node_id for native clusters; -1 (matches no node) only on a
542+
* controller-only node, whose id comes at runtime */
543+
*_shm_id = (current_id >= 1) ? current_id : -1;
544+
_current_id_shm = _shm_id;
545+
}
424546

425547
/* if statistics are disabled, prevent their registration to core */
426548
if (clusterer_enable_stats==0)
@@ -445,12 +567,14 @@ static int mod_init(void)
445567

446568
if (cl->current_node->node_id != current_id) {
447569
LM_ERR("Bad 'my_node_id' parameter, value: %d different than"
448-
" the node_id property in the 'my_node_info' parameter\n", current_id);
570+
" the node_id property in the 'my_node_info' parameter\n", GET_CURRENT_ID);
449571
goto error;
450572
}
451573
}
452574
}
453575

576+
/* DB-native clusters load even with the controller active (hybrid); the DB
577+
* never holds controller clusters */
454578
if (db_mode) {
455579
/* bind to the mysql module */
456580
if (db_bind_mod(&clusterer_db_url, &dr_dbf)) {
@@ -471,11 +595,51 @@ static int mod_init(void)
471595
LM_ERR("Failed to load info from DB\n");
472596
goto error;
473597
}
474-
475598
dr_dbf.close(db_hdl);
476599
db_hdl = NULL;
477600
}
478601

602+
/* a stub per controller-managed cluster_id, so tm/dialog cl_register_cap()
603+
* works before the controller sets identity; a native overlap fails here */
604+
if (use_controller) {
605+
int _ci;
606+
for (_ci = 0; _ci < cl_ctr_stub_count; _ci++) {
607+
cluster_info_t *_ex;
608+
for (_ex = *cluster_list; _ex; _ex = _ex->next)
609+
if (_ex->cluster_id == cl_ctr_stub_ids[_ci]) {
610+
LM_ERR("clusterer: cluster_id %d is registered as "
611+
"controller-managed (cluster_options use_controller=1) but is "
612+
"already defined via native config "
613+
"(my_node_info/neighbor_node_info/DB) or listed "
614+
"twice - a cluster_id must be unique and either "
615+
"controller-managed or native, not both\n",
616+
cl_ctr_stub_ids[_ci]);
617+
return -1;
618+
}
619+
620+
cluster_info_t *_cl = shm_malloc(sizeof *_cl);
621+
if (!_cl) {
622+
LM_ERR("clusterer: no shm for cluster %d stub\n",
623+
cl_ctr_stub_ids[_ci]);
624+
return -1;
625+
}
626+
memset(_cl, 0, sizeof *_cl);
627+
_cl->cluster_id = cl_ctr_stub_ids[_ci];
628+
_cl->controller_managed = 1;
629+
_cl->lock = lock_alloc();
630+
if (!_cl->lock || lock_init(_cl->lock) == NULL) {
631+
LM_ERR("clusterer: lock_alloc failed for cluster %d\n",
632+
cl_ctr_stub_ids[_ci]);
633+
shm_free(_cl);
634+
return -1;
635+
}
636+
_cl->next = *cluster_list;
637+
*cluster_list = _cl;
638+
LM_INFO("clusterer: pre-created controller-managed cluster %d stub\n",
639+
cl_ctr_stub_ids[_ci]);
640+
}
641+
}
642+
479643
/* register timer */
480644
heartbeats_timer_interval = gcd(ping_interval*1000, ping_timeout);
481645
heartbeats_timer_interval = gcd(heartbeats_timer_interval, node_timeout*1000);
@@ -536,7 +700,8 @@ static int mod_init(void)
536700
/* check if the cluster IDs in the the sharing tag list are valid */
537701
shtag_init_list();
538702
shtag_init_reporting();
539-
shtag_validate_list();
703+
if (!use_controller)
704+
shtag_validate_list();
540705

541706
return 0;
542707
error:
@@ -559,6 +724,16 @@ static int child_init(int rank)
559724
return -1;
560725
}
561726
}
727+
728+
/* rank 1 runs once, after every mod_init: a clusterer_controller would
729+
* have bound the controller API by now */
730+
if (rank == 1 && use_controller && !clusterer_ctrl_bound)
731+
LM_ERR("clusterer: cluster_options declares controller-managed cluster(s) "
732+
"but no clusterer_controller module registered the controller API - "
733+
"they will never obtain a node identity or form. Load the "
734+
"clusterer_controller module, or drop use_controller from "
735+
"cluster_options.\n");
736+
562737
return 0;
563738
}
564739

@@ -893,7 +1068,7 @@ static mi_response_t *clusterer_list_topology(const mi_params_t *params,
8931068
if (!node_item)
8941069
goto error;
8951070

896-
if (add_mi_number(node_item, MI_SSTR("node_id"), current_id) < 0)
1071+
if (add_mi_number(node_item, MI_SSTR("node_id"), GET_CURRENT_ID) < 0)
8971072
goto error;
8981073

8991074
neigh_arr = add_mi_array(node_item, MI_SSTR("Neighbours"));
@@ -925,7 +1100,7 @@ static mi_response_t *clusterer_list_topology(const mi_params_t *params,
9251100
}
9261101

9271102
if (n_info->link_state == LS_UP)
928-
if (add_mi_number(neigh_arr, 0,0, current_id) < 0) {
1103+
if (add_mi_number(neigh_arr, 0,0, GET_CURRENT_ID) < 0) {
9291104
lock_release(n_info->lock);
9301105
goto error;
9311106
}
@@ -1054,7 +1229,7 @@ static mi_response_t *cluster_send_mi(const mi_params_t *params,
10541229
return init_mi_param_error();
10551230
if (node_id < 1)
10561231
return init_mi_error(400, MI_SSTR("Bad value for 'destination'"));
1057-
if (node_id == current_id)
1232+
if (node_id == GET_CURRENT_ID)
10581233
return init_mi_error(400, MI_SSTR("Local node specified as destination"));
10591234

10601235
if (get_mi_string_param(params, "cmd_name", &cmd_name.s, &cmd_name.len) < 0)
@@ -1250,7 +1425,7 @@ static inline void generate_msg_tag(pv_value_t *tag_val, int cluster_id)
12501425
memcpy(tag_val->rs.s, tmp, len);
12511426
tag_val->rs.s[len] = '-';
12521427
tag_val->rs.len = len + 1;
1253-
tmp = int2str(current_id, &len);
1428+
tmp = int2str(GET_CURRENT_ID, &len);
12541429
memcpy(tag_val->rs.s + tag_val->rs.len, tmp, len);
12551430
tag_val->rs.s[tag_val->rs.len + len] = '-';
12561431
tag_val->rs.len += len + 1;

0 commit comments

Comments
 (0)