Skip to content

Commit 3d833da

Browse files
author
Nelson Goncalves
committed
BUG#26870329: CLUSTER.REJOININSTANCE() SHOULD ENSURE MEMBER IS NOT ALREADY IN GROUP
The rejoinInstance operation will attempt to rejoin an instance even if the member being rejoined is already part of the group. This patch adds a new check that validates if the instance being added is missing from the group, in which case it is rejoined. Otherwise a runtime error is thrown. Interactive documentation was updated. New tests were added and existing tests were updated.
1 parent f3d798a commit 3d833da

File tree

9 files changed

+283
-9
lines changed

9 files changed

+283
-9
lines changed

modules/adminapi/mod_dba_cluster.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,9 @@ REGISTER_HELP(CLUSTER_REJOININSTANCE_THROWS5, "@throws ArgumentError if the "\
411411
REGISTER_HELP(CLUSTER_REJOININSTANCE_THROWS6, "@throws RuntimeError if the SSL "\
412412
"mode specified is not compatible "\
413413
"with the one used in the cluster.");
414+
REGISTER_HELP(CLUSTER_REJOININSTANCE_THROWS7, "@throws RuntimeError if the "\
415+
"instance is an active member "\
416+
"of the ReplicaSet.");
414417

415418
REGISTER_HELP(CLUSTER_REJOININSTANCE_RETURNS, "@returns nothing.");
416419

@@ -493,6 +496,7 @@ REGISTER_HELP(CLUSTER_REJOININSTANCE_DETAIL25, "The ipWhitelist format is a "\
493496
* $(CLUSTER_REJOININSTANCE_THROWS4)
494497
* $(CLUSTER_REJOININSTANCE_THROWS5)
495498
* $(CLUSTER_REJOININSTANCE_THROWS6)
499+
* $(CLUSTER_REJOININSTANCE_THROWS7)
496500
*
497501
* $(CLUSTER_REJOININSTANCE_RETURNS)
498502
*

modules/adminapi/mod_dba_common.cc

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1075,7 +1075,7 @@ std::string get_gr_replicaset_group_name(
10751075
*
10761076
* @param metadata metadata object which represents the session to the metadata
10771077
* storage, i.e. the current instance session on this case
1078-
* @param rd_id the ReplicaSet id
1078+
* @param rs_id the ReplicaSet id
10791079
* @return a boolean value indicating whether the replicaSet
10801080
* 'group_replication_group_name' is the same as the one registered in the
10811081
* corresponding replicaset in the Metadata
@@ -1158,5 +1158,39 @@ bool validate_super_read_only(
11581158
return super_read_only;
11591159
}
11601160

1161+
/*
1162+
* Checks if an instance can be rejoined to a given ReplicaSet.
1163+
*
1164+
* @param session Object which represents the session to the instance
1165+
* @param metadata Metadata object which represents the session to the metadata
1166+
* storage
1167+
* @param rs_id The ReplicaSet id
1168+
*
1169+
* @return a boolean value which is true if the instance can be rejoined to the
1170+
* ReplicaSet and false otherwise.
1171+
*/
1172+
bool validate_instance_rejoinable(
1173+
mysqlsh::mysql::ClassicSession *instance_session,
1174+
const std::shared_ptr<MetadataStorage> &metadata,
1175+
uint64_t rs_id) {
1176+
std::string instance_uuid;
1177+
// get server_uuid from the instance that we're trying to rejoin
1178+
get_server_variable(instance_session->connection(), "server_uuid",
1179+
instance_uuid);
1180+
// get the list of missing instances
1181+
std::vector<MissingInstanceInfo> missing_instances_list =
1182+
get_unavailable_instances(metadata, rs_id);
1183+
1184+
// Unless the instance is missing, we cannot add it back to the cluster
1185+
auto it =
1186+
std::find_if(missing_instances_list.begin(), missing_instances_list.end(),
1187+
[&instance_uuid](MissingInstanceInfo &info) {
1188+
return instance_uuid == info.id;
1189+
});
1190+
// if the instance is not on the list of missing instances, then it cannot
1191+
// be rejoined
1192+
return it != std::end(missing_instances_list);
1193+
}
1194+
11611195
} // namespace dba
11621196
} // namespace mysqlsh

modules/adminapi/mod_dba_common.h

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
* 02110-1301 USA
1818
*/
1919

20-
#ifndef _MODULES_ADMINAPI_MOD_DBA_COMMON_
21-
#define _MODULES_ADMINAPI_MOD_DBA_COMMON_
20+
#ifndef MODULES_ADMINAPI_MOD_DBA_COMMON_H_
21+
#define MODULES_ADMINAPI_MOD_DBA_COMMON_H_
2222

2323
#include <locale>
2424
#include <string>
2525
#include <vector>
26+
#include <memory>
2627

2728
#include "modules/adminapi/mod_dba_provisioning_interface.h"
2829
#include "modules/mod_mysql_session.h"
@@ -165,6 +166,9 @@ bool SHCORE_PUBLIC validate_replicaset_group_name(
165166
mysqlsh::mysql::ClassicSession *session, uint64_t rs_id);
166167
bool validate_super_read_only(
167168
mysqlsh::mysql::ClassicSession *session, bool clear_read_only);
168-
}
169-
}
170-
#endif
169+
bool validate_instance_rejoinable(
170+
mysqlsh::mysql::ClassicSession *instance_session,
171+
const std::shared_ptr<MetadataStorage> &metadata, uint64_t rs_id);
172+
} // namespace dba
173+
} // namespace mysqlsh
174+
#endif // MODULES_ADMINAPI_MOD_DBA_COMMON_H_

