Skip to content

Commit 6528c8d

Browse files
authoredFeb 7, 2025··
fix(NODE-6613): Update error messages when primaries go stale (#4397)
1 parent 057693e commit 6528c8d

19 files changed

+171
-31
lines changed
 

‎src/error.ts

+4-12
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import type { Document, ObjectId } from './bson';
1+
import type { Document } from './bson';
22
import {
33
type ClientBulkWriteError,
44
type ClientBulkWriteResult
55
} from './operations/client_bulk_write/common';
66
import type { ServerType } from './sdam/common';
7-
import type { ServerDescription, TopologyVersion } from './sdam/server_description';
7+
import type { TopologyVersion } from './sdam/server_description';
88
import type { TopologyDescription } from './sdam/topology_description';
99

1010
/** @public */
@@ -355,16 +355,8 @@ export class MongoStalePrimaryError extends MongoRuntimeError {
355355
*
356356
* @public
357357
**/
358-
constructor(
359-
serverDescription: ServerDescription,
360-
maxSetVersion: number | null,
361-
maxElectionId: ObjectId | null,
362-
options?: { cause?: Error }
363-
) {
364-
super(
365-
`primary marked stale due to electionId/setVersion mismatch: server setVersion: ${serverDescription.setVersion}, server electionId: ${serverDescription.electionId}, topology setVersion: ${maxSetVersion}, topology electionId: ${maxElectionId}`,
366-
options
367-
);
358+
constructor(message: string, options?: { cause?: Error }) {
359+
super(message, options);
368360
}
369361

370362
override get name(): string {

‎src/sdam/topology_description.ts

+22-3
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,19 @@ function updateRsFromPrimary(
376376
maxSetVersion: number | null = null,
377377
maxElectionId: ObjectId | null = null
378378
): [TopologyType, string | null, number | null, ObjectId | null] {
379+
const setVersionElectionIdMismatch = (
380+
serverDescription: ServerDescription,
381+
maxSetVersion: number | null,
382+
maxElectionId: ObjectId | null
383+
) => {
384+
return (
385+
`primary marked stale due to electionId/setVersion mismatch:` +
386+
` server setVersion: ${serverDescription.setVersion},` +
387+
` server electionId: ${serverDescription.electionId},` +
388+
` topology setVersion: ${maxSetVersion},` +
389+
` topology electionId: ${maxElectionId}`
390+
);
391+
};
379392
setName = setName || serverDescription.setName;
380393
if (setName !== serverDescription.setName) {
381394
serverDescriptions.delete(serverDescription.address);
@@ -401,7 +414,9 @@ function updateRsFromPrimary(
401414
serverDescriptions.set(
402415
serverDescription.address,
403416
new ServerDescription(serverDescription.address, undefined, {
404-
error: new MongoStalePrimaryError(serverDescription, maxSetVersion, maxElectionId)
417+
error: new MongoStalePrimaryError(
418+
setVersionElectionIdMismatch(serverDescription, maxSetVersion, maxElectionId)
419+
)
405420
})
406421
);
407422

@@ -419,7 +434,9 @@ function updateRsFromPrimary(
419434
serverDescriptions.set(
420435
serverDescription.address,
421436
new ServerDescription(serverDescription.address, undefined, {
422-
error: new MongoStalePrimaryError(serverDescription, maxSetVersion, maxElectionId)
437+
error: new MongoStalePrimaryError(
438+
setVersionElectionIdMismatch(serverDescription, maxSetVersion, maxElectionId)
439+
)
423440
})
424441
);
425442

@@ -445,7 +462,9 @@ function updateRsFromPrimary(
445462
serverDescriptions.set(
446463
address,
447464
new ServerDescription(server.address, undefined, {
448-
error: new MongoStalePrimaryError(serverDescription, maxSetVersion, maxElectionId)
465+
error: new MongoStalePrimaryError(
466+
'primary marked stale due to discovery of newer primary'
467+
)
449468
})
450469
);
451470

‎test/spec/server-discovery-and-monitoring/rs/new_primary.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@
5858
"servers": {
5959
"a:27017": {
6060
"type": "Unknown",
61-
"setName": null
61+
"setName": null,
62+
"error": "primary marked stale due to discovery of newer primary"
6263
},
6364
"b:27017": {
6465
"type": "RSPrimary",

‎test/spec/server-discovery-and-monitoring/rs/new_primary.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ phases: [
6363
"a:27017": {
6464

6565
type: "Unknown",
66-
setName:
66+
setName:,
67+
error: "primary marked stale due to discovery of newer primary"
6768
},
6869

6970
"b:27017": {

‎test/spec/server-discovery-and-monitoring/rs/new_primary_new_electionid.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
"type": "Unknown",
7878
"setName": null,
7979
"electionId": null,
80-
"error": "primary marked stale due to electionId/setVersion mismatch"
80+
"error": "primary marked stale due to discovery of newer primary"
8181
},
8282
"b:27017": {
8383
"type": "RSPrimary",

‎test/spec/server-discovery-and-monitoring/rs/new_primary_new_electionid.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ phases: [
6464
type: "Unknown",
6565
setName: ,
6666
electionId: ,
67-
error: "primary marked stale due to electionId/setVersion mismatch"
67+
error: "primary marked stale due to discovery of newer primary"
6868
},
6969
"b:27017": {
7070
type: "RSPrimary",

‎test/spec/server-discovery-and-monitoring/rs/new_primary_new_setversion.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
"type": "Unknown",
7878
"setName": null,
7979
"electionId": null,
80-
"error": "primary marked stale due to electionId/setVersion mismatch"
80+
"error": "primary marked stale due to discovery of newer primary"
8181
},
8282
"b:27017": {
8383
"type": "RSPrimary",

‎test/spec/server-discovery-and-monitoring/rs/new_primary_new_setversion.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ phases: [
6464
type: "Unknown",
6565
setName: ,
6666
electionId:,
67-
error: "primary marked stale due to electionId/setVersion mismatch"
67+
error: "primary marked stale due to discovery of newer primary"
6868
},
6969
"b:27017": {
7070
type: "RSPrimary",

‎test/spec/server-discovery-and-monitoring/rs/primary_disconnect_electionid.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"type": "Unknown",
5050
"setName": null,
5151
"electionId": null,
52-
"error": "primary marked stale due to electionId/setVersion mismatch"
52+
"error": "primary marked stale due to discovery of newer primary"
5353
},
5454
"b:27017": {
5555
"type": "RSPrimary",
@@ -125,6 +125,7 @@
125125
"a:27017": {
126126
"type": "Unknown",
127127
"setName": null,
128+
"error": "primary marked stale due to electionId/setVersion mismatch",
128129
"electionId": null
129130
},
130131
"b:27017": {

‎test/spec/server-discovery-and-monitoring/rs/primary_disconnect_electionid.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ phases: [
3737
type: "Unknown",
3838
setName: ,
3939
electionId:,
40-
error: "primary marked stale due to electionId/setVersion mismatch"
40+
error: "primary marked stale due to discovery of newer primary"
4141
},
4242
"b:27017": {
4343
type: "RSPrimary",
@@ -100,6 +100,7 @@ phases: [
100100
"a:27017": {
101101
type: "Unknown",
102102
setName: ,
103+
error: "primary marked stale due to electionId/setVersion mismatch",
103104
electionId:
104105
},
105106
"b:27017": {

‎test/spec/server-discovery-and-monitoring/rs/primary_disconnect_setversion.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"type": "Unknown",
5050
"setName": null,
5151
"electionId": null,
52-
"error": "primary marked stale due to electionId/setVersion mismatch"
52+
"error": "primary marked stale due to discovery of newer primary"
5353
},
5454
"b:27017": {
5555
"type": "RSPrimary",
@@ -125,6 +125,7 @@
125125
"a:27017": {
126126
"type": "Unknown",
127127
"setName": null,
128+
"error": "primary marked stale due to electionId/setVersion mismatch",
128129
"electionId": null
129130
},
130131
"b:27017": {

‎test/spec/server-discovery-and-monitoring/rs/primary_disconnect_setversion.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ phases: [
3737
type: "Unknown",
3838
setName: ,
3939
electionId:,
40-
error: "primary marked stale due to electionId/setVersion mismatch"
40+
error: "primary marked stale due to discovery of newer primary"
4141
},
4242
"b:27017": {
4343
type: "RSPrimary",
@@ -100,6 +100,7 @@ phases: [
100100
"a:27017": {
101101
type: "Unknown",
102102
setName: ,
103+
error: "primary marked stale due to electionId/setVersion mismatch",
103104
electionId:
104105
},
105106
"b:27017": {

‎test/spec/server-discovery-and-monitoring/rs/setversion_greaterthan_max_without_electionid.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
"type": "Unknown",
6767
"setName": null,
6868
"electionId": null,
69-
"error": "primary marked stale due to electionId/setVersion mismatch"
69+
"error": "primary marked stale due to discovery of newer primary"
7070
},
7171
"b:27017": {
7272
"type": "RSPrimary",

‎test/spec/server-discovery-and-monitoring/rs/setversion_greaterthan_max_without_electionid.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ phases: [
6262
type: "Unknown",
6363
setName: ,
6464
electionId:,
65-
error: "primary marked stale due to electionId/setVersion mismatch"
65+
error: "primary marked stale due to discovery of newer primary"
6666
},
6767
"b:27017": {
6868
type: "RSPrimary",

‎test/spec/server-discovery-and-monitoring/rs/setversion_without_electionid-pre-6.0.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
"type": "Unknown",
6767
"setName": null,
6868
"electionId": null,
69-
"error": "primary marked stale due to electionId/setVersion mismatch"
69+
"error": "primary marked stale due to discovery of newer primary"
7070
},
7171
"b:27017": {
7272
"type": "RSPrimary",

‎test/spec/server-discovery-and-monitoring/rs/setversion_without_electionid-pre-6.0.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ phases: [
6262
type: "Unknown",
6363
setName: ,
6464
electionId:,
65-
error: "primary marked stale due to electionId/setVersion mismatch"
65+
error: "primary marked stale due to discovery of newer primary"
6666
},
6767
"b:27017": {
6868
type: "RSPrimary",

‎test/spec/server-discovery-and-monitoring/rs/use_setversion_without_electionid-pre-6.0.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
"type": "Unknown",
7575
"setName": null,
7676
"electionId": null,
77-
"error": "primary marked stale due to electionId/setVersion mismatch"
77+
"error": "primary marked stale due to discovery of newer primary"
7878
},
7979
"b:27017": {
8080
"type": "RSPrimary",

‎test/spec/server-discovery-and-monitoring/rs/use_setversion_without_electionid-pre-6.0.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ phases: [
6363
type: "Unknown",
6464
setName: ,
6565
electionId:,
66-
error: "primary marked stale due to electionId/setVersion mismatch"
66+
error: "primary marked stale due to discovery of newer primary"
6767
},
6868
"b:27017": {
6969
type: "RSPrimary",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { expect } from 'chai';
2+
import { type TopologyDescription } from 'mongodb-legacy';
3+
import * as sinon from 'sinon';
4+
5+
import {
6+
MongoClient,
7+
ObjectId,
8+
Server,
9+
ServerDescription,
10+
Topology,
11+
type TopologyDescriptionChangedEvent
12+
} from '../../mongodb';
13+
14+
describe('Server Discovery and Monitoring', function () {
15+
let serverConnect: sinon.SinonStub;
16+
let topologySelectServer: sinon.SinonStub;
17+
let client: MongoClient;
18+
let events: TopologyDescriptionChangedEvent[];
19+
20+
function getNewDescription() {
21+
const topologyDescriptionChanged = events[events.length - 1];
22+
return topologyDescriptionChanged.newDescription;
23+
}
24+
25+
beforeEach(async function () {
26+
serverConnect = sinon.stub(Server.prototype, 'connect').callsFake(function () {
27+
this.s.state = 'connected';
28+
this.emit('connect');
29+
});
30+
31+
topologySelectServer = sinon
32+
.stub(Topology.prototype, 'selectServer')
33+
.callsFake(async function (_selector, _options) {
34+
topologySelectServer.restore();
35+
36+
const fakeServer = { s: { state: 'connected' }, removeListener: () => true };
37+
return fakeServer;
38+
});
39+
40+
events = [];
41+
client = new MongoClient('mongodb://a/?replicaSet=rs');
42+
client.on('topologyDescriptionChanged', event => events.push(event));
43+
await client.connect();
44+
45+
// Start with a as primary
46+
client.topology.serverUpdateHandler(
47+
new ServerDescription('a:27017', {
48+
ok: 1,
49+
helloOk: true,
50+
isWritablePrimary: true,
51+
hosts: ['a:27017', 'b:27017'],
52+
setName: 'rs',
53+
setVersion: 1,
54+
electionId: ObjectId.createFromHexString('000000000000000000000001'),
55+
minWireVersion: 0,
56+
maxWireVersion: 21
57+
})
58+
);
59+
60+
// b is elected as primary, a gets marked stale
61+
client.topology.serverUpdateHandler(
62+
new ServerDescription('b:27017', {
63+
ok: 1,
64+
helloOk: true,
65+
isWritablePrimary: true,
66+
hosts: ['a:27017', 'b:27017'],
67+
setName: 'rs',
68+
setVersion: 2,
69+
electionId: ObjectId.createFromHexString('000000000000000000000001'),
70+
minWireVersion: 0,
71+
maxWireVersion: 21
72+
})
73+
);
74+
});
75+
76+
afterEach(async function () {
77+
serverConnect.restore();
78+
await client.close().catch(() => null);
79+
});
80+
81+
let newDescription: TopologyDescription;
82+
83+
describe('when a newer primary is detected', function () {
84+
it('steps down original primary to unknown server description with appropriate error message', function () {
85+
newDescription = getNewDescription();
86+
87+
const aOutcome = newDescription.servers.get('a:27017');
88+
const bOutcome = newDescription.servers.get('b:27017');
89+
expect(aOutcome.type).to.equal('Unknown');
90+
expect(aOutcome.error).to.match(/primary marked stale due to discovery of newer primary/);
91+
92+
expect(bOutcome.type).to.equal('RSPrimary');
93+
});
94+
});
95+
96+
describe('when a stale primary still reports itself as primary', function () {
97+
it('gets marked as unknown with an error message with the new and old replicaSetVersion and electionId', function () {
98+
// a still incorrectly reports as primary
99+
client.topology.serverUpdateHandler(
100+
new ServerDescription('a:27017', {
101+
ok: 1,
102+
helloOk: true,
103+
isWritablePrimary: true,
104+
hosts: ['a:27017', 'b:27017'],
105+
setName: 'rs',
106+
setVersion: 1,
107+
electionId: ObjectId.createFromHexString('000000000000000000000001'),
108+
minWireVersion: 0,
109+
maxWireVersion: 21
110+
})
111+
);
112+
113+
newDescription = getNewDescription();
114+
115+
const aOutcome = newDescription.servers.get('a:27017');
116+
117+
expect(aOutcome.type).to.equal('Unknown');
118+
expect(aOutcome.error).to.match(
119+
/primary marked stale due to electionId\/setVersion mismatch: server setVersion: \d+, server electionId: \d{24}, topology setVersion: \d+, topology electionId: \d{24}/
120+
);
121+
});
122+
});
123+
});

0 commit comments

Comments
 (0)
Please sign in to comment.