-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathtransactions-convenient-api.prose.test.ts
More file actions
247 lines (218 loc) · 8.48 KB
/
transactions-convenient-api.prose.test.ts
File metadata and controls
247 lines (218 loc) · 8.48 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
import { expect } from 'chai';
import { test } from 'mocha';
import * as sinon from 'sinon';
import { type ClientSession, type Collection, type MongoClient, MongoError } from '../../mongodb';
import {
clearFailPoint,
configureFailPoint,
type FailCommandFailPoint,
measureDuration
} from '../../tools/utils';
const failCommand: FailCommandFailPoint = {
configureFailPoint: 'failCommand',
mode: {
times: 13
},
data: {
failCommands: ['commitTransaction'],
errorCode: 251 // no such transaction
}
};
describe('Retry Backoff is Enforced', function () {
// 1. let client be a MongoClient
let client: MongoClient;
// 2. let coll be a collection
let collection: Collection;
beforeEach(async function () {
client = this.configuration.newClient();
collection = client.db('foo').collection('bar');
});
afterEach(async function () {
sinon.restore();
await client?.close();
});
test(
'works',
{
requires: {
mongodb: '>=4.4', // failCommand
topology: '!single' // transactions can't run on standalone servers
}
},
async function () {
const randomStub = sinon.stub(Math, 'random');
// 3.i Configure the random number generator used for jitter to always return 0
randomStub.returns(0);
// 3.ii Configure a fail point that forces 13 retries
await configureFailPoint(this.configuration, failCommand);
// 3.iii
const callback = async (s: ClientSession) => {
await collection.insertOne({}, { session: s });
};
// 3.iv Let no_backoff_time be the duration of the withTransaction API call
const { duration: noBackoffTime } = await measureDuration(() => {
return client.withSession(async s => {
await s.withTransaction(callback);
});
});
// 4.i Configure the random number generator used for jitter to always return 1.
randomStub.returns(1);
// 4.ii Configure a fail point that forces 13 retries like in step 3.2.
await configureFailPoint(this.configuration, failCommand);
// 4.iii Use the same callback defined in 3.3.
// 4.iv Let with_backoff_time be the duration of the withTransaction API call
const { duration: fullBackoffDuration } = await measureDuration(() => {
return client.withSession(async s => {
await s.withTransaction(callback);
});
});
// 5. Compare the two time between the two runs.
// The sum of 13 backoffs is roughly 1.8 seconds. There is a half-second window to account for potential variance between the two runs.
expect(fullBackoffDuration).to.be.within(
noBackoffTime + 1800 - 500,
noBackoffTime + 1800 + 500
);
}
);
});
describe('Retry Timeout is Enforced', function () {
// Drivers should test that withTransaction enforces a non-configurable timeout before retrying
// both commits and entire transactions.
//
// We stub performance.now() to simulate elapsed time exceeding the 120-second retry limit,
// as recommended by the spec: "This might be done by internally modifying the timeout value
// used by withTransaction with some private API or using a mock timer."
//
// Without CSOT, the original error is propagated directly.
// With CSOT, the error is wrapped in a MongoOperationTimeoutError.
let client: MongoClient;
let collection: Collection;
let timeOffset: number;
beforeEach(async function () {
client = this.configuration.newClient();
collection = client.db('foo').collection('bar');
timeOffset = 0;
const originalNow = performance.now.bind(performance);
sinon.stub(performance, 'now').callsFake(() => originalNow() + timeOffset);
});
afterEach(async function () {
sinon.restore();
await clearFailPoint(this.configuration);
await client?.close();
});
// Case 1: If the callback raises an error with the TransientTransactionError label and the retry
// timeout has been exceeded, withTransaction should propagate the error to its caller.
test(
'callback TransientTransactionError propagated when retry timeout exceeded',
{
requires: {
mongodb: '>=4.4',
topology: '!single'
}
},
async function () {
// 1. Configure a failpoint that fails insert with TransientTransactionError.
await configureFailPoint(this.configuration, {
configureFailPoint: 'failCommand',
mode: { times: 1 },
data: {
failCommands: ['insert'],
errorCode: 24,
errorLabels: ['TransientTransactionError']
}
});
// 2. Run withTransaction. The callback advances the clock past the 120-second retry
// limit before the insert fails, so the timeout is detected immediately.
const { result } = await measureDuration(() => {
return client.withSession(async s => {
await s.withTransaction(async session => {
timeOffset = 120_000;
await collection.insertOne({}, { session });
});
});
});
// 3. Assert that the error is the original TransientTransactionError (propagated directly
// in the legacy non-CSOT path).
expect(result).to.be.instanceOf(MongoError);
expect((result as MongoError).hasErrorLabel('TransientTransactionError')).to.be.true;
}
);
// Case 2: If committing raises an error with the UnknownTransactionCommitResult label, and the
// retry timeout has been exceeded, withTransaction should propagate the error to
// its caller.
test(
'commit UnknownTransactionCommitResult propagated when retry timeout exceeded',
{
requires: {
mongodb: '>=4.4',
topology: '!single'
}
},
async function () {
// 1. Configure a failpoint that fails commitTransaction with UnknownTransactionCommitResult.
await configureFailPoint(this.configuration, {
configureFailPoint: 'failCommand',
mode: { times: 1 },
data: {
failCommands: ['commitTransaction'],
errorCode: 64,
errorLabels: ['UnknownTransactionCommitResult']
}
});
// 2. Run withTransaction. The callback advances the clock past the 120-second retry
// limit. The insert succeeds, but the commit fails and the timeout is detected.
const { result } = await measureDuration(() => {
return client.withSession(async s => {
await s.withTransaction(async session => {
timeOffset = 120_000;
await collection.insertOne({}, { session });
});
});
});
// 3. Assert that the error is the original commit error (propagated directly
// in the legacy non-CSOT path).
expect(result).to.be.instanceOf(MongoError);
expect((result as MongoError).hasErrorLabel('UnknownTransactionCommitResult')).to.be.true;
}
);
// Case 3: If committing raises an error with the TransientTransactionError label and the retry
// timeout has been exceeded, withTransaction should propagate the error to its
// caller. This case may occur if the commit was internally retried against a new primary after a
// failover and the second primary returned a NoSuchTransaction error response.
test(
'commit TransientTransactionError propagated when retry timeout exceeded',
{
requires: {
mongodb: '>=4.4',
topology: '!single'
}
},
async function () {
// 1. Configure a failpoint that fails commitTransaction with TransientTransactionError
// (errorCode 251 = NoSuchTransaction).
await configureFailPoint(this.configuration, {
configureFailPoint: 'failCommand',
mode: { times: 1 },
data: {
failCommands: ['commitTransaction'],
errorCode: 251,
errorLabels: ['TransientTransactionError']
}
});
// 2. Run withTransaction. The callback advances the clock past the 120-second retry
// limit. The insert succeeds, but the commit fails and the timeout is detected.
const { result } = await measureDuration(() => {
return client.withSession(async s => {
await s.withTransaction(async session => {
timeOffset = 120_000;
await collection.insertOne({}, { session });
});
});
});
// 3. Assert that the error is the original commit error (propagated directly
// in the legacy non-CSOT path).
expect(result).to.be.instanceOf(MongoError);
expect((result as MongoError).hasErrorLabel('TransientTransactionError')).to.be.true;
}
);
});