Skip to content

Commit 4501a1c

Browse files
authored
fix(NODE-4281): ensure that the driver always uses Node.js timers (#3275)
Ensure that the driver always uses the Node.js timers API, rather than the global one, in case they diverge. This affects Compass, where the `setTimeout(...).unref()` usage currently results in uncaught exceptions because `setTimeout()` returns a number in browsers. This change adds `import ... from 'timers';` where necessary and adds a linter rule to prevent regressions. If this is not an acceptable solution, we can go back to the drawing board, but this seems like a good solution that doesn’t add too much overhead when writing driver code.
1 parent 47adfb3 commit 4501a1c

30 files changed

+79
-1
lines changed

.eslintrc.json

+15
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@
3838
"property": "only"
3939
}
4040
],
41+
"no-restricted-globals": [
42+
"error",
43+
{
44+
"name": "setTimeout",
45+
"message": "Use `import { setTimeout } from 'timers';` instead"
46+
},
47+
{
48+
"name": "setImmediate",
49+
"message": "Use `import { setImmediate } from 'timers';` instead"
50+
},
51+
{
52+
"name": "setInterval",
53+
"message": "Use `import { setInterval } from 'timers';` instead"
54+
}
55+
],
4156
"prettier/prettier": "error",
4257
"tsdoc/syntax": "warn",
4358
"no-console": "error",

src/change_stream.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Denque = require('denque');
22
import type { Readable } from 'stream';
3+
import { setTimeout } from 'timers';
34

45
import type { Document, Long, Timestamp } from './bson';
56
import { Collection } from './collection';