modules/adminapi/mod_dba_replicaset.cc

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,33 @@ shcore::Value ReplicaSet::rejoin_instance(const shcore::Argument_list &args) {
725725
}
726726
}
727727

728+
// Verify if the instance being added is MISSING, otherwise throw an error
729+
// Bug#26870329
730+
{
731+
// get server_uuid from the instance that we're trying to rejoin
732+
classic = dynamic_cast<mysqlsh::mysql::ClassicSession*>(session.get());
733+
if (!validate_instance_rejoinable(classic, _metadata_storage, _id)) {
734+
// instance not missing, so throw an error
735+
std::string get_state_query =
736+
"SELECT MEMBER_STATE FROM "
737+
"performance_schema.replication_group_members as m JOIN "
738+
"performance_schema.replication_group_member_stats as s "
739+
"on m.MEMBER_ID = s.MEMBER_ID "
740+
"AND m.MEMBER_ID = @@server_uuid";
741+
auto result = classic->execute_sql(get_state_query);
742+
auto row = result->fetch_one();
743+
std::string member_state = row->get_value(0).as_string();
744+
std::string nice_error_msg = "Cannot rejoin instance '" +
745+
instance_address + "' with state '" +
746+
member_state +
747+
"' since it "
748+
"is an active member of the ReplicaSet '" +
749+
get_member("name").as_string() + "'.";
750+
session->close(shcore::Argument_list());
751+
throw std::runtime_error(nice_error_msg);
752+
}
753+
}
754+
728755
// Get @@group_replication_local_address
729756
std::string seed_instance_xcom_address;
730757
get_server_variable(classic->connection(), "group_replication_local_address",

unittest/modules/adminapi/mod_dba_common_t.cc

Lines changed: 201 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,7 +1038,7 @@ TEST_F(Dba_common_test, get_unavailable_instances_001) {
10381038
EXPECT_TRUE(unavailable_instances_list.empty());
10391039
} catch (const shcore::Exception &e) {
10401040
SCOPED_TRACE(e.what());
1041-
SCOPED_TRACE("Unexpected failure at get_instances_md");
1041+
SCOPED_TRACE("Unexpected failure at get_unavailable_instances_001");
10421042
ADD_FAILURE();
10431043
}
10441044

@@ -1109,7 +1109,7 @@ TEST_F(Dba_common_test, get_unavailable_instances_002) {
11091109
EXPECT_TRUE(unavailable_instances_list.empty());
11101110
} catch (const shcore::Exception &e) {
11111111
SCOPED_TRACE(e.what());
1112-
SCOPED_TRACE("Unexpected failure at get_instances_md");
1112+
SCOPED_TRACE("Unexpected failure at get_unavailable_instances_002");
11131113
ADD_FAILURE();
11141114
}
11151115

@@ -1245,6 +1245,205 @@ TEST_F(Dba_common_test, validate_replicaset_group_name_002) {
12451245
stop_server_mock(_mysql_sandbox_nport1);
12461246
stop_server_mock(_mysql_sandbox_nport2);
12471247
}
1248+
1249+
TEST_F(Dba_common_test, validate_instance_rejoinable_01) {
1250+
// There are missing instances and the instance we are checking belongs to
1251+
// that list
1252+
1253+
// get_instances_gr():
1254+
//
1255+
// member_id
1256+
// ------------------------------------
1257+
// 851f0e89-5730-11e7-9e4f-b86b230042b9
1258+
// 8a8ae9ce-5730-11e7-a437-b86b230042b9
1259+
1260+
std::vector<tests::Fake_result_data> queries;
1261+
1262+
std::vector<std::vector<std::string>> values;
1263+
values = {{"8fcb92c9-5730-11e7-aa60-b86b230042b9"},
1264+
{"851f0e89-5730-11e7-9e4f-b86b230042b9"}};
1265+
1266+
add_ps_gr_group_members_query(&queries, values);
1267+
1268+
// get_instances_md():
1269+
//
1270+
// member_id
1271+
// ------------------------------------
1272+
// 851f0e89-5730-11e7-9e4f-b86b230042b9
1273+
// 8a8ae9ce-5730-11e7-a437-b86b230042b9
1274+
// 8fcb92c9-5730-11e7-aa60-b86b230042b9
1275+
1276+
values = {{"851f0e89-5730-11e7-9e4f-b86b230042b9"},
1277+
{"8a8ae9ce-5730-11e7-a437-b86b230042b9"},
1278+
{"8fcb92c9-5730-11e7-aa60-b86b230042b9"}};
1279+
1280+
add_md_group_members_query(&queries, values);
1281+
1282+
values = {{"8a8ae9ce-5730-11e7-a437-b86b230042b9", "localhost:3320",
1283+
"localhost:3320"}};
1284+
1285+
add_md_group_members_full_query(
1286+
&queries, "8a8ae9ce-5730-11e7-a437-b86b230042b9", values);
1287+
1288+
START_SERVER_MOCK(_mysql_sandbox_nport1, queries);
1289+
1290+
auto md_session = create_dev_session(_mysql_sandbox_nport1);
1291+
1292+
std::shared_ptr<mysqlsh::dba::MetadataStorage> metadata;
1293+
metadata.reset(new mysqlsh::dba::MetadataStorage(md_session));
1294+
1295+
std::vector<tests::Fake_result_data> instance_queries;
1296+
add_get_server_variable_query(&instance_queries, "server_uuid",
1297+
tests::Type::String,
1298+
"8a8ae9ce-5730-11e7-a437-b86b230042b9");
1299+
START_SERVER_MOCK(_mysql_sandbox_nport2, instance_queries);
1300+
auto instance_session = create_session(_mysql_sandbox_nport2);
1301+
try {
1302+
bool is_rejoinable(
1303+
validate_instance_rejoinable(instance_session.get(), metadata, 1));
1304+
1305+
EXPECT_TRUE(is_rejoinable);
1306+
} catch (const shcore::Exception &e) {
1307+
SCOPED_TRACE(e.what());
1308+
SCOPED_TRACE("Unexpected failure at validate_instance_rejoinable_01");
1309+
ADD_FAILURE();
1310+
}
1311+
1312+
md_session->close(shcore::Argument_list());
1313+
stop_server_mock(_mysql_sandbox_nport1);
1314+
instance_session->close(shcore::Argument_list());
1315+
stop_server_mock(_mysql_sandbox_nport2);
1316+
}
1317+
1318+
TEST_F(Dba_common_test, validate_instance_rejoinable_02) {
1319+
// There are missing instances but the instance we are checking does not
1320+
// belong to that list.
1321+
1322+
// get_instances_gr():
1323+
//
1324+
// member_id
1325+
// ------------------------------------
1326+
// 851f0e89-5730-11e7-9e4f-b86b230042b9
1327+
// 8a8ae9ce-5730-11e7-a437-b86b230042b9
1328+
1329+
std::vector<tests::Fake_result_data> queries;
1330+
1331+
std::vector<std::vector<std::string>> values;
1332+
values = {{"8fcb92c9-5730-11e7-aa60-b86b230042b9"},
1333+
{"851f0e89-5730-11e7-9e4f-b86b230042b9"}};
1334+
1335+
add_ps_gr_group_members_query(&queries, values);
1336+
1337+
// get_instances_md():
1338+
//
1339+
// member_id
1340+
// ------------------------------------
1341+
// 851f0e89-5730-11e7-9e4f-b86b230042b9
1342+
// 8a8ae9ce-5730-11e7-a437-b86b230042b9
1343+
// 8fcb92c9-5730-11e7-aa60-b86b230042b9
1344+
1345+
values = {{"851f0e89-5730-11e7-9e4f-b86b230042b9"},
1346+
{"8a8ae9ce-5730-11e7-a437-b86b230042b9"},
1347+
{"8fcb92c9-5730-11e7-aa60-b86b230042b9"}};
1348+
1349+
add_md_group_members_query(&queries, values);
1350+
1351+
values = {{"8a8ae9ce-5730-11e7-a437-b86b230042b9", "localhost:3320",
1352+
"localhost:3320"}};
1353+
1354+
add_md_group_members_full_query(
1355+
&queries, "8a8ae9ce-5730-11e7-a437-b86b230042b9", values);
1356+
1357+
START_SERVER_MOCK(_mysql_sandbox_nport1, queries);
1358+
1359+
auto md_session = create_dev_session(_mysql_sandbox_nport1);
1360+
1361+
std::shared_ptr<mysqlsh::dba::MetadataStorage> metadata;
1362+
metadata.reset(new mysqlsh::dba::MetadataStorage(md_session));
1363+
1364+
std::vector<tests::Fake_result_data> instance_queries;
1365+
add_get_server_variable_query(&instance_queries, "server_uuid",
1366+
tests::Type::String,
1367+
"8a8ae9ce-5730-11e7-a437-b86b230042b1");
1368+
START_SERVER_MOCK(_mysql_sandbox_nport2, instance_queries);
1369+
auto instance_session = create_session(_mysql_sandbox_nport2);
1370+
try {
1371+
bool is_rejoinable(
1372+
validate_instance_rejoinable(instance_session.get(), metadata, 1));
1373+
1374+
EXPECT_FALSE(is_rejoinable);
1375+
} catch (const shcore::Exception &e) {
1376+
SCOPED_TRACE(e.what());
1377+
SCOPED_TRACE("Unexpected failure at validate_instance_rejoinable_02");
1378+
ADD_FAILURE();
1379+
}
1380+
1381+
md_session->close(shcore::Argument_list());
1382+
stop_server_mock(_mysql_sandbox_nport1);
1383+
instance_session->close(shcore::Argument_list());
1384+
stop_server_mock(_mysql_sandbox_nport2);
1385+
}
1386+
1387+
TEST_F(Dba_common_test, validate_instance_rejoinable_03) {
1388+
// There are no missing instances
1389+
1390+
// get_instances_gr():
1391+
//
1392+
// member_id
1393+
// ------------------------------------
1394+
// 851f0e89-5730-11e7-9e4f-b86b230042b9
1395+
// 8a8ae9ce-5730-11e7-a437-b86b230042b9
1396+
// 8fcb92c9-5730-11e7-aa60-b86b230042b9
1397+
1398+
std::vector<tests::Fake_result_data> queries;
1399+
1400+
std::vector<std::vector<std::string>> values;
1401+
values = {{"851f0e89-5730-11e7-9e4f-b86b230042b9"},
1402+
{"8a8ae9ce-5730-11e7-a437-b86b230042b9"},
1403+
{"8fcb92c9-5730-11e7-aa60-b86b230042b9"}};
1404+
1405+
add_ps_gr_group_members_query(&queries, values);
1406+
1407+
// get_instances_md():
1408+
//
1409+
// member_id
1410+
// ------------------------------------
1411+
// 851f0e89-5730-11e7-9e4f-b86b230042b9
1412+
// 8a8ae9ce-5730-11e7-a437-b86b230042b9
1413+
// 8fcb92c9-5730-11e7-aa60-b86b230042b9
1414+
1415+
add_md_group_members_query(&queries, values);
1416+
1417+
START_SERVER_MOCK(_mysql_sandbox_nport1, queries);
1418+
1419+
auto md_session = create_dev_session(_mysql_sandbox_nport1);
1420+
1421+
std::shared_ptr<mysqlsh::dba::MetadataStorage> metadata;
1422+
metadata.reset(new mysqlsh::dba::MetadataStorage(md_session));
1423+
1424+
std::vector<tests::Fake_result_data> instance_queries;
1425+
add_get_server_variable_query(&instance_queries, "server_uuid",
1426+
tests::Type::String,
1427+
"8a8ae9ce-5730-11e7-a437-b86b230042b9");
1428+
START_SERVER_MOCK(_mysql_sandbox_nport2, instance_queries);
1429+
auto instance_session = create_session(_mysql_sandbox_nport2);
1430+
try {
1431+
bool is_rejoinable(
1432+
validate_instance_rejoinable(instance_session.get(), metadata, 1));
1433+
1434+
EXPECT_FALSE(is_rejoinable);
1435+
} catch (const shcore::Exception &e) {
1436+
SCOPED_TRACE(e.what());
1437+
SCOPED_TRACE("Unexpected failure at validate_instance_rejoinable_03");
1438+
ADD_FAILURE();
1439+
}
1440+
1441+
md_session->close(shcore::Argument_list());
1442+
stop_server_mock(_mysql_sandbox_nport1);
1443+
instance_session->close(shcore::Argument_list());
1444+
stop_server_mock(_mysql_sandbox_nport2);
1445+
}
1446+
12481447
} // namespace tests
12491448

