SERVER-119413: Change configTerm from int to long long in idl and cod… (#48912)

GitOrigin-RevId: e23c0c72eef88abeebf1567048e8d01a3b39f8ec
This commit is contained in:
seanzimm 2026-03-09 11:47:33 -04:00 committed by MongoDB Bot
parent 2b1ff713b8
commit ad71b7376c
7 changed files with 122 additions and 18 deletions

View File

@ -524,6 +524,8 @@ last-continuous:
ticket: SERVER-117009
- test_file: jstests/noPassthrough/replication/killOp_against_repl_threads.js
ticket: SERVER-112273
- test_file: jstests/replsets/election_term_over_int_max.js
ticket: SERVER-119413
suites: null
last-lts:
all:
@ -1105,4 +1107,6 @@ last-lts:
ticket: SERVER-117009
- test_file: jstests/noPassthrough/replication/killOp_against_repl_threads.js
ticket: SERVER-112273
- test_file: jstests/replsets/election_term_over_int_max.js
ticket: SERVER-119413
suites: null

View File

@ -0,0 +1,85 @@
/**
* Basic smoke test that a replica set can be stopped, one member restarted as standalone,
* then the full replica set brought back up. While standalone, we manually corrupt the
* election term document.
*
* @tags: [requires_persistence, requires_replication]
*/
import {ReplSetTest} from "jstests/libs/replsettest.js";
const rst = new ReplSetTest({name: jsTestName(), nodes: 3});
rst.startSet();
rst.initiate();
let primary = rst.getPrimary();
const dbName = jsTestName();
const collName = "coll";
// Do a majority write so we have something in the oplog.
assert.commandWorked(
primary.getDB(dbName)[collName].insert({x: 1}, {writeConcern: {w: "majority"}}));
jsTest.log.info("Stopping the replica set with forRestart=true");
rst.stopSet(/* signal */ null, /* forRestart */ true);
jsTest.log.info("Starting node 0 as a standalone");
const node0 = rst.nodes[0];
let standalone = MongoRunner.runMongod({
dbpath: node0.dbpath,
noReplSet: true,
noCleanData: true,
});
assert.neq(null, standalone, "failed to start standalone mongod");
// Sanity check: data is visible in standalone.
let standaloneDB = standalone.getDB(dbName);
assert.eq(1,
standaloneDB[collName].find().itcount(),
"expected to see document written before stopping replset");
// Set the election term to > int max
jsTest.log.info("Updating local.replset.election.term while standalone");
const electionColl = standalone.getDB("local").getCollection("replset.election");
const res = electionColl.update(
{},
{$set: {term: NumberLong("18014398509482012")}},
/* upsert */ false,
/* multi */ true,
);
assert.commandWorked(res);
jsTest.log.info("Stopping standalone");
MongoRunner.stopMongod(standalone);
jsTest.log.info("Restarting full replica set");
rst.startSet({restart: true, noCleanData: true});
// Expecting node0 to become primary.
primary = rst.getPrimary();
// The term we set to + 1 should be the new term when the replset restarts.
assert.eq(NumberLong("18014398509482013"),
primary.getDB("local").getCollection("replset.election").findOne({}).term);
const secondaries = rst.getSecondaries();
// Do a w:3 write so we know all nodes are on the new term.
assert.commandWorked(primary.getDB(dbName)[collName].insert({y: 1}, {writeConcern: {w: 3}}));
// Force an election: step up one of the secondaries and verify it wins.
jsTest.log.info("Forcing an election via replSetStepUp");
const oldPrimary = primary;
const candidate = secondaries[0];
assert.commandWorked(candidate.adminCommand({replSetStepUp: 1}));
// Wait for the cluster to agree on the new primary.
rst.awaitNodesAgreeOnPrimary();
const newPrimary = rst.getPrimary();
jsTest.log.info("New primary after forced election: " + tojson(newPrimary.host));
assert.neq(newPrimary.host, oldPrimary.host, "expected a different primary after stepUp");
assert.eq(candidate, newPrimary, "different primary than expected after forced stepup");
rst.stopSet();

View File

@ -170,7 +170,7 @@ structs:
created this configuration. Configurations in a replica set are
totally ordered by their term and configuration version."
default: -1
validator: {gte: -1, lt: 2147483648}
validator: {gte: -1, lte: 9223372036854775807}
members:
type: array<memberConfig>
description:

View File

@ -339,14 +339,26 @@ TEST(ReplSetConfig, ParseFailsWithBadOrMissingTermField) {
<< BSON_ARRAY(BSON("_id" << 0 << "host"
<< "localhost:12345")))),
DBException);
ASSERT_THROWS(ReplSetConfig::parse(
BSON("_id" << "rs0"
<< "version" << 1 << "term"
<< static_cast<long long>(std::numeric_limits<int>::max()) + 1
<< "protocolVersion" << 1 << "members"
<< BSON_ARRAY(BSON("_id" << 0 << "host"
<< "localhost:12345")))),
DBException);
config = ReplSetConfig::parse(
BSON("_id" << "rs0"
<< "version" << 1 << "term"
<< static_cast<long long>(std::numeric_limits<int>::max()) + 1
<< "protocolVersion" << 1 << "members"
<< BSON_ARRAY(BSON("_id" << 0 << "host"
<< "localhost:12345"))));
ASSERT_OK(config.validate());
// Verify we can parse up to long long
config = ReplSetConfig::parse(
BSON("_id" << "rs0"
<< "version" << 1 << "term"
<< static_cast<long long>(std::numeric_limits<long long>::max())
<< "protocolVersion" << 1 << "members"
<< BSON_ARRAY(BSON("_id" << 0 << "host"
<< "localhost:12345"))));
ASSERT_OK(config.validate());
}
TEST(ReplSetConfig, ParseFailsWithBadMembers) {

View File

@ -252,19 +252,22 @@ Status ReplSetHeartbeatResponse::initialize(const BSONObj& doc, long long term)
str::stream() << "Response to replSetHeartbeat missing required \""
<< kConfigVersionFieldName << "\" field");
}
if (configVersionElement.type() != BSONType::numberInt) {
if (configVersionElement.type() != BSONType::numberInt &&
configVersionElement.type() != BSONType::numberLong) {
return Status(ErrorCodes::TypeMismatch,
str::stream() << "Expected \"" << kConfigVersionFieldName
<< "\" field in response to replSetHeartbeat to have "
"type NumberInt, but found "
"type NumberInt/NumberLong, but found "
<< typeName(configVersionElement.type()));
}
_configVersion = configVersionElement.numberInt();
_configVersion = configVersionElement.numberLong();
// Allow a missing term field for backward compatibility.
const BSONElement configTermElement = doc[kConfigTermFieldName];
if (!configTermElement.eoo() && configVersionElement.type() == BSONType::numberInt) {
_configTerm = configTermElement.numberInt();
if (!configTermElement.eoo() &&
(configTermElement.type() == BSONType::numberInt ||
configTermElement.type() == BSONType::numberLong)) {
_configTerm = configTermElement.numberLong();
}
const BSONElement syncingToElement = doc[kSyncSourceFieldName];

View File

@ -94,7 +94,7 @@ public:
int getConfigVersion() const {
return _configVersion;
}
int getConfigTerm() const {
long long getConfigTerm() const {
return _configTerm;
}
ConfigVersionAndTerm getConfigVersionAndTerm() const {
@ -171,7 +171,7 @@ public:
/**
* Sets _configTerm to "configTerm".
*/
void setConfigTerm(int configTerm) {
void setConfigTerm(long long configTerm) {
_configTerm = configTerm;
}
@ -235,7 +235,7 @@ private:
MemberState _state;
int _configVersion = -1;
int _configTerm = OpTime::kUninitializedTerm;
long long _configTerm = OpTime::kUninitializedTerm;
std::string _setName;
HostAndPort _syncingTo;

View File

@ -322,7 +322,7 @@ TEST(ReplSetHeartbeatResponse, InitializeVersionWrongType) {
ASSERT_EQUALS(ErrorCodes::TypeMismatch, result);
ASSERT_EQUALS(
"Expected \"v\" field in response to replSetHeartbeat to "
"have type NumberInt, but found string",
"have type NumberInt/NumberLong, but found string",
result.reason());
}