src/cmap/connection.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { setTimeout } from 'timers';
2+
13
import { BSONSerializeOptions, Document, Long, ObjectId, pluckBSONSerializeOptions } from '../bson';
24
import {
35
CLOSE,

src/cmap/connection_pool.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import Denque = require('denque');
2+
import { setTimeout } from 'timers';
3+
24
import type { ObjectId } from '../bson';
35
import {
46
APM_EVENTS,

src/sdam/monitor.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { setTimeout } from 'timers';
2+
13
import { Document, Long } from '../bson';
24
import { connect } from '../cmap/connect';
35
import { Connection, ConnectionOptions } from '../cmap/connection';

src/sdam/srv_polling.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as dns from 'dns';
2+
import { setTimeout } from 'timers';
23

34
import { MongoRuntimeError } from '../error';
45
import { Logger, LoggerOptions } from '../logger';

src/sdam/topology.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import Denque = require('denque');
2+
import { setTimeout } from 'timers';
3+
24
import type { BSONSerializeOptions, Document } from '../bson';
35
import { deserialize, serialize } from '../bson';
46
import type { MongoCredentials } from '../cmap/auth/mongo_credentials';

src/utils.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as crypto from 'crypto';
22
import type { SrvRecord } from 'dns';
33
import * as os from 'os';
4+
import { setTimeout } from 'timers';
45
import { URL } from 'url';
56

67
import { Document, ObjectId, resolveBSONOptions } from './bson';

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

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { strict as assert } from 'assert';
22
import { expect } from 'chai';
33
import { on, once } from 'events';
44
import { PassThrough } from 'stream';
5+
import { setTimeout } from 'timers';
56
import { promisify } from 'util';
67

78
import {

test/integration/change-streams/change_streams.prose.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect } from 'chai';
22
import * as sinon from 'sinon';
3+
import { setTimeout } from 'timers';
34

45
import {
56
ChangeStream,

test/integration/crud/find.test.js

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
const { assert: test, setupDatabase, withMonitoredClient } = require('../shared');
33
const { expect } = require('chai');
44
const sinon = require('sinon');
5+
const { setTimeout } = require('timers');
56
const { Code, ObjectId, Long, Binary, ReturnDocument } = require('../../../src');
67

78
describe('Find', function () {

test/integration/crud/misc_cursors.test.js

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const { expect } = require('chai');
1414
const BSON = require('bson');
1515
const sinon = require('sinon');
1616
const { Writable } = require('stream');
17+
const { setTimeout } = require('timers');
1718
const { ReadPreference } = require('../../../src/read_preference');
1819
const { ServerType } = require('../../../src/sdam/common');
1920
const { formatSort } = require('../../../src/sort');

test/integration/node-specific/cursor_stream.test.js

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
var expect = require('chai').expect;
33
const { setupDatabase } = require('../shared');
44
const { Binary } = require('../../../src');
5+
const { setTimeout, setImmediate } = require('timers');
56

67
describe('Cursor Streams', function () {
78
before(function () {

test/integration/node-specific/examples/change_streams.test.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint no-unused-vars: 0 */
22
'use strict';
33

4+
const { setTimeout } = require('timers');
45
const setupDatabase = require('../../shared').setupDatabase;
56
const expect = require('chai').expect;
67

test/integration/node-specific/operation_example.test.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict';
22
const { assert: test, setupDatabase } = require('../shared');
3+
const { setTimeout } = require('timers');
34
const { format: f } = require('util');
45
const { Topology } = require('../../../src/sdam/topology');
56
const { Code, ObjectId, ReturnDocument } = require('../../../src');

test/integration/objectid.test.js

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ var test = require('./shared').assert;
33
const { expect } = require('chai');
44
var setupDatabase = require('./shared').setupDatabase;
55
const { ObjectId } = require('../../src');
6+
const { setTimeout, setInterval } = require('timers');
67

78
describe('ObjectId', function () {
89
before(function () {

test/integration/server-selection/server_selection.prose.operation_count.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect } from 'chai';
2+
import { setTimeout } from 'timers';
23
import { promisify } from 'util';
34

45
import { CommandStartedEvent } from '../../../src';

test/integration/shared.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const expect = require('chai').expect;
4+
const { setTimeout } = require('timers');
45

56
// helpers for using chai.expect in the assert style
67
const assert = {

test/tools/mongodb-mock/src/server.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const compressorIDs = require('./utils').compressorIDs;
88
const Request = require('./request');
99
const { Query } = require('./protocol');
1010
const EventEmitter = require('events');
11+
const { setTimeout } = require('timers');
1112
const { HostAddress } = require('../../../../src/utils');
1213

1314
/*

test/tools/spec-runner/context.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict';
22
const { expect } = require('chai');
3+
const { setTimeout } = require('timers');
34
const { resolveConnectionString } = require('./utils');
45
const { ns } = require('../../../src/utils');
56
const { extractAuthFromConnectionString } = require('../utils');

test/tools/utils.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { EJSON } from 'bson';
22
import * as BSON from 'bson';
33
import { expect } from 'chai';
4+
import { setTimeout } from 'timers';
45
import { inspect, promisify } from 'util';
56

67
import { OP_MSG } from '../../src/cmap/wire_protocol/constants';

test/unit/assorted/polling_srv_records_for_mongos_discovery.prose.test.ts

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { HostAddress, isHello } from '../../../src/utils';
1010
import * as mock from '../../tools/mongodb-mock/index';
1111
import type { MockServer } from '../../tools/mongodb-mock/src/server';
1212
import { processTick } from '../../tools/utils';
13+
import { createTimerSandbox } from '../timer_sandbox';
1314

1415
/*
1516
The SRV Prose Tests make use of the following REAL DNS records.
@@ -215,17 +216,20 @@ describe('Polling Srv Records for Mongos Discovery', () => {
215216
let lookupStub: sinon.SinonStub;
216217
let client: MongoClient;
217218
let clock: sinon.SinonFakeTimers;
219+
let timerSandbox: sinon.SinonSandbox;
218220
const initialRecords = Object.freeze([
219221
{ name: 'localhost.test.mock.test.build.10gen.cc', port: 2017 },
220222
{ name: 'localhost.test.mock.test.build.10gen.cc', port: 2018 }
221223
]);
222224

223225
beforeEach(() => {
226+
timerSandbox = createTimerSandbox();
224227
clock = sinon.useFakeTimers();
225228
});
226229

227230
afterEach(() => {
228231
if (clock) {
232+
timerSandbox.restore();
229233
clock.restore();
230234
clock = undefined;
231235
}

test/unit/cmap/connect.test.js

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const mock = require('../../tools/mongodb-mock/index');
44
const { expect } = require('chai');
55
const EventEmitter = require('events');
6+
const { setTimeout } = require('timers');
67

78
const { connect } = require('../../../src/cmap/connect');
89
const { MongoCredentials } = require('../../../src/cmap/auth/mongo_credentials');

test/unit/cmap/connection.test.ts

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { expect } from 'chai';
22
import { EventEmitter } from 'events';
33
import { Socket } from 'net';
44
import * as sinon from 'sinon';
5+
import { setTimeout } from 'timers';
56

67
import { connect } from '../../../src/cmap/connect';
78
import { Connection, hasSessionSupport } from '../../../src/cmap/connection';
@@ -10,6 +11,7 @@ import { MongoNetworkTimeoutError } from '../../../src/error';
1011
import { isHello, ns } from '../../../src/utils';
1112
import * as mock from '../../tools/mongodb-mock/index';
1213
import { getSymbolFrom } from '../../tools/utils';
14+
import { createTimerSandbox } from '../timer_sandbox';
1315

1416
const connectionOptionsDefaults = {
1517
id: 0,
@@ -138,6 +140,7 @@ describe('new Connection()', function () {
138140
describe('onTimeout()', () => {
139141
let connection: sinon.SinonSpiedInstance<Connection>;
140142
let clock: sinon.SinonFakeTimers;
143+
let timerSandbox: sinon.SinonFakeTimers;
141144
let driverSocket: sinon.SinonSpiedInstance<FakeSocket>;
142145
let messageStream: MessageStream;
143146
let kDelayedTimeoutId: symbol;
@@ -163,6 +166,7 @@ describe('new Connection()', function () {
163166
}
164167

165168
beforeEach(() => {
169+
timerSandbox = createTimerSandbox();
166170
clock = sinon.useFakeTimers();
167171

168172
NodeJSTimeoutClass = setTimeout(() => null, 1).constructor;
@@ -176,6 +180,7 @@ describe('new Connection()', function () {
176180
});
177181

178182
afterEach(() => {
183+
timerSandbox.restore();
179184
clock.restore();
180185
});
181186

test/unit/cmap/connection_pool.test.js

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const mock = require('../../tools/mongodb-mock/index');
66
const cmapEvents = require('../../../src/cmap/connection_pool_events');
77
const sinon = require('sinon');
88
const { expect } = require('chai');
9+
const { setImmediate } = require('timers');
910
const { ns, isHello } = require('../../../src/utils');
1011
const { LEGACY_HELLO_COMMAND } = require('../../../src/constants');
1112

test/unit/error.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect } from 'chai';
2+
import { setTimeout } from 'timers';
23

34
import {
45
PoolClosedError as MongoPoolClosedError,

test/unit/sdam/monitor.test.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
'use strict';
2+
const { setTimeout } = require('timers');
23
const mock = require('../../tools/mongodb-mock/index');
34
const { ServerType } = require('../../../src/sdam/common');
45
const { Topology } = require('../../../src/sdam/topology');

test/unit/sdam/topology.test.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict';
22

3+
const { setTimeout } = require('timers');
34
const mock = require('../../tools/mongodb-mock/index');
45
const { expect } = require('chai');
56
const sinon = require('sinon');

test/unit/timer_sandbox.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict';
2+
const sinon = require('sinon');
3+
4+
/**
5+
* sinon.useFakeTimers() only affects global methods, this function
6+
* creates a sinon sandbox that ensures that require('timers')
7+
* also uses the mocked variants.
8+
*
9+
* @returns {sinon.SinonSandbox}
10+
*/
11+
exports.createTimerSandbox = () => {
12+
const timerSandbox = sinon.createSandbox();
13+
const timers = require('timers');
14+
for (const method in timers) {
15+
if (method in global) {
16+
timerSandbox.replace(timers, method, (...args) => {
17+
return global[method](...args);
18+
});
19+
}
20+
}
21+
return timerSandbox;
22+
};

test/unit/utils.test.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const { expect } = require('chai');
1010
const sinon = require('sinon');
1111
const { MongoRuntimeError } = require('../../src/error');
1212
const { LEGACY_HELLO_COMMAND } = require('../../src/constants');
13+
const { createTimerSandbox } = require('./timer_sandbox');
1314

1415
describe('driver utils', function () {
1516
context('eachAsync()', function () {
@@ -44,9 +45,10 @@ describe('driver utils', function () {
4445
});
4546

4647
describe('#makeInterruptibleAsyncInterval', function () {
47-
let clock, executor, fnSpy;
48+
let timerSandbox, clock, executor, fnSpy;
4849

4950
beforeEach(function () {
51+
timerSandbox = createTimerSandbox();
5052
clock = sinon.useFakeTimers();
5153
fnSpy = sinon.spy(cb => {
5254
cb();
@@ -58,6 +60,7 @@ describe('driver utils', function () {
5860
executor.stop();
5961
}
6062
clock.restore();
63+
timerSandbox.restore();
6164
});
6265

6366
context('when the immediate option is provided', function () {

0 commit comments

Comments
 (0)