-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathclient_side_operations_timeout.unit.test.ts
More file actions
247 lines (211 loc) · 9.45 KB
/
client_side_operations_timeout.unit.test.ts
File metadata and controls
247 lines (211 loc) · 9.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
/**
* The following tests are described in CSOTs spec prose tests as "unit" tests
* The tests enumerated in this section could not be expressed in either spec or prose format.
* Drivers SHOULD implement these if it is possible to do so using the driver's existing test infrastructure.
*/
import { expect } from 'chai';
import * as sinon from 'sinon';
import { setTimeout } from 'timers';
import { TLSSocket } from 'tls';
import { promisify } from 'util';
import { type MongoClient, MongoOperationTimeoutError, ObjectId } from '../../../src';
import { StateMachine } from '../../../src/client-side-encryption/state_machine';
import { Connection } from '../../../src/cmap/connection';
import { ConnectionPool } from '../../../src/cmap/connection_pool';
import { Topology } from '../../../src/sdam/topology';
import { CSOTTimeoutContext, Timeout, TimeoutContext } from '../../../src/timeout';
import { measureDuration, sleep } from '../../tools/utils';
import { createTimerSandbox } from '../../unit/timer_sandbox';
describe('CSOT spec unit tests', function () {
let client: MongoClient;
afterEach(async function () {
sinon.restore();
await client?.close();
});
context('Server Selection and Connection Checkout', function () {
it('Operations should ignore waitQueueTimeoutMS if timeoutMS is also set.', async function () {
client = this.configuration.newClient({ waitQueueTimeoutMS: 999999, timeoutMS: 10000 });
sinon.spy(Timeout, 'expires');
const timeoutContextSpy = sinon.spy(TimeoutContext, 'create');
await client.db('db').collection('collection').insertOne({ x: 1 });
const createCalls = timeoutContextSpy.getCalls().filter(
// @ts-expect-error accessing concrete field
call => call.args[0].timeoutMS === 10000
);
expect(createCalls).to.have.length.greaterThanOrEqual(1);
expect(Timeout.expires).to.not.have.been.calledWith(999999);
});
it('If timeoutMS is set for an operation, the remaining timeoutMS value should apply to connection checkout after a server has been selected.', async function () {
client = this.configuration.newClient({ timeoutMS: 1000 });
// Spy on connection checkout and pull options argument
const checkoutSpy = sinon.spy(ConnectionPool.prototype, 'checkOut');
const expiresSpy = sinon.spy(Timeout, 'expires');
await client.db('db').collection('collection').insertOne({ x: 1 });
expect(checkoutSpy).to.have.been.calledOnce;
const timeoutContext = checkoutSpy.lastCall.args[0].timeoutContext;
expect(timeoutContext).to.exist;
// Check that we passed through the timeout
// @ts-expect-error accessing private properties
expect(timeoutContext._serverSelectionTimeout).to.be.instanceOf(Timeout);
// @ts-expect-error accessing private properties
expect(timeoutContext._serverSelectionTimeout).to.equal(
// @ts-expect-error accessing private properties
timeoutContext._connectionCheckoutTimeout
);
// Check that no more Timeouts are constructed after we enter checkout
expect(!expiresSpy.calledAfter(checkoutSpy));
});
it('If timeoutMS is not set for an operation, waitQueueTimeoutMS should apply to connection checkout after a server has been selected.', async function () {
client = this.configuration.newClient({ waitQueueTimeoutMS: 123456 });
const checkoutSpy = sinon.spy(ConnectionPool.prototype, 'checkOut');
const selectServerSpy = sinon.spy(Topology.prototype, 'selectServer');
const expiresSpy = sinon.spy(Timeout, 'expires');
await client.db('db').collection('collection').insertOne({ x: 1 });
expect(checkoutSpy).to.have.been.calledAfter(selectServerSpy);
expect(expiresSpy).to.have.been.calledWith(123456);
});
/* eslint-disable @typescript-eslint/no-empty-function */
context.skip(
'If a new connection is required to execute an operation, min(remaining computedServerSelectionTimeout, connectTimeoutMS) should apply to socket establishment.',
() => {}
).skipReason =
'TODO(DRIVERS-2347): Requires this ticket to be implemented before we can assert on connection CSOT behaviour';
context(
'For drivers that have control over OCSP behavior, min(remaining computedServerSelectionTimeout, 5 seconds) should apply to HTTP requests against OCSP responders.',
() => {}
);
});
context.skip('Socket timeouts', function () {
context(
'If timeoutMS is unset, operations fail after two non-consecutive socket timeouts.',
() => {}
);
}).skipReason =
'TODO(NODE-6518): Add CSOT support for socket read/write at the connection layer for CRUD APIs';
describe('Client side encryption', function () {
describe('KMS requests', function () {
const stateMachine = new StateMachine({} as any);
const request = {
addResponse: _response => {},
status: {
type: 1,
code: 1,
message: 'notARealStatus'
},
bytesNeeded: 500,
kmsProvider: 'notRealAgain',
endpoint: 'fake',
message: Buffer.from('foobar')
};
context('when StateMachine.kmsRequest() is passed a `CSOTimeoutContext`', function () {
beforeEach(async function () {
sinon.stub(TLSSocket.prototype, 'connect').callsFake(function (..._args) {});
});
afterEach(async function () {
sinon.restore();
});
it('the kms request times out through remainingTimeMS', async function () {
const timeoutContext = new CSOTTimeoutContext({
timeoutMS: 500,
serverSelectionTimeoutMS: 30000
});
const err = await stateMachine.kmsRequest(request, { timeoutContext }).catch(e => e);
expect(err).to.be.instanceOf(MongoOperationTimeoutError);
expect(err.errmsg).to.equal('KMS request timed out');
});
});
context('when StateMachine.kmsRequest() is not passed a `CSOTimeoutContext`', function () {
let clock: sinon.SinonFakeTimers;
let timerSandbox: sinon.SinonSandbox;
let sleep;
beforeEach(async function () {
sinon.stub(TLSSocket.prototype, 'connect').callsFake(function (..._args) {
clock.tick(30000);
});
timerSandbox = createTimerSandbox();
clock = sinon.useFakeTimers();
sleep = promisify(setTimeout);
});
afterEach(async function () {
if (clock) {
timerSandbox.restore();
clock.restore();
clock = undefined;
}
sinon.restore();
});
it('the kms request does not timeout within 30 seconds', async function () {
const sleepingFn = async () => {
await sleep(30000);
throw Error('Slept for 30s');
};
const err$ = Promise.all([stateMachine.kmsRequest(request), sleepingFn()]).catch(e => e);
clock.tick(30000);
const err = await err$;
expect(err.message).to.equal('Slept for 30s');
});
});
});
describe('Auto Encryption', function () {
context(
'when an auto encrypted client is configured with timeoutMS and the command takes longer than timeoutMS',
function () {
let encryptedClient;
const timeoutMS = 500;
beforeEach(async function () {
encryptedClient = this.configuration.newClient(
{},
{
autoEncryption: {
extraOptions: {
mongocryptdURI: 'mongodb://localhost:27020/db?serverSelectionTimeoutMS=2000',
mongocryptdSpawnArgs: [
`--pidfilepath=${new ObjectId().toHexString()}.pid`,
'--port=27020'
],
cryptSharedLibSearchPaths: []
},
keyVaultNamespace: 'admin.datakeys',
kmsProviders: {
aws: { accessKeyId: 'example', secretAccessKey: 'example' },
local: { key: Buffer.alloc(96) }
}
},
timeoutMS
}
);
await encryptedClient.connect();
const stub = sinon
// @ts-expect-error accessing private method
.stub(Connection.prototype, 'sendCommand')
.callsFake(async function* (...args) {
await sleep(timeoutMS + 50);
yield* stub.wrappedMethod.call(this, ...args);
});
});
afterEach(async function () {
await encryptedClient?.close();
sinon.restore();
});
it('the command should fail due to a timeout error', async function () {
const { duration, result: error } = await measureDuration(() =>
encryptedClient
.db()
.command({ ping: 1 })
.catch(e => e)
);
expect(error).to.be.instanceOf(MongoOperationTimeoutError);
expect(duration).to.be.within(timeoutMS - 100, timeoutMS + 100);
});
}
);
});
});
context.skip('Background Connection Pooling', function () {
context(
'When doing minPoolSize maintenance, connectTimeoutMS is used as the timeout for socket establishment.',
() => {}
);
}).skipReason = 'TODO(NODE-6091): Implement CSOT logic for Background Connection Pooling';
/* eslint-enable @typescript-eslint/no-empty-function */
});