Skip to content

Commit 5f37cb6

Browse files
authored
fix(NODE-4475): make interrupted message more specific (#3437)
1 parent 26bce4a commit 5f37cb6

File tree

2 files changed

+92
-36
lines changed

2 files changed

+92
-36
lines changed

src/cursor/abstract_cursor.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -897,14 +897,22 @@ class ReadableCursorStream extends Readable {
897897
}
898898

899899
// NOTE: This is also perhaps questionable. The rationale here is that these errors tend
900-
// to be "operation interrupted", where a cursor has been closed but there is an
900+
// to be "operation was interrupted", where a cursor has been closed but there is an
901901
// active getMore in-flight. This used to check if the cursor was killed but once
902902
// that changed to happen in cleanup legitimate errors would not destroy the
903903
// stream. There are change streams test specifically test these cases.
904-
if (err.message.match(/interrupted/)) {
904+
if (err.message.match(/operation was interrupted/)) {
905905
return this.push(null);
906906
}
907907

908+
// NOTE: The two above checks on the message of the error will cause a null to be pushed
909+
// to the stream, thus closing the stream before the destroy call happens. This means
910+
// that either of those error messages on a change stream will not get a proper
911+
// 'error' event to be emitted (the error passed to destroy). Change stream resumability
912+
// relies on that error event to be emitted to create its new cursor and thus was not
913+
// working on 4.4 servers because the error emitted on failover was "interrupted at
914+
// shutdown" while on 5.0+ it is "The server is in quiesce mode and will shut down".
915+
// See NODE-4475.
908916
return this.destroy(err);
909917
}
910918

test/integration/change-streams/change_stream.test.ts

+82-34
Original file line numberDiff line numberDiff line change
@@ -1674,22 +1674,30 @@ describe('ChangeStream resumability', function () {
16741674
};
16751675

16761676
const resumableErrorCodes = [
1677-
{ error: 'HostUnreachable', code: 6 },
1678-
{ error: 'HostNotFound', code: 7 },
1679-
{ error: 'NetworkTimeout', code: 89 },
1680-
{ error: 'ShutdownInProgress', code: 91 },
1681-
{ error: 'PrimarySteppedDown', code: 189 },
1682-
{ error: 'ExceededTimeLimit', code: 262 },
1683-
{ error: 'SocketException', code: 9001 },
1684-
{ error: 'NotWritablePrimary', code: 10107 },
1685-
{ error: 'InterruptedAtShutdown', code: 11600 },
1686-
{ error: 'InterruptedDueToReplStateChange', code: 11602 },
1687-
{ error: 'NotPrimaryNoSecondaryOk', code: 13435 },
1688-
{ error: 'StaleShardVersion', code: 63 },
1689-
{ error: 'StaleEpoch', code: 150 },
1690-
{ error: 'RetryChangeStream', code: 234 },
1691-
{ error: 'FailedToSatisfyReadPreference', code: 133 },
1692-
{ error: 'CursorNotFound', code: 43 }
1677+
{ error: 'HostUnreachable', code: 6, message: 'host unreachable' },
1678+
{ error: 'HostNotFound', code: 7, message: 'hot not found' },
1679+
{ error: 'NetworkTimeout', code: 89, message: 'network timeout' },
1680+
{ error: 'ShutdownInProgress', code: 91, message: 'shutdown in progress' },
1681+
{ error: 'PrimarySteppedDown', code: 189, message: 'primary stepped down' },
1682+
{ error: 'ExceededTimeLimit', code: 262, message: 'operation exceeded time limit' },
1683+
{ error: 'SocketException', code: 9001, message: 'socket exception' },
1684+
{ error: 'NotWritablePrimary', code: 10107, message: 'not writable primary' },
1685+
{ error: 'InterruptedAtShutdown', code: 11600, message: 'interrupted at shutdown' },
1686+
{
1687+
error: 'InterruptedDueToReplStateChange',
1688+
code: 11602,
1689+
message: 'interrupted due to state change'
1690+
},
1691+
{ error: 'NotPrimaryNoSecondaryOk', code: 13435, message: 'not primary and no secondary ok' },
1692+
{ error: 'StaleShardVersion', code: 63, message: 'stale shard version' },
1693+
{ error: 'StaleEpoch', code: 150, message: 'stale epoch' },
1694+
{ error: 'RetryChangeStream', code: 234, message: 'retry change stream' },
1695+
{
1696+
error: 'FailedToSatisfyReadPreference',
1697+
code: 133,
1698+
message: 'failed to satisfy read preference'
1699+
},
1700+
{ error: 'CursorNotFound', code: 43, message: 'cursor not found' }
16931701
];
16941702

16951703
const is4_2Server = (serverVersion: string) =>
@@ -1731,7 +1739,7 @@ describe('ChangeStream resumability', function () {
17311739

17321740
context('iterator api', function () {
17331741
context('#next', function () {
1734-
for (const { error, code } of resumableErrorCodes) {
1742+
for (const { error, code, message } of resumableErrorCodes) {
17351743
it(
17361744
`resumes on error code ${code} (${error})`,
17371745
{ requires: { topology: '!single', mongodb: '>=4.2' } },
@@ -1746,7 +1754,8 @@ describe('ChangeStream resumability', function () {
17461754
mode: { times: 1 },
17471755
data: {
17481756
failCommands: ['getMore'],
1749-
errorCode: code
1757+
errorCode: code,
1758+
errmsg: message
17501759
}
17511760
} as FailPoint);
17521761

@@ -1759,7 +1768,7 @@ describe('ChangeStream resumability', function () {
17591768
}
17601769
);
17611770
}
1762-
for (const { error, code } of resumableErrorCodes) {
1771+
for (const { error, code, message } of resumableErrorCodes) {
17631772
it(
17641773
`resumes on error code ${code} (${error})`,
17651774
{ requires: { topology: '!single', mongodb: '<4.2' } },
@@ -1778,7 +1787,7 @@ describe('ChangeStream resumability', function () {
17781787
.stub(changeStream.cursor, '_getMore')
17791788
.callsFake((_batchSize, callback) => {
17801789
mock.restore();
1781-
const error = new MongoServerError({ message: 'Something went wrong' });
1790+
const error = new MongoServerError({ message: message });
17821791
error.code = code;
17831792
callback(error);
17841793
});
@@ -1807,7 +1816,8 @@ describe('ChangeStream resumability', function () {
18071816
mode: { times: 1 },
18081817
data: {
18091818
failCommands: ['getMore'],
1810-
errorCode: resumableErrorCodes[0].code
1819+
errorCode: resumableErrorCodes[0].code,
1820+
errmsg: resumableErrorCodes[0].message
18111821
}
18121822
} as FailPoint);
18131823

@@ -1858,7 +1868,7 @@ describe('ChangeStream resumability', function () {
18581868
});
18591869

18601870
context('#hasNext', function () {
1861-
for (const { error, code } of resumableErrorCodes) {
1871+
for (const { error, code, message } of resumableErrorCodes) {
18621872
it(
18631873
`resumes on error code ${code} (${error})`,
18641874
{ requires: { topology: '!single', mongodb: '>=4.2' } },
@@ -1873,7 +1883,8 @@ describe('ChangeStream resumability', function () {
18731883
mode: { times: 1 },
18741884
data: {
18751885
failCommands: ['getMore'],
1876-
errorCode: code
1886+
errorCode: code,
1887+
errmsg: message
18771888
}
18781889
} as FailPoint);
18791890

@@ -1887,7 +1898,7 @@ describe('ChangeStream resumability', function () {
18871898
);
18881899
}
18891900

1890-
for (const { error, code } of resumableErrorCodes) {
1901+
for (const { error, code, message } of resumableErrorCodes) {
18911902
it(
18921903
`resumes on error code ${code} (${error})`,
18931904
{ requires: { topology: '!single', mongodb: '<4.2' } },
@@ -1906,7 +1917,7 @@ describe('ChangeStream resumability', function () {
19061917
.stub(changeStream.cursor, '_getMore')
19071918
.callsFake((_batchSize, callback) => {
19081919
mock.restore();
1909-
const error = new MongoServerError({ message: 'Something went wrong' });
1920+
const error = new MongoServerError({ message: message });
19101921
error.code = code;
19111922
callback(error);
19121923
});
@@ -1935,7 +1946,8 @@ describe('ChangeStream resumability', function () {
19351946
mode: { times: 1 },
19361947
data: {
19371948
failCommands: ['getMore'],
1938-
errorCode: resumableErrorCodes[0].code
1949+
errorCode: resumableErrorCodes[0].code,
1950+
errmsg: resumableErrorCodes[0].message
19391951
}
19401952
} as FailPoint);
19411953

@@ -1986,7 +1998,7 @@ describe('ChangeStream resumability', function () {
19861998
});
19871999

19882000
context('#tryNext', function () {
1989-
for (const { error, code } of resumableErrorCodes) {
2001+
for (const { error, code, message } of resumableErrorCodes) {
19902002
it(
19912003
`resumes on error code ${code} (${error})`,
19922004
{ requires: { topology: '!single', mongodb: '>=4.2' } },
@@ -2001,7 +2013,8 @@ describe('ChangeStream resumability', function () {
20012013
mode: { times: 1 },
20022014
data: {
20032015
failCommands: ['getMore'],
2004-
errorCode: code
2016+
errorCode: code,
2017+
errmsg: message
20052018
}
20062019
} as FailPoint);
20072020

@@ -2022,7 +2035,7 @@ describe('ChangeStream resumability', function () {
20222035
);
20232036
}
20242037

2025-
for (const { error, code } of resumableErrorCodes) {
2038+
for (const { error, code, message } of resumableErrorCodes) {
20262039
it(
20272040
`resumes on error code ${code} (${error})`,
20282041
{ requires: { topology: '!single', mongodb: '<4.2' } },
@@ -2041,7 +2054,7 @@ describe('ChangeStream resumability', function () {
20412054
.stub(changeStream.cursor, '_getMore')
20422055
.callsFake((_batchSize, callback) => {
20432056
mock.restore();
2044-
const error = new MongoServerError({ message: 'Something went wrong' });
2057+
const error = new MongoServerError({ message: message });
20452058
error.code = code;
20462059
callback(error);
20472060
});
@@ -2077,7 +2090,8 @@ describe('ChangeStream resumability', function () {
20772090
mode: { times: 1 },
20782091
data: {
20792092
failCommands: ['getMore'],
2080-
errorCode: resumableErrorCodes[0].code
2093+
errorCode: resumableErrorCodes[0].code,
2094+
errmsg: resumableErrorCodes[0].message
20812095
}
20822096
} as FailPoint);
20832097

@@ -2127,7 +2141,7 @@ describe('ChangeStream resumability', function () {
21272141
});
21282142

21292143
describe('event emitter based iteration', function () {
2130-
for (const { error, code } of resumableErrorCodes) {
2144+
for (const { error, code, message } of resumableErrorCodes) {
21312145
it(
21322146
`resumes on error code ${code} (${error})`,
21332147
{ requires: { topology: '!single', mongodb: '>=4.2' } },
@@ -2141,7 +2155,8 @@ describe('ChangeStream resumability', function () {
21412155
mode: { times: 1 },
21422156
data: {
21432157
failCommands: ['getMore'],
2144-
errorCode: code
2158+
errorCode: code,
2159+
errmsg: message
21452160
}
21462161
} as FailPoint);
21472162

@@ -2171,7 +2186,8 @@ describe('ChangeStream resumability', function () {
21712186
mode: { times: 1 },
21722187
data: {
21732188
failCommands: ['getMore'],
2174-
errorCode: resumableErrorCodes[0].code
2189+
errorCode: resumableErrorCodes[0].code,
2190+
errmsg: resumableErrorCodes[0].message
21752191
}
21762192
} as FailPoint);
21772193

@@ -2222,6 +2238,38 @@ describe('ChangeStream resumability', function () {
22222238
}
22232239
);
22242240
});
2241+
2242+
context('when the error is operation was interrupted', function () {
2243+
it(
2244+
'does not resume',
2245+
{ requires: { topology: '!single', mongodb: '>=4.2' } },
2246+
async function () {
2247+
changeStream = collection.watch([]);
2248+
2249+
const unresumableErrorCode = 237;
2250+
await client.db('admin').command({
2251+
configureFailPoint: is4_2Server(this.configuration.version)
2252+
? 'failCommand'
2253+
: 'failGetMoreAfterCursorCheckout',
2254+
mode: { times: 1 },
2255+
data: {
2256+
failCommands: ['getMore'],
2257+
errorCode: unresumableErrorCode,
2258+
errmsg: 'operation was interrupted'
2259+
}
2260+
} as FailPoint);
2261+
2262+
const willBeError = once(changeStream, 'change').catch(error => error);
2263+
await once(changeStream.cursor, 'init');
2264+
await collection.insertOne({ name: 'bailey' });
2265+
2266+
const error = await willBeError;
2267+
2268+
expect(error).to.be.instanceOf(MongoServerError);
2269+
expect(aggregateEvents).to.have.lengthOf(1);
2270+
}
2271+
);
2272+
});
22252273
});
22262274

22272275
it(

0 commit comments

Comments
 (0)