|
1 | 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */
|
2 | 2 | import { expect } from 'chai';
|
| 3 | +import * as sinon from 'sinon'; |
3 | 4 |
|
4 |
| -import { Collection, MongoClient, MongoError, MongoServerError } from '../../../src'; |
| 5 | +import { Collection, MongoClient, MongoServerError, MongoWriteConcernError } from '../../../src'; |
| 6 | +import { Server } from '../../../src/sdam/server'; |
5 | 7 |
|
6 | 8 | describe('Retryable Writes Spec Prose', () => {
|
7 |
| - let client: MongoClient, failPointName; |
8 |
| - |
9 |
| - afterEach(async () => { |
10 |
| - try { |
11 |
| - if (failPointName) { |
12 |
| - await client.db('admin').command({ configureFailPoint: failPointName, mode: 'off' }); |
13 |
| - } |
14 |
| - } finally { |
15 |
| - failPointName = undefined; |
16 |
| - await client?.close(); |
17 |
| - } |
18 |
| - }); |
19 |
| - |
20 | 9 | describe('1. Test that retryable writes raise an exception when using the MMAPv1 storage engine.', () => {
|
| 10 | + let client: MongoClient; |
| 11 | + let failPointName: string | undefined; |
21 | 12 | /**
|
22 | 13 | * For this test, execute a write operation, such as insertOne, which should generate an exception and the error code is 20.
|
23 | 14 | * Assert that the error message is the replacement error message:
|
@@ -46,6 +37,17 @@ describe('Retryable Writes Spec Prose', () => {
|
46 | 37 | expect(failPoint).to.have.property('ok', 1);
|
47 | 38 | });
|
48 | 39 |
|
| 40 | + afterEach(async () => { |
| 41 | + try { |
| 42 | + if (failPointName) { |
| 43 | + await client.db('admin').command({ configureFailPoint: failPointName, mode: 'off' }); |
| 44 | + } |
| 45 | + } finally { |
| 46 | + failPointName = undefined; |
| 47 | + await client?.close(); |
| 48 | + } |
| 49 | + }); |
| 50 | + |
49 | 51 | for (const testTopology of ['replicaset', 'sharded']) {
|
50 | 52 | const minFailPointVersion = testTopology === 'replicaset' ? '>=4.0.0' : '>=4.1.5';
|
51 | 53 | it(`should error with the correct error message when topology is ${testTopology}`, {
|
@@ -74,6 +76,8 @@ describe('Retryable Writes Spec Prose', () => {
|
74 | 76 | // This test MUST be implemented by any driver that implements the CMAP specification.
|
75 | 77 | // This test requires MongoDB 4.2.9+ for blockConnection support in the failpoint.
|
76 | 78 |
|
| 79 | + let client: MongoClient; |
| 80 | + let failPointName: string | undefined; |
77 | 81 | let cmapEvents: Array<{ name: string; event: Record<string, any> }>;
|
78 | 82 | let commandStartedEvents: Array<Record<string, any>>;
|
79 | 83 | let testCollection: Collection;
|
@@ -123,6 +127,17 @@ describe('Retryable Writes Spec Prose', () => {
|
123 | 127 | });
|
124 | 128 | });
|
125 | 129 |
|
| 130 | + afterEach(async () => { |
| 131 | + try { |
| 132 | + if (failPointName) { |
| 133 | + await client.db('admin').command({ configureFailPoint: failPointName, mode: 'off' }); |
| 134 | + } |
| 135 | + } finally { |
| 136 | + failPointName = undefined; |
| 137 | + await client?.close(); |
| 138 | + } |
| 139 | + }); |
| 140 | + |
126 | 141 | it('should emit events in the expected sequence', {
|
127 | 142 | metadata: { requires: { mongodb: '>=4.2.9', topology: ['replicaset', 'sharded'] } },
|
128 | 143 | test: async function () {
|
@@ -188,4 +203,95 @@ describe('Retryable Writes Spec Prose', () => {
|
188 | 203 | }
|
189 | 204 | });
|
190 | 205 | });
|
| 206 | + |
| 207 | + describe('3. Test that drivers return the original error after encountering a WriteConcernError with a RetryableWriteError label', () => { |
| 208 | + let client: MongoClient; |
| 209 | + let collection: Collection<{ _id: 1 }>; |
| 210 | + |
| 211 | + beforeEach(async function () { |
| 212 | + client = this.configuration.newClient({ monitorCommands: true, retryWrites: true }); |
| 213 | + await client |
| 214 | + .db() |
| 215 | + .collection('retryReturnsOriginal') |
| 216 | + .drop() |
| 217 | + .catch(() => null); |
| 218 | + collection = client.db().collection('retryReturnsOriginal'); |
| 219 | + }); |
| 220 | + |
| 221 | + afterEach(async function () { |
| 222 | + sinon.restore(); |
| 223 | + await client.close(); |
| 224 | + }); |
| 225 | + |
| 226 | + /** |
| 227 | + * **NOTE:** Node emits a command failed event for writeConcern errors, making the commandSucceeded part of this test inconsistent see (DRIVERS-2468). |
| 228 | + * Second our event emitters are called synchronously but operations are asynchronous, we don't have a way to make sure a fail point is set before a retry |
| 229 | + * is attempted, if the server allowed us to specify an ordered list of fail points this would be possible, alas we can use sinon. Sinon will set an error |
| 230 | + * to be thrown on the first and second call to Server.command(), this should enter the retry logic for the second error thrown. |
| 231 | + * |
| 232 | + * This test MUST be implemented by any driver that implements the Command Monitoring specification, |
| 233 | + * only run against replica sets as mongos does not propagate the NoWritesPerformed label to the drivers. |
| 234 | + * Additionally, this test requires drivers to set a fail point after an insertOne operation but before the subsequent retry. |
| 235 | + * Drivers that are unable to set a failCommand after the CommandSucceededEvent SHOULD use mocking or write a unit test to cover the same sequence of events. |
| 236 | + * |
| 237 | + * Create a client with retryWrites=true. |
| 238 | + * |
| 239 | + * Configure a fail point with error code 91 (ShutdownInProgress): |
| 240 | + * ```js |
| 241 | + * db.adminCommand({ |
| 242 | + * configureFailPoint: 'failCommand', |
| 243 | + * mode: { times: 1 }, |
| 244 | + * data: { |
| 245 | + * writeConcernError: { |
| 246 | + * code: 91, |
| 247 | + * errorLabels: ['RetryableWriteError'] |
| 248 | + * }, |
| 249 | + * failCommands: ['insert'] |
| 250 | + * } |
| 251 | + * }); |
| 252 | + * ``` |
| 253 | + * Via the command monitoring CommandSucceededEvent, configure a fail point with error code 10107 (NotWritablePrimary) and a NoWritesPerformed label: |
| 254 | + * |
| 255 | + * ```js |
| 256 | + * db.adminCommand({ |
| 257 | + * configureFailPoint: 'failCommand', |
| 258 | + * mode: { times: 1 }, |
| 259 | + * data: { |
| 260 | + * errorCode: 10107, |
| 261 | + * errorLabels: ['RetryableWriteError', 'NoWritesPerformed'], |
| 262 | + * failCommands: ['insert'] |
| 263 | + * } |
| 264 | + * }); |
| 265 | + * ``` |
| 266 | + * Drivers SHOULD only configure the 10107 fail point command if the the succeeded event is for the 91 error configured in step 2. |
| 267 | + * |
| 268 | + * Attempt an insertOne operation on any record for any database and collection. For the resulting error, assert that the associated error code is 91. |
| 269 | + */ |
| 270 | + it( |
| 271 | + 'when a retry attempt fails with an error labeled NoWritesPerformed, drivers MUST return the original error.', |
| 272 | + { requires: { topology: 'replicaset', mongodb: '>=4.2.9' } }, |
| 273 | + async () => { |
| 274 | + const serverCommandStub = sinon.stub(Server.prototype, 'command'); |
| 275 | + serverCommandStub |
| 276 | + .onCall(0) |
| 277 | + .yieldsRight( |
| 278 | + new MongoWriteConcernError({ errorLabels: ['RetryableWriteError'], code: 91 }, {}) |
| 279 | + ); |
| 280 | + serverCommandStub |
| 281 | + .onCall(1) |
| 282 | + .yieldsRight( |
| 283 | + new MongoWriteConcernError( |
| 284 | + { errorLabels: ['RetryableWriteError', 'NoWritesPerformed'], errorCode: 10107 }, |
| 285 | + {} |
| 286 | + ) |
| 287 | + ); |
| 288 | + |
| 289 | + const insertResult = await collection.insertOne({ _id: 1 }).catch(error => error); |
| 290 | + sinon.restore(); |
| 291 | + |
| 292 | + expect(insertResult).to.be.instanceOf(MongoServerError); |
| 293 | + expect(insertResult).to.have.property('code', 91); |
| 294 | + } |
| 295 | + ); |
| 296 | + }); |
191 | 297 | });
|
0 commit comments