12501449
TEST(mod_dba_common, validate_label) {

unittest/scripts/js_devapi/scripts/dba_cluster_rejoin_instance.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ wait_slave_state(cluster, uri2, "ONLINE");
7575
//@<OUT> Cluster status after rejoin
7676
cluster.status();
7777

78-
// Dissolve the cluser and remove the created accounts
78+
//@ Cannot rejoin an instance that is already in the group (not missing) Bug#26870329
79+
cluster.rejoinInstance({dbUser: 'foo', host: 'localhost', port:__mysql_sandbox_port2}, {password: 'bar'});
7980

8081
//@ Dissolve cluster
8182
cluster.dissolve({force: true})

unittest/scripts/js_devapi/validation/dba_cluster_help.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ EXCEPTIONS
386386
allowed: "AUTO", "DISABLED", "REQUIRED".
387387
RuntimeError: if the SSL mode specified is not compatible with the one used
388388
in the cluster.
389+
RuntimeError: if the instance is an active member of the ReplicaSet.
389390

390391
RETURNS
391392

unittest/scripts/js_devapi/validation/dba_cluster_rejoin_instance.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@
8484
}
8585
}
8686

87+
//@ Cannot rejoin an instance that is already in the group (not missing) Bug#26870329
88+
||Cluster.rejoinInstance: Cannot rejoin instance '<<<localhost>>>:<<<__mysql_sandbox_port2>>>' with state 'ONLINE' since it is an active member of the ReplicaSet 'default'.
89+
8790
//@ Dissolve cluster
8891
||
8992

unittest/scripts/py_devapi/validation/dba_cluster_help.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@
388388
allowed: "AUTO", "DISABLED", "REQUIRED".
389389
RuntimeError: if the SSL mode specified is not compatible with the one used
390390
in the cluster.
391+
RuntimeError: if the instance is an active member of the ReplicaSet.
391392

392393
RETURNS
393394

0 commit comments

Comments
 (0)