From 74330d93b111081d33841f8639f4e7b02ad412b7 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 9 Mar 2022 09:27:11 +0100 Subject: [PATCH 01/21] test(NODE-3688): sync spec tests --- src/cmap/connect.ts | 3 +- src/error.ts | 15 + .../retryable_reads.spec.test.js | 10 +- test/spec/retryable-reads/README.rst | 97 +++++- .../{ => legacy}/aggregate-merge.json | 0 .../{ => legacy}/aggregate-merge.yml | 0 .../{ => legacy}/aggregate-serverErrors.json | 0 .../{ => legacy}/aggregate-serverErrors.yml | 0 .../{ => legacy}/aggregate.json | 0 .../{ => legacy}/aggregate.yml | 0 ...angeStreams-client.watch-serverErrors.json | 0 ...hangeStreams-client.watch-serverErrors.yml | 0 .../changeStreams-client.watch.json | 0 .../changeStreams-client.watch.yml | 0 ...ngeStreams-db.coll.watch-serverErrors.json | 0 ...angeStreams-db.coll.watch-serverErrors.yml | 0 .../changeStreams-db.coll.watch.json | 0 .../changeStreams-db.coll.watch.yml | 0 .../changeStreams-db.watch-serverErrors.json | 0 .../changeStreams-db.watch-serverErrors.yml | 0 .../{ => legacy}/changeStreams-db.watch.json | 0 .../{ => legacy}/changeStreams-db.watch.yml | 0 .../{ => legacy}/count-serverErrors.json | 0 .../{ => legacy}/count-serverErrors.yml | 0 .../retryable-reads/{ => legacy}/count.json | 0 .../retryable-reads/{ => legacy}/count.yml | 0 .../countDocuments-serverErrors.json | 0 .../countDocuments-serverErrors.yml | 0 .../{ => legacy}/countDocuments.json | 0 .../{ => legacy}/countDocuments.yml | 0 .../{ => legacy}/distinct-serverErrors.json | 0 .../{ => legacy}/distinct-serverErrors.yml | 0 .../{ => legacy}/distinct.json | 0 .../retryable-reads/{ => legacy}/distinct.yml | 0 .../estimatedDocumentCount-4.9.json | 0 .../estimatedDocumentCount-4.9.yml | 0 .../estimatedDocumentCount-pre4.9.json | 0 .../estimatedDocumentCount-pre4.9.yml | 0 ...timatedDocumentCount-serverErrors-4.9.json | 0 ...stimatedDocumentCount-serverErrors-4.9.yml | 0 ...atedDocumentCount-serverErrors-pre4.9.json | 0 ...matedDocumentCount-serverErrors-pre4.9.yml | 0 .../{ => legacy}/find-serverErrors.json | 0 .../{ => legacy}/find-serverErrors.yml | 0 .../retryable-reads/{ => legacy}/find.json | 0 .../retryable-reads/{ => legacy}/find.yml | 0 .../{ => legacy}/findOne-serverErrors.json | 0 .../{ => legacy}/findOne-serverErrors.yml | 0 .../retryable-reads/{ => legacy}/findOne.json | 0 .../retryable-reads/{ => legacy}/findOne.yml | 0 .../gridfs-download-serverErrors.json | 0 .../gridfs-download-serverErrors.yml | 0 .../{ => legacy}/gridfs-download.json | 0 .../{ => legacy}/gridfs-download.yml | 0 .../gridfs-downloadByName-serverErrors.json | 0 .../gridfs-downloadByName-serverErrors.yml | 0 .../{ => legacy}/gridfs-downloadByName.json | 0 .../{ => legacy}/gridfs-downloadByName.yml | 0 .../listCollectionNames-serverErrors.json | 0 .../listCollectionNames-serverErrors.yml | 0 .../{ => legacy}/listCollectionNames.json | 0 .../{ => legacy}/listCollectionNames.yml | 0 .../listCollectionObjects-serverErrors.json | 0 .../listCollectionObjects-serverErrors.yml | 0 .../{ => legacy}/listCollectionObjects.json | 0 .../{ => legacy}/listCollectionObjects.yml | 0 .../listCollections-serverErrors.json | 0 .../listCollections-serverErrors.yml | 0 .../{ => legacy}/listCollections.json | 0 .../{ => legacy}/listCollections.yml | 0 .../listDatabaseNames-serverErrors.json | 0 .../listDatabaseNames-serverErrors.yml | 0 .../{ => legacy}/listDatabaseNames.json | 0 .../{ => legacy}/listDatabaseNames.yml | 0 .../listDatabaseObjects-serverErrors.json | 0 .../listDatabaseObjects-serverErrors.yml | 0 .../{ => legacy}/listDatabaseObjects.json | 0 .../{ => legacy}/listDatabaseObjects.yml | 0 .../listDatabases-serverErrors.json | 0 .../listDatabases-serverErrors.yml | 0 .../{ => legacy}/listDatabases.json | 0 .../{ => legacy}/listDatabases.yml | 0 .../listIndexNames-serverErrors.json | 0 .../listIndexNames-serverErrors.yml | 0 .../{ => legacy}/listIndexNames.json | 0 .../{ => legacy}/listIndexNames.yml | 0 .../listIndexes-serverErrors.json | 0 .../{ => legacy}/listIndexes-serverErrors.yml | 0 .../{ => legacy}/listIndexes.json | 0 .../{ => legacy}/listIndexes.yml | 0 .../{ => legacy}/mapReduce.json | 0 .../{ => legacy}/mapReduce.yml | 0 .../unified/handshakeError.json | 257 ++++++++++++++++ .../unified/handshakeError.yml | 129 ++++++++ .../retryable-writes/{legacy => }/README.rst | 85 +++++- test/spec/retryable-writes/unified/.gitkeep | 0 .../unified/bulkWrite-serverErrors.json | 205 +++++++++++++ .../unified/bulkWrite-serverErrors.yml | 96 ++++++ .../unified/handshakeError.json | 279 ++++++++++++++++++ .../unified/handshakeError.yml | 137 +++++++++ .../unified/insertOne-serverErrors.json | 173 +++++++++++ .../unified/insertOne-serverErrors.yml | 78 +++++ test/spec/transactions/README.rst | 17 ++ .../transactions/unified/mongos-unpin.json | 107 ++++++- .../transactions/unified/mongos-unpin.yml | 42 ++- .../unified/retryable-abort-handshake.json | 204 +++++++++++++ .../unified/retryable-abort-handshake.yml | 118 ++++++++ .../unified/retryable-commit-handshake.json | 211 +++++++++++++ .../unified/retryable-commit-handshake.yml | 118 ++++++++ 109 files changed, 2350 insertions(+), 31 deletions(-) rename test/spec/retryable-reads/{ => legacy}/aggregate-merge.json (100%) rename test/spec/retryable-reads/{ => legacy}/aggregate-merge.yml (100%) rename test/spec/retryable-reads/{ => legacy}/aggregate-serverErrors.json (100%) rename test/spec/retryable-reads/{ => legacy}/aggregate-serverErrors.yml (100%) rename test/spec/retryable-reads/{ => legacy}/aggregate.json (100%) rename test/spec/retryable-reads/{ => legacy}/aggregate.yml (100%) rename test/spec/retryable-reads/{ => legacy}/changeStreams-client.watch-serverErrors.json (100%) rename test/spec/retryable-reads/{ => legacy}/changeStreams-client.watch-serverErrors.yml (100%) rename test/spec/retryable-reads/{ => legacy}/changeStreams-client.watch.json (100%) rename test/spec/retryable-reads/{ => legacy}/changeStreams-client.watch.yml (100%) rename test/spec/retryable-reads/{ => legacy}/changeStreams-db.coll.watch-serverErrors.json (100%) rename test/spec/retryable-reads/{ => legacy}/changeStreams-db.coll.watch-serverErrors.yml (100%) rename test/spec/retryable-reads/{ => legacy}/changeStreams-db.coll.watch.json (100%) rename test/spec/retryable-reads/{ => legacy}/changeStreams-db.coll.watch.yml (100%) rename test/spec/retryable-reads/{ => legacy}/changeStreams-db.watch-serverErrors.json (100%) rename test/spec/retryable-reads/{ => legacy}/changeStreams-db.watch-serverErrors.yml (100%) rename test/spec/retryable-reads/{ => legacy}/changeStreams-db.watch.json (100%) rename test/spec/retryable-reads/{ => legacy}/changeStreams-db.watch.yml (100%) rename test/spec/retryable-reads/{ => legacy}/count-serverErrors.json (100%) rename test/spec/retryable-reads/{ => legacy}/count-serverErrors.yml (100%) rename test/spec/retryable-reads/{ => legacy}/count.json (100%) rename test/spec/retryable-reads/{ => legacy}/count.yml (100%) rename test/spec/retryable-reads/{ => legacy}/countDocuments-serverErrors.json (100%) rename test/spec/retryable-reads/{ => legacy}/countDocuments-serverErrors.yml (100%) rename test/spec/retryable-reads/{ => legacy}/countDocuments.json (100%) rename test/spec/retryable-reads/{ => legacy}/countDocuments.yml (100%) rename test/spec/retryable-reads/{ => legacy}/distinct-serverErrors.json (100%) rename test/spec/retryable-reads/{ => legacy}/distinct-serverErrors.yml (100%) rename test/spec/retryable-reads/{ => legacy}/distinct.json (100%) rename test/spec/retryable-reads/{ => legacy}/distinct.yml (100%) rename test/spec/retryable-reads/{ => legacy}/estimatedDocumentCount-4.9.json (100%) rename test/spec/retryable-reads/{ => legacy}/estimatedDocumentCount-4.9.yml (100%) rename test/spec/retryable-reads/{ => legacy}/estimatedDocumentCount-pre4.9.json (100%) rename test/spec/retryable-reads/{ => legacy}/estimatedDocumentCount-pre4.9.yml (100%) rename test/spec/retryable-reads/{ => legacy}/estimatedDocumentCount-serverErrors-4.9.json (100%) rename test/spec/retryable-reads/{ => legacy}/estimatedDocumentCount-serverErrors-4.9.yml (100%) rename test/spec/retryable-reads/{ => legacy}/estimatedDocumentCount-serverErrors-pre4.9.json (100%) rename test/spec/retryable-reads/{ => legacy}/estimatedDocumentCount-serverErrors-pre4.9.yml (100%) rename test/spec/retryable-reads/{ => legacy}/find-serverErrors.json (100%) rename test/spec/retryable-reads/{ => legacy}/find-serverErrors.yml (100%) rename test/spec/retryable-reads/{ => legacy}/find.json (100%) rename test/spec/retryable-reads/{ => legacy}/find.yml (100%) rename test/spec/retryable-reads/{ => legacy}/findOne-serverErrors.json (100%) rename test/spec/retryable-reads/{ => legacy}/findOne-serverErrors.yml (100%) rename test/spec/retryable-reads/{ => legacy}/findOne.json (100%) rename test/spec/retryable-reads/{ => legacy}/findOne.yml (100%) rename test/spec/retryable-reads/{ => legacy}/gridfs-download-serverErrors.json (100%) rename test/spec/retryable-reads/{ => legacy}/gridfs-download-serverErrors.yml (100%) rename test/spec/retryable-reads/{ => legacy}/gridfs-download.json (100%) rename test/spec/retryable-reads/{ => legacy}/gridfs-download.yml (100%) rename test/spec/retryable-reads/{ => legacy}/gridfs-downloadByName-serverErrors.json (100%) rename test/spec/retryable-reads/{ => legacy}/gridfs-downloadByName-serverErrors.yml (100%) rename test/spec/retryable-reads/{ => legacy}/gridfs-downloadByName.json (100%) rename test/spec/retryable-reads/{ => legacy}/gridfs-downloadByName.yml (100%) rename test/spec/retryable-reads/{ => legacy}/listCollectionNames-serverErrors.json (100%) rename test/spec/retryable-reads/{ => legacy}/listCollectionNames-serverErrors.yml (100%) rename test/spec/retryable-reads/{ => legacy}/listCollectionNames.json (100%) rename test/spec/retryable-reads/{ => legacy}/listCollectionNames.yml (100%) rename test/spec/retryable-reads/{ => legacy}/listCollectionObjects-serverErrors.json (100%) rename test/spec/retryable-reads/{ => legacy}/listCollectionObjects-serverErrors.yml (100%) rename test/spec/retryable-reads/{ => legacy}/listCollectionObjects.json (100%) rename test/spec/retryable-reads/{ => legacy}/listCollectionObjects.yml (100%) rename test/spec/retryable-reads/{ => legacy}/listCollections-serverErrors.json (100%) rename test/spec/retryable-reads/{ => legacy}/listCollections-serverErrors.yml (100%) rename test/spec/retryable-reads/{ => legacy}/listCollections.json (100%) rename test/spec/retryable-reads/{ => legacy}/listCollections.yml (100%) rename test/spec/retryable-reads/{ => legacy}/listDatabaseNames-serverErrors.json (100%) rename test/spec/retryable-reads/{ => legacy}/listDatabaseNames-serverErrors.yml (100%) rename test/spec/retryable-reads/{ => legacy}/listDatabaseNames.json (100%) rename test/spec/retryable-reads/{ => legacy}/listDatabaseNames.yml (100%) rename test/spec/retryable-reads/{ => legacy}/listDatabaseObjects-serverErrors.json (100%) rename test/spec/retryable-reads/{ => legacy}/listDatabaseObjects-serverErrors.yml (100%) rename test/spec/retryable-reads/{ => legacy}/listDatabaseObjects.json (100%) rename test/spec/retryable-reads/{ => legacy}/listDatabaseObjects.yml (100%) rename test/spec/retryable-reads/{ => legacy}/listDatabases-serverErrors.json (100%) rename test/spec/retryable-reads/{ => legacy}/listDatabases-serverErrors.yml (100%) rename test/spec/retryable-reads/{ => legacy}/listDatabases.json (100%) rename test/spec/retryable-reads/{ => legacy}/listDatabases.yml (100%) rename test/spec/retryable-reads/{ => legacy}/listIndexNames-serverErrors.json (100%) rename test/spec/retryable-reads/{ => legacy}/listIndexNames-serverErrors.yml (100%) rename test/spec/retryable-reads/{ => legacy}/listIndexNames.json (100%) rename test/spec/retryable-reads/{ => legacy}/listIndexNames.yml (100%) rename test/spec/retryable-reads/{ => legacy}/listIndexes-serverErrors.json (100%) rename test/spec/retryable-reads/{ => legacy}/listIndexes-serverErrors.yml (100%) rename test/spec/retryable-reads/{ => legacy}/listIndexes.json (100%) rename test/spec/retryable-reads/{ => legacy}/listIndexes.yml (100%) rename test/spec/retryable-reads/{ => legacy}/mapReduce.json (100%) rename test/spec/retryable-reads/{ => legacy}/mapReduce.yml (100%) create mode 100644 test/spec/retryable-reads/unified/handshakeError.json create mode 100644 test/spec/retryable-reads/unified/handshakeError.yml rename test/spec/retryable-writes/{legacy => }/README.rst (81%) delete mode 100644 test/spec/retryable-writes/unified/.gitkeep create mode 100644 test/spec/retryable-writes/unified/bulkWrite-serverErrors.json create mode 100644 test/spec/retryable-writes/unified/bulkWrite-serverErrors.yml create mode 100644 test/spec/retryable-writes/unified/handshakeError.json create mode 100644 test/spec/retryable-writes/unified/handshakeError.yml create mode 100644 test/spec/retryable-writes/unified/insertOne-serverErrors.json create mode 100644 test/spec/retryable-writes/unified/insertOne-serverErrors.yml create mode 100644 test/spec/transactions/unified/retryable-abort-handshake.json create mode 100644 test/spec/transactions/unified/retryable-abort-handshake.yml create mode 100644 test/spec/transactions/unified/retryable-commit-handshake.json create mode 100644 test/spec/transactions/unified/retryable-commit-handshake.yml diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index 0520c5d07e..0f1ca7fe9a 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -10,6 +10,7 @@ import { LEGACY_HELLO_COMMAND } from '../constants'; import { AnyError, MongoCompatibilityError, + MongoHandshakeError, MongoInvalidArgumentError, MongoNetworkError, MongoNetworkTimeoutError, @@ -127,7 +128,7 @@ function performInitialHandshake( const start = new Date().getTime(); conn.command(ns('admin.$cmd'), handshakeDoc, handshakeOptions, (err, response) => { if (err) { - callback(err); + callback(new MongoHandshakeError(err.message)); return; } diff --git a/src/error.ts b/src/error.ts index 5ea75e84ee..443ba61f19 100644 --- a/src/error.ts +++ b/src/error.ts @@ -204,6 +204,21 @@ export class MongoDriverError extends MongoError { } } +/** + * An error during the handshake. + * + * @public + * @category Error + */ +export class MongoHandshakeError extends MongoError { + constructor(message: string) { + super(message); + } + + get name(): string { + return 'MongoHandshakeError'; + } +} /** * An error generated when the driver API is used incorrectly * diff --git a/test/integration/retryable-reads/retryable_reads.spec.test.js b/test/integration/retryable-reads/retryable_reads.spec.test.js index 429fbd4a76..166563ae10 100644 --- a/test/integration/retryable-reads/retryable_reads.spec.test.js +++ b/test/integration/retryable-reads/retryable_reads.spec.test.js @@ -1,11 +1,13 @@ 'use strict'; +const path = require('path'); const { TestRunnerContext, generateTopologyTests } = require('../../tools/spec-runner'); const { loadSpecTests } = require('../../spec'); +const { runUnifiedSuite } = require('../../tools/unified-spec-runner/runner'); -describe('Retryable Reads', function () { +describe('Retryable Reads (legacy)', function () { const testContext = new TestRunnerContext(); - const testSuites = loadSpecTests('retryable-reads'); + const testSuites = loadSpecTests(path.join('retryable-reads', 'legacy')); after(() => testContext.teardown()); before(function () { @@ -28,3 +30,7 @@ describe('Retryable Reads', function () { ); }); }); + +describe('Retryable Reads (unified)', function () { + runUnifiedSuite(loadSpecTests(path.join('retryable-reads', 'unified'))); +}); diff --git a/test/spec/retryable-reads/README.rst b/test/spec/retryable-reads/README.rst index 0f56d23b0d..06c9bb7886 100644 --- a/test/spec/retryable-reads/README.rst +++ b/test/spec/retryable-reads/README.rst @@ -9,8 +9,10 @@ Retryable Reads Tests Introduction ============ -The YAML and JSON files in this directory tree are platform-independent tests -that drivers can use to prove their conformance to the Retryable Reads spec. +The YAML and JSON files in the ``legacy`` and ``unified`` sub-directories are platform-independent tests +that drivers can use to prove their conformance to the Retryable Reads spec. Tests in the +``unified`` directory are written using the `Unified Test Format <../../unified-test-format/unified-test-format.rst>`_. +Tests in the ``legacy`` directory are written using the format described below. Prose tests, which are not easily expressed in YAML, are also presented in this file. Those tests will need to be manually implemented by each driver. @@ -79,9 +81,21 @@ Each YAML file has the following keys: the default is all topologies (i.e. ``["single", "replicaset", "sharded", "load-balanced"]``). + - ``serverless``: Optional string. Whether or not the test should be run on + serverless instances imitating sharded clusters. Valid values are "require", + "forbid", and "allow". If "require", the test MUST only be run on serverless + instances. If "forbid", the test MUST NOT be run on serverless instances. If + omitted or "allow", this option has no effect. + + The test runner MUST be informed whether or not serverless is being used in + order to determine if this requirement is met (e.g. through an environment + variable or configuration option). Since the serverless proxy imitates a + mongos, the runner is not capable of determining this by issuing a server + command such as ``buildInfo`` or ``hello``. + - ``database_name`` and ``collection_name``: Optional. The database and collection to use for testing. - + - ``bucket_name``: Optional. The GridFS bucket name to use for testing. - ``data``: The data that should exist in the collection(s) under test before @@ -89,19 +103,27 @@ Each YAML file has the following keys: into the collection under test (i.e. ``collection_name``); however, this field may also be an object mapping collection names to arrays of documents to be inserted into the specified collection. - + - ``tests``: An array of tests that are to be run independently of each other. Each test will have some or all of the following fields: - ``description``: The name of the test. - + - ``clientOptions``: Optional, parameters to pass to MongoClient(). - - ``useMultipleMongoses`` (optional): If ``true``, the MongoClient for this - test should be initialized with multiple mongos seed addresses. If ``false`` - or omitted, only a single mongos address should be specified. This field has - no effect for non-sharded topologies. - + - ``useMultipleMongoses`` (optional): If ``true``, and the topology type is + ``Sharded``, the MongoClient for this test should be initialized with multiple + mongos seed addresses. If ``false`` or omitted, only a single mongos address + should be specified. + + If ``true``, and the topology type is ``LoadBalanced``, the MongoClient for + this test should be initialized with the URI of the load balancer fronting + multiple servers. If ``false`` or omitted, the MongoClient for this test + should be initialized with the URI of the load balancer fronting a single + server. + + ``useMultipleMongoses`` only affects ``Sharded`` and ``LoadBalanced`` topologies. + - ``skipReason``: Optional, string describing why this test should be skipped. - ``failPoint``: Optional, a server fail point to enable, expressed as the @@ -120,10 +142,10 @@ Each YAML file has the following keys: - ``result``: Optional. The return value from the operation, if any. This field may be a scalar (e.g. in the case of a count), a single document, or an array of documents in the case of a multi-document read. - + - ``error``: Optional. If ``true``, the test should expect an error or exception. - + - ``expectations``: Optional list of command-started events. GridFS Tests @@ -147,7 +169,8 @@ data. .. _GridFSBucket spec: https://github.com/mongodb/specifications/blob/master/source/gridfs/gridfs-spec.rst#configurable-gridfsbucket-class - + + Speeding Up Tests ----------------- @@ -165,9 +188,53 @@ Optional Enumeration Commands A driver only needs to test the optional enumeration commands it has chosen to implement (e.g. ``Database.listCollectionNames()``). +PoolClearedError Retryability Test +================================== + +This test will be used to ensure drivers properly retry after encountering PoolClearedErrors. +It MUST be implemented by any driver that implements the CMAP specification. +This test requires MongoDB 4.2.9+ for ``blockConnection`` support in the failpoint. + +1. Create a client with maxPoolSize=1 and retryReads=true. If testing against a + sharded deployment, be sure to connect to only a single mongos. + +2. Enable the following failpoint:: + + { + configureFailPoint: "failCommand", + mode: { times: 1 }, + data: { + failCommands: ["find"], + errorCode: 91, + blockConnection: true, + blockTimeMS: 1000 + } + } + +3. Start two threads and attempt to perform a ``findOne`` simultaneously on both. + +4. Verify that both ``findOne`` attempts succeed. + +5. Via CMAP monitoring, assert that the first check out succeeds. + +6. Via CMAP monitoring, assert that a PoolClearedEvent is then emitted. + +7. Via CMAP monitoring, assert that the second check out then fails due to a + connection error. + +8. Via Command Monitoring, assert that exactly three ``find`` CommandStartedEvents + were observed in total. + +9. Disable the failpoint. + + Changelog ========= +:2022-01-10: Create legacy and unified subdirectories for new unified tests + +:2021-08-27: Clarify behavior of ``useMultipleMongoses`` for ``LoadBalanced`` topologies. + :2019-03-19: Add top-level ``runOn`` field to denote server version and/or topology requirements requirements for the test file. Removes the ``minServerVersion`` and ``topology`` top-level fields, which are @@ -177,4 +244,6 @@ Changelog :2020-09-16: Suggest lowering heartbeatFrequencyMS in addition to minHeartbeatFrequencyMS. -:2021-04-23: Add ``load-balanced`` to test topology requirements. +:2021-03-23: Add prose test for retrying PoolClearedErrors + +:2021-04-29: Add ``load-balanced`` to test topology requirements. diff --git a/test/spec/retryable-reads/aggregate-merge.json b/test/spec/retryable-reads/legacy/aggregate-merge.json similarity index 100% rename from test/spec/retryable-reads/aggregate-merge.json rename to test/spec/retryable-reads/legacy/aggregate-merge.json diff --git a/test/spec/retryable-reads/aggregate-merge.yml b/test/spec/retryable-reads/legacy/aggregate-merge.yml similarity index 100% rename from test/spec/retryable-reads/aggregate-merge.yml rename to test/spec/retryable-reads/legacy/aggregate-merge.yml diff --git a/test/spec/retryable-reads/aggregate-serverErrors.json b/test/spec/retryable-reads/legacy/aggregate-serverErrors.json similarity index 100% rename from test/spec/retryable-reads/aggregate-serverErrors.json rename to test/spec/retryable-reads/legacy/aggregate-serverErrors.json diff --git a/test/spec/retryable-reads/aggregate-serverErrors.yml b/test/spec/retryable-reads/legacy/aggregate-serverErrors.yml similarity index 100% rename from test/spec/retryable-reads/aggregate-serverErrors.yml rename to test/spec/retryable-reads/legacy/aggregate-serverErrors.yml diff --git a/test/spec/retryable-reads/aggregate.json b/test/spec/retryable-reads/legacy/aggregate.json similarity index 100% rename from test/spec/retryable-reads/aggregate.json rename to test/spec/retryable-reads/legacy/aggregate.json diff --git a/test/spec/retryable-reads/aggregate.yml b/test/spec/retryable-reads/legacy/aggregate.yml similarity index 100% rename from test/spec/retryable-reads/aggregate.yml rename to test/spec/retryable-reads/legacy/aggregate.yml diff --git a/test/spec/retryable-reads/changeStreams-client.watch-serverErrors.json b/test/spec/retryable-reads/legacy/changeStreams-client.watch-serverErrors.json similarity index 100% rename from test/spec/retryable-reads/changeStreams-client.watch-serverErrors.json rename to test/spec/retryable-reads/legacy/changeStreams-client.watch-serverErrors.json diff --git a/test/spec/retryable-reads/changeStreams-client.watch-serverErrors.yml b/test/spec/retryable-reads/legacy/changeStreams-client.watch-serverErrors.yml similarity index 100% rename from test/spec/retryable-reads/changeStreams-client.watch-serverErrors.yml rename to test/spec/retryable-reads/legacy/changeStreams-client.watch-serverErrors.yml diff --git a/test/spec/retryable-reads/changeStreams-client.watch.json b/test/spec/retryable-reads/legacy/changeStreams-client.watch.json similarity index 100% rename from test/spec/retryable-reads/changeStreams-client.watch.json rename to test/spec/retryable-reads/legacy/changeStreams-client.watch.json diff --git a/test/spec/retryable-reads/changeStreams-client.watch.yml b/test/spec/retryable-reads/legacy/changeStreams-client.watch.yml similarity index 100% rename from test/spec/retryable-reads/changeStreams-client.watch.yml rename to test/spec/retryable-reads/legacy/changeStreams-client.watch.yml diff --git a/test/spec/retryable-reads/changeStreams-db.coll.watch-serverErrors.json b/test/spec/retryable-reads/legacy/changeStreams-db.coll.watch-serverErrors.json similarity index 100% rename from test/spec/retryable-reads/changeStreams-db.coll.watch-serverErrors.json rename to test/spec/retryable-reads/legacy/changeStreams-db.coll.watch-serverErrors.json diff --git a/test/spec/retryable-reads/changeStreams-db.coll.watch-serverErrors.yml b/test/spec/retryable-reads/legacy/changeStreams-db.coll.watch-serverErrors.yml similarity index 100% rename from test/spec/retryable-reads/changeStreams-db.coll.watch-serverErrors.yml rename to test/spec/retryable-reads/legacy/changeStreams-db.coll.watch-serverErrors.yml diff --git a/test/spec/retryable-reads/changeStreams-db.coll.watch.json b/test/spec/retryable-reads/legacy/changeStreams-db.coll.watch.json similarity index 100% rename from test/spec/retryable-reads/changeStreams-db.coll.watch.json rename to test/spec/retryable-reads/legacy/changeStreams-db.coll.watch.json diff --git a/test/spec/retryable-reads/changeStreams-db.coll.watch.yml b/test/spec/retryable-reads/legacy/changeStreams-db.coll.watch.yml similarity index 100% rename from test/spec/retryable-reads/changeStreams-db.coll.watch.yml rename to test/spec/retryable-reads/legacy/changeStreams-db.coll.watch.yml diff --git a/test/spec/retryable-reads/changeStreams-db.watch-serverErrors.json b/test/spec/retryable-reads/legacy/changeStreams-db.watch-serverErrors.json similarity index 100% rename from test/spec/retryable-reads/changeStreams-db.watch-serverErrors.json rename to test/spec/retryable-reads/legacy/changeStreams-db.watch-serverErrors.json diff --git a/test/spec/retryable-reads/changeStreams-db.watch-serverErrors.yml b/test/spec/retryable-reads/legacy/changeStreams-db.watch-serverErrors.yml similarity index 100% rename from test/spec/retryable-reads/changeStreams-db.watch-serverErrors.yml rename to test/spec/retryable-reads/legacy/changeStreams-db.watch-serverErrors.yml diff --git a/test/spec/retryable-reads/changeStreams-db.watch.json b/test/spec/retryable-reads/legacy/changeStreams-db.watch.json similarity index 100% rename from test/spec/retryable-reads/changeStreams-db.watch.json rename to test/spec/retryable-reads/legacy/changeStreams-db.watch.json diff --git a/test/spec/retryable-reads/changeStreams-db.watch.yml b/test/spec/retryable-reads/legacy/changeStreams-db.watch.yml similarity index 100% rename from test/spec/retryable-reads/changeStreams-db.watch.yml rename to test/spec/retryable-reads/legacy/changeStreams-db.watch.yml diff --git a/test/spec/retryable-reads/count-serverErrors.json b/test/spec/retryable-reads/legacy/count-serverErrors.json similarity index 100% rename from test/spec/retryable-reads/count-serverErrors.json rename to test/spec/retryable-reads/legacy/count-serverErrors.json diff --git a/test/spec/retryable-reads/count-serverErrors.yml b/test/spec/retryable-reads/legacy/count-serverErrors.yml similarity index 100% rename from test/spec/retryable-reads/count-serverErrors.yml rename to test/spec/retryable-reads/legacy/count-serverErrors.yml diff --git a/test/spec/retryable-reads/count.json b/test/spec/retryable-reads/legacy/count.json similarity index 100% rename from test/spec/retryable-reads/count.json rename to test/spec/retryable-reads/legacy/count.json diff --git a/test/spec/retryable-reads/count.yml b/test/spec/retryable-reads/legacy/count.yml similarity index 100% rename from test/spec/retryable-reads/count.yml rename to test/spec/retryable-reads/legacy/count.yml diff --git a/test/spec/retryable-reads/countDocuments-serverErrors.json b/test/spec/retryable-reads/legacy/countDocuments-serverErrors.json similarity index 100% rename from test/spec/retryable-reads/countDocuments-serverErrors.json rename to test/spec/retryable-reads/legacy/countDocuments-serverErrors.json diff --git a/test/spec/retryable-reads/countDocuments-serverErrors.yml b/test/spec/retryable-reads/legacy/countDocuments-serverErrors.yml similarity index 100% rename from test/spec/retryable-reads/countDocuments-serverErrors.yml rename to test/spec/retryable-reads/legacy/countDocuments-serverErrors.yml diff --git a/test/spec/retryable-reads/countDocuments.json b/test/spec/retryable-reads/legacy/countDocuments.json similarity index 100% rename from test/spec/retryable-reads/countDocuments.json rename to test/spec/retryable-reads/legacy/countDocuments.json diff --git a/test/spec/retryable-reads/countDocuments.yml b/test/spec/retryable-reads/legacy/countDocuments.yml similarity index 100% rename from test/spec/retryable-reads/countDocuments.yml rename to test/spec/retryable-reads/legacy/countDocuments.yml diff --git a/test/spec/retryable-reads/distinct-serverErrors.json b/test/spec/retryable-reads/legacy/distinct-serverErrors.json similarity index 100% rename from test/spec/retryable-reads/distinct-serverErrors.json rename to test/spec/retryable-reads/legacy/distinct-serverErrors.json diff --git a/test/spec/retryable-reads/distinct-serverErrors.yml b/test/spec/retryable-reads/legacy/distinct-serverErrors.yml similarity index 100% rename from test/spec/retryable-reads/distinct-serverErrors.yml rename to test/spec/retryable-reads/legacy/distinct-serverErrors.yml diff --git a/test/spec/retryable-reads/distinct.json b/test/spec/retryable-reads/legacy/distinct.json similarity index 100% rename from test/spec/retryable-reads/distinct.json rename to test/spec/retryable-reads/legacy/distinct.json diff --git a/test/spec/retryable-reads/distinct.yml b/test/spec/retryable-reads/legacy/distinct.yml similarity index 100% rename from test/spec/retryable-reads/distinct.yml rename to test/spec/retryable-reads/legacy/distinct.yml diff --git a/test/spec/retryable-reads/estimatedDocumentCount-4.9.json b/test/spec/retryable-reads/legacy/estimatedDocumentCount-4.9.json similarity index 100% rename from test/spec/retryable-reads/estimatedDocumentCount-4.9.json rename to test/spec/retryable-reads/legacy/estimatedDocumentCount-4.9.json diff --git a/test/spec/retryable-reads/estimatedDocumentCount-4.9.yml b/test/spec/retryable-reads/legacy/estimatedDocumentCount-4.9.yml similarity index 100% rename from test/spec/retryable-reads/estimatedDocumentCount-4.9.yml rename to test/spec/retryable-reads/legacy/estimatedDocumentCount-4.9.yml diff --git a/test/spec/retryable-reads/estimatedDocumentCount-pre4.9.json b/test/spec/retryable-reads/legacy/estimatedDocumentCount-pre4.9.json similarity index 100% rename from test/spec/retryable-reads/estimatedDocumentCount-pre4.9.json rename to test/spec/retryable-reads/legacy/estimatedDocumentCount-pre4.9.json diff --git a/test/spec/retryable-reads/estimatedDocumentCount-pre4.9.yml b/test/spec/retryable-reads/legacy/estimatedDocumentCount-pre4.9.yml similarity index 100% rename from test/spec/retryable-reads/estimatedDocumentCount-pre4.9.yml rename to test/spec/retryable-reads/legacy/estimatedDocumentCount-pre4.9.yml diff --git a/test/spec/retryable-reads/estimatedDocumentCount-serverErrors-4.9.json b/test/spec/retryable-reads/legacy/estimatedDocumentCount-serverErrors-4.9.json similarity index 100% rename from test/spec/retryable-reads/estimatedDocumentCount-serverErrors-4.9.json rename to test/spec/retryable-reads/legacy/estimatedDocumentCount-serverErrors-4.9.json diff --git a/test/spec/retryable-reads/estimatedDocumentCount-serverErrors-4.9.yml b/test/spec/retryable-reads/legacy/estimatedDocumentCount-serverErrors-4.9.yml similarity index 100% rename from test/spec/retryable-reads/estimatedDocumentCount-serverErrors-4.9.yml rename to test/spec/retryable-reads/legacy/estimatedDocumentCount-serverErrors-4.9.yml diff --git a/test/spec/retryable-reads/estimatedDocumentCount-serverErrors-pre4.9.json b/test/spec/retryable-reads/legacy/estimatedDocumentCount-serverErrors-pre4.9.json similarity index 100% rename from test/spec/retryable-reads/estimatedDocumentCount-serverErrors-pre4.9.json rename to test/spec/retryable-reads/legacy/estimatedDocumentCount-serverErrors-pre4.9.json diff --git a/test/spec/retryable-reads/estimatedDocumentCount-serverErrors-pre4.9.yml b/test/spec/retryable-reads/legacy/estimatedDocumentCount-serverErrors-pre4.9.yml similarity index 100% rename from test/spec/retryable-reads/estimatedDocumentCount-serverErrors-pre4.9.yml rename to test/spec/retryable-reads/legacy/estimatedDocumentCount-serverErrors-pre4.9.yml diff --git a/test/spec/retryable-reads/find-serverErrors.json b/test/spec/retryable-reads/legacy/find-serverErrors.json similarity index 100% rename from test/spec/retryable-reads/find-serverErrors.json rename to test/spec/retryable-reads/legacy/find-serverErrors.json diff --git a/test/spec/retryable-reads/find-serverErrors.yml b/test/spec/retryable-reads/legacy/find-serverErrors.yml similarity index 100% rename from test/spec/retryable-reads/find-serverErrors.yml rename to test/spec/retryable-reads/legacy/find-serverErrors.yml diff --git a/test/spec/retryable-reads/find.json b/test/spec/retryable-reads/legacy/find.json similarity index 100% rename from test/spec/retryable-reads/find.json rename to test/spec/retryable-reads/legacy/find.json diff --git a/test/spec/retryable-reads/find.yml b/test/spec/retryable-reads/legacy/find.yml similarity index 100% rename from test/spec/retryable-reads/find.yml rename to test/spec/retryable-reads/legacy/find.yml diff --git a/test/spec/retryable-reads/findOne-serverErrors.json b/test/spec/retryable-reads/legacy/findOne-serverErrors.json similarity index 100% rename from test/spec/retryable-reads/findOne-serverErrors.json rename to test/spec/retryable-reads/legacy/findOne-serverErrors.json diff --git a/test/spec/retryable-reads/findOne-serverErrors.yml b/test/spec/retryable-reads/legacy/findOne-serverErrors.yml similarity index 100% rename from test/spec/retryable-reads/findOne-serverErrors.yml rename to test/spec/retryable-reads/legacy/findOne-serverErrors.yml diff --git a/test/spec/retryable-reads/findOne.json b/test/spec/retryable-reads/legacy/findOne.json similarity index 100% rename from test/spec/retryable-reads/findOne.json rename to test/spec/retryable-reads/legacy/findOne.json diff --git a/test/spec/retryable-reads/findOne.yml b/test/spec/retryable-reads/legacy/findOne.yml similarity index 100% rename from test/spec/retryable-reads/findOne.yml rename to test/spec/retryable-reads/legacy/findOne.yml diff --git a/test/spec/retryable-reads/gridfs-download-serverErrors.json b/test/spec/retryable-reads/legacy/gridfs-download-serverErrors.json similarity index 100% rename from test/spec/retryable-reads/gridfs-download-serverErrors.json rename to test/spec/retryable-reads/legacy/gridfs-download-serverErrors.json diff --git a/test/spec/retryable-reads/gridfs-download-serverErrors.yml b/test/spec/retryable-reads/legacy/gridfs-download-serverErrors.yml similarity index 100% rename from test/spec/retryable-reads/gridfs-download-serverErrors.yml rename to test/spec/retryable-reads/legacy/gridfs-download-serverErrors.yml diff --git a/test/spec/retryable-reads/gridfs-download.json b/test/spec/retryable-reads/legacy/gridfs-download.json similarity index 100% rename from test/spec/retryable-reads/gridfs-download.json rename to test/spec/retryable-reads/legacy/gridfs-download.json diff --git a/test/spec/retryable-reads/gridfs-download.yml b/test/spec/retryable-reads/legacy/gridfs-download.yml similarity index 100% rename from test/spec/retryable-reads/gridfs-download.yml rename to test/spec/retryable-reads/legacy/gridfs-download.yml diff --git a/test/spec/retryable-reads/gridfs-downloadByName-serverErrors.json b/test/spec/retryable-reads/legacy/gridfs-downloadByName-serverErrors.json similarity index 100% rename from test/spec/retryable-reads/gridfs-downloadByName-serverErrors.json rename to test/spec/retryable-reads/legacy/gridfs-downloadByName-serverErrors.json diff --git a/test/spec/retryable-reads/gridfs-downloadByName-serverErrors.yml b/test/spec/retryable-reads/legacy/gridfs-downloadByName-serverErrors.yml similarity index 100% rename from test/spec/retryable-reads/gridfs-downloadByName-serverErrors.yml rename to test/spec/retryable-reads/legacy/gridfs-downloadByName-serverErrors.yml diff --git a/test/spec/retryable-reads/gridfs-downloadByName.json b/test/spec/retryable-reads/legacy/gridfs-downloadByName.json similarity index 100% rename from test/spec/retryable-reads/gridfs-downloadByName.json rename to test/spec/retryable-reads/legacy/gridfs-downloadByName.json diff --git a/test/spec/retryable-reads/gridfs-downloadByName.yml b/test/spec/retryable-reads/legacy/gridfs-downloadByName.yml similarity index 100% rename from test/spec/retryable-reads/gridfs-downloadByName.yml rename to test/spec/retryable-reads/legacy/gridfs-downloadByName.yml diff --git a/test/spec/retryable-reads/listCollectionNames-serverErrors.json b/test/spec/retryable-reads/legacy/listCollectionNames-serverErrors.json similarity index 100% rename from test/spec/retryable-reads/listCollectionNames-serverErrors.json rename to test/spec/retryable-reads/legacy/listCollectionNames-serverErrors.json diff --git a/test/spec/retryable-reads/listCollectionNames-serverErrors.yml b/test/spec/retryable-reads/legacy/listCollectionNames-serverErrors.yml similarity index 100% rename from test/spec/retryable-reads/listCollectionNames-serverErrors.yml rename to test/spec/retryable-reads/legacy/listCollectionNames-serverErrors.yml diff --git a/test/spec/retryable-reads/listCollectionNames.json b/test/spec/retryable-reads/legacy/listCollectionNames.json similarity index 100% rename from test/spec/retryable-reads/listCollectionNames.json rename to test/spec/retryable-reads/legacy/listCollectionNames.json diff --git a/test/spec/retryable-reads/listCollectionNames.yml b/test/spec/retryable-reads/legacy/listCollectionNames.yml similarity index 100% rename from test/spec/retryable-reads/listCollectionNames.yml rename to test/spec/retryable-reads/legacy/listCollectionNames.yml diff --git a/test/spec/retryable-reads/listCollectionObjects-serverErrors.json b/test/spec/retryable-reads/legacy/listCollectionObjects-serverErrors.json similarity index 100% rename from test/spec/retryable-reads/listCollectionObjects-serverErrors.json rename to test/spec/retryable-reads/legacy/listCollectionObjects-serverErrors.json diff --git a/test/spec/retryable-reads/listCollectionObjects-serverErrors.yml b/test/spec/retryable-reads/legacy/listCollectionObjects-serverErrors.yml similarity index 100% rename from test/spec/retryable-reads/listCollectionObjects-serverErrors.yml rename to test/spec/retryable-reads/legacy/listCollectionObjects-serverErrors.yml diff --git a/test/spec/retryable-reads/listCollectionObjects.json b/test/spec/retryable-reads/legacy/listCollectionObjects.json similarity index 100% rename from test/spec/retryable-reads/listCollectionObjects.json rename to test/spec/retryable-reads/legacy/listCollectionObjects.json diff --git a/test/spec/retryable-reads/listCollectionObjects.yml b/test/spec/retryable-reads/legacy/listCollectionObjects.yml similarity index 100% rename from test/spec/retryable-reads/listCollectionObjects.yml rename to test/spec/retryable-reads/legacy/listCollectionObjects.yml diff --git a/test/spec/retryable-reads/listCollections-serverErrors.json b/test/spec/retryable-reads/legacy/listCollections-serverErrors.json similarity index 100% rename from test/spec/retryable-reads/listCollections-serverErrors.json rename to test/spec/retryable-reads/legacy/listCollections-serverErrors.json diff --git a/test/spec/retryable-reads/listCollections-serverErrors.yml b/test/spec/retryable-reads/legacy/listCollections-serverErrors.yml similarity index 100% rename from test/spec/retryable-reads/listCollections-serverErrors.yml rename to test/spec/retryable-reads/legacy/listCollections-serverErrors.yml diff --git a/test/spec/retryable-reads/listCollections.json b/test/spec/retryable-reads/legacy/listCollections.json similarity index 100% rename from test/spec/retryable-reads/listCollections.json rename to test/spec/retryable-reads/legacy/listCollections.json diff --git a/test/spec/retryable-reads/listCollections.yml b/test/spec/retryable-reads/legacy/listCollections.yml similarity index 100% rename from test/spec/retryable-reads/listCollections.yml rename to test/spec/retryable-reads/legacy/listCollections.yml diff --git a/test/spec/retryable-reads/listDatabaseNames-serverErrors.json b/test/spec/retryable-reads/legacy/listDatabaseNames-serverErrors.json similarity index 100% rename from test/spec/retryable-reads/listDatabaseNames-serverErrors.json rename to test/spec/retryable-reads/legacy/listDatabaseNames-serverErrors.json diff --git a/test/spec/retryable-reads/listDatabaseNames-serverErrors.yml b/test/spec/retryable-reads/legacy/listDatabaseNames-serverErrors.yml similarity index 100% rename from test/spec/retryable-reads/listDatabaseNames-serverErrors.yml rename to test/spec/retryable-reads/legacy/listDatabaseNames-serverErrors.yml diff --git a/test/spec/retryable-reads/listDatabaseNames.json b/test/spec/retryable-reads/legacy/listDatabaseNames.json similarity index 100% rename from test/spec/retryable-reads/listDatabaseNames.json rename to test/spec/retryable-reads/legacy/listDatabaseNames.json diff --git a/test/spec/retryable-reads/listDatabaseNames.yml b/test/spec/retryable-reads/legacy/listDatabaseNames.yml similarity index 100% rename from test/spec/retryable-reads/listDatabaseNames.yml rename to test/spec/retryable-reads/legacy/listDatabaseNames.yml diff --git a/test/spec/retryable-reads/listDatabaseObjects-serverErrors.json b/test/spec/retryable-reads/legacy/listDatabaseObjects-serverErrors.json similarity index 100% rename from test/spec/retryable-reads/listDatabaseObjects-serverErrors.json rename to test/spec/retryable-reads/legacy/listDatabaseObjects-serverErrors.json diff --git a/test/spec/retryable-reads/listDatabaseObjects-serverErrors.yml b/test/spec/retryable-reads/legacy/listDatabaseObjects-serverErrors.yml similarity index 100% rename from test/spec/retryable-reads/listDatabaseObjects-serverErrors.yml rename to test/spec/retryable-reads/legacy/listDatabaseObjects-serverErrors.yml diff --git a/test/spec/retryable-reads/listDatabaseObjects.json b/test/spec/retryable-reads/legacy/listDatabaseObjects.json similarity index 100% rename from test/spec/retryable-reads/listDatabaseObjects.json rename to test/spec/retryable-reads/legacy/listDatabaseObjects.json diff --git a/test/spec/retryable-reads/listDatabaseObjects.yml b/test/spec/retryable-reads/legacy/listDatabaseObjects.yml similarity index 100% rename from test/spec/retryable-reads/listDatabaseObjects.yml rename to test/spec/retryable-reads/legacy/listDatabaseObjects.yml diff --git a/test/spec/retryable-reads/listDatabases-serverErrors.json b/test/spec/retryable-reads/legacy/listDatabases-serverErrors.json similarity index 100% rename from test/spec/retryable-reads/listDatabases-serverErrors.json rename to test/spec/retryable-reads/legacy/listDatabases-serverErrors.json diff --git a/test/spec/retryable-reads/listDatabases-serverErrors.yml b/test/spec/retryable-reads/legacy/listDatabases-serverErrors.yml similarity index 100% rename from test/spec/retryable-reads/listDatabases-serverErrors.yml rename to test/spec/retryable-reads/legacy/listDatabases-serverErrors.yml diff --git a/test/spec/retryable-reads/listDatabases.json b/test/spec/retryable-reads/legacy/listDatabases.json similarity index 100% rename from test/spec/retryable-reads/listDatabases.json rename to test/spec/retryable-reads/legacy/listDatabases.json diff --git a/test/spec/retryable-reads/listDatabases.yml b/test/spec/retryable-reads/legacy/listDatabases.yml similarity index 100% rename from test/spec/retryable-reads/listDatabases.yml rename to test/spec/retryable-reads/legacy/listDatabases.yml diff --git a/test/spec/retryable-reads/listIndexNames-serverErrors.json b/test/spec/retryable-reads/legacy/listIndexNames-serverErrors.json similarity index 100% rename from test/spec/retryable-reads/listIndexNames-serverErrors.json rename to test/spec/retryable-reads/legacy/listIndexNames-serverErrors.json diff --git a/test/spec/retryable-reads/listIndexNames-serverErrors.yml b/test/spec/retryable-reads/legacy/listIndexNames-serverErrors.yml similarity index 100% rename from test/spec/retryable-reads/listIndexNames-serverErrors.yml rename to test/spec/retryable-reads/legacy/listIndexNames-serverErrors.yml diff --git a/test/spec/retryable-reads/listIndexNames.json b/test/spec/retryable-reads/legacy/listIndexNames.json similarity index 100% rename from test/spec/retryable-reads/listIndexNames.json rename to test/spec/retryable-reads/legacy/listIndexNames.json diff --git a/test/spec/retryable-reads/listIndexNames.yml b/test/spec/retryable-reads/legacy/listIndexNames.yml similarity index 100% rename from test/spec/retryable-reads/listIndexNames.yml rename to test/spec/retryable-reads/legacy/listIndexNames.yml diff --git a/test/spec/retryable-reads/listIndexes-serverErrors.json b/test/spec/retryable-reads/legacy/listIndexes-serverErrors.json similarity index 100% rename from test/spec/retryable-reads/listIndexes-serverErrors.json rename to test/spec/retryable-reads/legacy/listIndexes-serverErrors.json diff --git a/test/spec/retryable-reads/listIndexes-serverErrors.yml b/test/spec/retryable-reads/legacy/listIndexes-serverErrors.yml similarity index 100% rename from test/spec/retryable-reads/listIndexes-serverErrors.yml rename to test/spec/retryable-reads/legacy/listIndexes-serverErrors.yml diff --git a/test/spec/retryable-reads/listIndexes.json b/test/spec/retryable-reads/legacy/listIndexes.json similarity index 100% rename from test/spec/retryable-reads/listIndexes.json rename to test/spec/retryable-reads/legacy/listIndexes.json diff --git a/test/spec/retryable-reads/listIndexes.yml b/test/spec/retryable-reads/legacy/listIndexes.yml similarity index 100% rename from test/spec/retryable-reads/listIndexes.yml rename to test/spec/retryable-reads/legacy/listIndexes.yml diff --git a/test/spec/retryable-reads/mapReduce.json b/test/spec/retryable-reads/legacy/mapReduce.json similarity index 100% rename from test/spec/retryable-reads/mapReduce.json rename to test/spec/retryable-reads/legacy/mapReduce.json diff --git a/test/spec/retryable-reads/mapReduce.yml b/test/spec/retryable-reads/legacy/mapReduce.yml similarity index 100% rename from test/spec/retryable-reads/mapReduce.yml rename to test/spec/retryable-reads/legacy/mapReduce.yml diff --git a/test/spec/retryable-reads/unified/handshakeError.json b/test/spec/retryable-reads/unified/handshakeError.json new file mode 100644 index 0000000000..2cf1d173f8 --- /dev/null +++ b/test/spec/retryable-reads/unified/handshakeError.json @@ -0,0 +1,257 @@ +{ + "description": "retryable reads handshake failures", + "schemaVersion": "1.3", + "runOnRequirements": [ + { + "minServerVersion": "4.2", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ], + "auth": true + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent", + "connectionCheckOutStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "retryable-handshake-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll" + } + } + ], + "initialData": [ + { + "collectionName": "coll", + "databaseName": "retryable-handshake-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "find succeeds after retryable handshake network error", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "saslContinue", + "ping" + ], + "closeConnection": true + } + } + } + }, + { + "name": "runCommand", + "object": "database0", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + } + }, + "expectError": { + "isError": true + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "_id": 2 + } + }, + "expectResult": [ + { + "_id": 2, + "x": 22 + } + ] + } + ], + "expectEvents": [ + { + "client": "client0", + "eventType": "cmap", + "events": [ + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + } + ] + }, + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "ping": 1 + }, + "databaseName": "retryable-handshake-tests" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "coll", + "filter": { + "_id": 2 + } + }, + "databaseName": "retryable-handshake-tests" + } + } + ] + } + ] + }, + { + "description": "find succeeds after retryable handshake network error (ShutdownInProgress)", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "saslContinue", + "ping" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "runCommand", + "object": "database0", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + } + }, + "expectError": { + "isError": true + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "_id": 2 + } + }, + "expectResult": [ + { + "_id": 2, + "x": 22 + } + ] + } + ], + "expectEvents": [ + { + "client": "client0", + "eventType": "cmap", + "events": [ + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + } + ] + }, + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "ping": 1 + }, + "databaseName": "retryable-handshake-tests" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "coll", + "filter": { + "_id": 2 + } + }, + "databaseName": "retryable-handshake-tests" + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/retryable-reads/unified/handshakeError.yml b/test/spec/retryable-reads/unified/handshakeError.yml new file mode 100644 index 0000000000..90e6947dd1 --- /dev/null +++ b/test/spec/retryable-reads/unified/handshakeError.yml @@ -0,0 +1,129 @@ +description: "retryable reads handshake failures" + +schemaVersion: "1.3" + +runOnRequirements: + - minServerVersion: "4.2" + topologies: [replicaset, sharded, load-balanced] + auth: true + +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeEvents: [commandStartedEvent, connectionCheckOutStartedEvent] + - database: + id: &database0 database0 + client: *client0 + databaseName: &databaseName retryable-handshake-tests + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collectionName coll + +initialData: + - collectionName: *collectionName + databaseName: *databaseName + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + +tests: + - description: "find succeeds after retryable handshake network error" + operations: + - name: failPoint # fail the next connection establishment + object: testRunner + arguments: + client: *client0 + failPoint: + configureFailPoint: failCommand + mode: { times: 2 } + data: + # use saslContinue here to avoid SDAM errors + # this failPoint itself will create a usable connection in the connection pool + # so we run a ping (that also fails) in order to discard the connection + # before testing our read operation "find" + failCommands: [saslContinue, ping] + closeConnection: true + + - name: runCommand + object: *database0 + arguments: + commandName: ping + command: { ping: 1 } + expectError: + isError: true + + - name: find + object: *collection0 + arguments: + filter: { _id: 2 } + expectResult: [{ _id: 2, x: 22 }] + + expectEvents: + - client: *client0 + eventType: cmap + events: + - { connectionCheckOutStartedEvent: {} } + - { connectionCheckOutStartedEvent: {} } + - { connectionCheckOutStartedEvent: {} } + - { connectionCheckOutStartedEvent: {} } + - client: *client0 + events: + - commandStartedEvent: + command: + ping: 1 + databaseName: *databaseName + - commandStartedEvent: + command: + find: *collectionName + filter: { _id: 2 } + databaseName: *databaseName + + - description: "find succeeds after retryable handshake network error (ShutdownInProgress)" + operations: + - name: failPoint # fail the next connection establishment + object: testRunner + arguments: + client: *client0 + failPoint: + configureFailPoint: failCommand + mode: { times: 2 } + data: + failCommands: [saslContinue, ping] + errorCode: 91 # ShutdownInProgress + + - name: runCommand + object: *database0 + arguments: + commandName: ping + command: { ping: 1 } + expectError: + isError: true + + - name: find + object: *collection0 + arguments: + filter: { _id: 2 } + expectResult: [{ _id: 2, x: 22 }] + + expectEvents: + - client: *client0 + eventType: cmap + events: + - { connectionCheckOutStartedEvent: {} } + - { connectionCheckOutStartedEvent: {} } + - { connectionCheckOutStartedEvent: {} } + - { connectionCheckOutStartedEvent: {} } + - client: *client0 + events: + - commandStartedEvent: + command: + ping: 1 + databaseName: *databaseName + - commandStartedEvent: + command: + find: *collectionName + filter: { _id: 2 } + databaseName: *databaseName diff --git a/test/spec/retryable-writes/legacy/README.rst b/test/spec/retryable-writes/README.rst similarity index 81% rename from test/spec/retryable-writes/legacy/README.rst rename to test/spec/retryable-writes/README.rst index 88d8e52fc8..7cc234d51c 100644 --- a/test/spec/retryable-writes/legacy/README.rst +++ b/test/spec/retryable-writes/README.rst @@ -9,8 +9,13 @@ Retryable Write Tests Introduction ============ -The YAML and JSON files in this directory tree are platform-independent tests -that drivers can use to prove their conformance to the Retryable Writes spec. +Tests in this directory are platform-independent tests that drivers can use to +prove their conformance to the Retryable Writes specification. + +Tests in the ``unified`` directory are implemented in the +`Unified Test Format <../../unified-test-format/unified-test-format.rst>`__. + +Tests in the ``legacy`` directory should be executed as described below. Several prose tests, which are not easily expressed in YAML, are also presented in this file. Those tests will need to be manually implemented by each driver. @@ -164,6 +169,18 @@ Each YAML file has the following keys: the default is all topologies (i.e. ``["single", "replicaset", "sharded", "load-balanced"]``). + - ``serverless``: Optional string. Whether or not the test should be run on + serverless instances imitating sharded clusters. Valid values are "require", + "forbid", and "allow". If "require", the test MUST only be run on serverless + instances. If "forbid", the test MUST NOT be run on serverless instances. If + omitted or "allow", this option has no effect. + + The test runner MUST be informed whether or not serverless is being used in + order to determine if this requirement is met (e.g. through an environment + variable or configuration option). Since the serverless proxy imitates a + mongos, the runner is not capable of determining this by issuing a server + command such as ``buildInfo`` or ``hello``. + - ``data``: The data that should exist in the collection under test before each test run. @@ -174,10 +191,18 @@ Each YAML file has the following keys: - ``clientOptions``: Parameters to pass to MongoClient(). - - ``useMultipleMongoses`` (optional): If ``true``, the MongoClient for this - test should be initialized with multiple mongos seed addresses. If ``false`` - or omitted, only a single mongos address should be specified. This field has - no effect for non-sharded topologies. + - ``useMultipleMongoses`` (optional): If ``true``, and the topology type is + ``Sharded``, the MongoClient for this test should be initialized with multiple + mongos seed addresses. If ``false`` or omitted, only a single mongos address + should be specified. + + If ``true``, and the topology type is ``LoadBalanced``, the MongoClient for + this test should be initialized with the URI of the load balancer fronting + multiple servers. If ``false`` or omitted, the MongoClient for this test + should be initialized with the URI of the load balancer fronting a single + server. + + ``useMultipleMongoses`` only affects ``Sharded`` and ``LoadBalanced`` topologies. - ``failPoint`` (optional): The ``configureFailPoint`` command document to run to configure a fail point on the primary server. Drivers must ensure that @@ -328,12 +353,58 @@ and sharded clusters. in use MAY skip this test for sharded clusters, since ``mongos`` does not report this information in its ``serverStatus`` response. +#. Test that drivers properly retry after encountering PoolClearedErrors. This + test MUST be implemented by any driver that implements the CMAP + specification. This test requires MongoDB 4.2.9+ for ``blockConnection`` support in the failpoint. + + 1. Create a client with maxPoolSize=1 and retryWrites=true. If testing + against a sharded deployment, be sure to connect to only a single mongos. + + 2. Enable the following failpoint:: + + { + configureFailPoint: "failCommand", + mode: { times: 1 }, + data: { + failCommands: ["insert"], + errorCode: 91, + blockConnection: true, + blockTimeMS: 1000, + errorLabels: ["RetryableWriteError"] + } + } + + 3. Start two threads and attempt to perform an ``insertOne`` simultaneously on both. + + 4. Verify that both ``insertOne`` attempts succeed. + + 5. Via CMAP monitoring, assert that the first check out succeeds. + + 6. Via CMAP monitoring, assert that a PoolClearedEvent is then emitted. + + 7. Via CMAP monitoring, assert that the second check out then fails due to a + connection error. + + 8. Via Command Monitoring, assert that exactly three ``insert`` + CommandStartedEvents were observed in total. + + 9. Disable the failpoint. + + Changelog ========= + + +:2021-08-27: Add ``serverless`` to ``runOn``. Clarify behavior of + ``useMultipleMongoses`` for ``LoadBalanced`` topologies. + :2021-04-23: Add ``load-balanced`` to test topology requirements. -:2019-10-21: Add ``errorLabelsContain`` and ``errorLabelsContain`` fields to ``result`` +:2021-03-24: Add prose test verifying ``PoolClearedErrors`` are retried. + +:2019-10-21: Add ``errorLabelsContain`` and ``errorLabelsContain`` fields to + ``result`` :2019-08-07: Add Prose Tests section diff --git a/test/spec/retryable-writes/unified/.gitkeep b/test/spec/retryable-writes/unified/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/spec/retryable-writes/unified/bulkWrite-serverErrors.json b/test/spec/retryable-writes/unified/bulkWrite-serverErrors.json new file mode 100644 index 0000000000..23cf2869a6 --- /dev/null +++ b/test/spec/retryable-writes/unified/bulkWrite-serverErrors.json @@ -0,0 +1,205 @@ +{ + "description": "retryable-writes bulkWrite serverErrors", + "schemaVersion": "1.0", + "runOnRequirements": [ + { + "minServerVersion": "3.6", + "topologies": [ + "replicaset" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "retryable-writes-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll" + } + } + ], + "initialData": [ + { + "collectionName": "coll", + "databaseName": "retryable-writes-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + ], + "tests": [ + { + "description": "BulkWrite succeeds after retryable writeConcernError in first batch", + "runOnRequirements": [ + { + "minServerVersion": "4.0", + "topologies": [ + "replicaset" + ] + }, + { + "minServerVersion": "4.1.7", + "topologies": [ + "sharded-replicaset" + ] + } + ], + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorLabels": [ + "RetryableWriteError" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + } + } + }, + { + "name": "bulkWrite", + "object": "collection0", + "arguments": { + "requests": [ + { + "insertOne": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + { + "deleteOne": { + "filter": { + "_id": 2 + } + } + } + ] + }, + "expectResult": { + "deletedCount": 1, + "insertedCount": 1, + "matchedCount": 0, + "modifiedCount": 0, + "upsertedCount": 0, + "insertedIds": { + "$$unsetOrMatches": { + "0": 3 + } + }, + "upsertedIds": {} + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "coll", + "documents": [ + { + "_id": 3, + "x": 33 + } + ] + }, + "commandName": "insert", + "databaseName": "retryable-writes-tests" + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "coll", + "documents": [ + { + "_id": 3, + "x": 33 + } + ] + }, + "commandName": "insert", + "databaseName": "retryable-writes-tests" + } + }, + { + "commandStartedEvent": { + "command": { + "delete": "coll", + "deletes": [ + { + "q": { + "_id": 2 + }, + "limit": 1 + } + ] + }, + "commandName": "delete", + "databaseName": "retryable-writes-tests" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll", + "databaseName": "retryable-writes-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + } + ] +} diff --git a/test/spec/retryable-writes/unified/bulkWrite-serverErrors.yml b/test/spec/retryable-writes/unified/bulkWrite-serverErrors.yml new file mode 100644 index 0000000000..cb67304c74 --- /dev/null +++ b/test/spec/retryable-writes/unified/bulkWrite-serverErrors.yml @@ -0,0 +1,96 @@ +description: "retryable-writes bulkWrite serverErrors" + +schemaVersion: "1.0" + +runOnRequirements: + - minServerVersion: "3.6" + topologies: [ replicaset ] + +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeEvents: [ commandStartedEvent ] + - database: + id: &database0 database0 + client: *client0 + databaseName: &databaseName retryable-writes-tests + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collectionName coll + +initialData: + - collectionName: *collectionName + databaseName: *databaseName + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + +tests: + - description: "BulkWrite succeeds after retryable writeConcernError in first batch" + runOnRequirements: + - minServerVersion: "4.0" + topologies: [ replicaset ] + - minServerVersion: "4.1.7" + topologies: [ sharded-replicaset ] + operations: + - name: failPoint + object: testRunner + arguments: + client: *client0 + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: [ insert ] + errorLabels: [RetryableWriteError] # top-level error labels + writeConcernError: + code: 91 # ShutdownInProgress + errmsg: "Replication is being shut down" + - name: bulkWrite + object: *collection0 + arguments: + requests: + - insertOne: + document: { _id: 3, x: 33 } + - deleteOne: + filter: { _id: 2 } + expectResult: + deletedCount: 1 + insertedCount: 1 + matchedCount: 0 + modifiedCount: 0 + upsertedCount: 0 + insertedIds: { $$unsetOrMatches: { 0: 3 } } + upsertedIds: { } + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + insert: *collectionName + documents: [{ _id: 3, x: 33 }] + commandName: insert + databaseName: *databaseName + - commandStartedEvent: + command: + insert: *collectionName + documents: [{ _id: 3, x: 33 }] + commandName: insert + databaseName: *databaseName + - commandStartedEvent: + command: + delete: *collectionName + deletes: + - + q: { _id: 2 } + limit: 1 + commandName: delete + databaseName: *databaseName + outcome: + - collectionName: *collectionName + databaseName: *databaseName + documents: + - { _id: 1, x: 11 } + - { _id: 3, x: 33 } # The write was still applied diff --git a/test/spec/retryable-writes/unified/handshakeError.json b/test/spec/retryable-writes/unified/handshakeError.json new file mode 100644 index 0000000000..6d6b4ac491 --- /dev/null +++ b/test/spec/retryable-writes/unified/handshakeError.json @@ -0,0 +1,279 @@ +{ + "description": "retryable writes handshake failures", + "schemaVersion": "1.3", + "runOnRequirements": [ + { + "minServerVersion": "4.2", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ], + "auth": true + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent", + "connectionCheckOutStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "retryable-handshake-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll" + } + } + ], + "initialData": [ + { + "collectionName": "coll", + "databaseName": "retryable-handshake-tests", + "documents": [ + { + "_id": 1, + "x": 11 + } + ] + } + ], + "tests": [ + { + "description": "InsertOne succeeds after retryable handshake error", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "saslContinue", + "ping" + ], + "closeConnection": true + } + } + } + }, + { + "name": "runCommand", + "object": "database0", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + } + }, + "expectError": { + "isError": true + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 2, + "x": 22 + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "eventType": "cmap", + "events": [ + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + } + ] + }, + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "ping": 1 + }, + "databaseName": "retryable-handshake-tests" + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "coll", + "documents": [ + { + "_id": 2, + "x": 22 + } + ] + }, + "commandName": "insert", + "databaseName": "retryable-handshake-tests" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll", + "databaseName": "retryable-handshake-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + ] + }, + { + "description": "InsertOne succeeds after retryable handshake error ShutdownInProgress", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "saslContinue", + "ping" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "runCommand", + "object": "database0", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + } + }, + "expectError": { + "isError": true + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 2, + "x": 22 + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "eventType": "cmap", + "events": [ + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + } + ] + }, + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "ping": 1 + }, + "databaseName": "retryable-handshake-tests" + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "coll", + "documents": [ + { + "_id": 2, + "x": 22 + } + ] + }, + "commandName": "insert", + "databaseName": "retryable-handshake-tests" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll", + "databaseName": "retryable-handshake-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + ] + } + ] +} diff --git a/test/spec/retryable-writes/unified/handshakeError.yml b/test/spec/retryable-writes/unified/handshakeError.yml new file mode 100644 index 0000000000..bac3a7dba3 --- /dev/null +++ b/test/spec/retryable-writes/unified/handshakeError.yml @@ -0,0 +1,137 @@ +description: "retryable writes handshake failures" + +schemaVersion: "1.3" + +runOnRequirements: + - minServerVersion: "4.2" + topologies: [replicaset, sharded, load-balanced] + auth: true + +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeEvents: [commandStartedEvent, connectionCheckOutStartedEvent] + - database: + id: &database0 database0 + client: *client0 + databaseName: &databaseName retryable-handshake-tests + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collectionName coll + +initialData: + - collectionName: *collectionName + databaseName: *databaseName + documents: + - { _id: 1, x: 11 } + +tests: + - description: "InsertOne succeeds after retryable handshake error" + operations: + - name: failPoint # fail the next connection establishment + object: testRunner + arguments: + client: *client0 + failPoint: + configureFailPoint: failCommand + mode: { times: 2 } + data: + failCommands: [saslContinue, ping] + closeConnection: true + + - name: runCommand + object: *database0 + arguments: + commandName: ping + command: { ping: 1 } + expectError: + isError: true + + - name: insertOne + object: *collection0 + arguments: + document: { _id: 2, x: 22 } + + expectEvents: + - client: *client0 + eventType: cmap + events: + - { connectionCheckOutStartedEvent: {} } + - { connectionCheckOutStartedEvent: {} } + - { connectionCheckOutStartedEvent: {} } + - { connectionCheckOutStartedEvent: {} } + - client: *client0 + events: + - commandStartedEvent: + command: + ping: 1 + databaseName: *databaseName + - commandStartedEvent: + command: + insert: *collectionName + documents: [{ _id: 2, x: 22 }] + commandName: insert + databaseName: *databaseName + + outcome: + - collectionName: *collectionName + databaseName: *databaseName + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } # The write was still applied + + - description: "InsertOne succeeds after retryable handshake error ShutdownInProgress" + operations: + - name: failPoint # fail the next connection establishment + object: testRunner + arguments: + client: *client0 + failPoint: + configureFailPoint: failCommand + mode: { times: 2 } + data: + failCommands: [saslContinue, ping] + errorCode: 91 # ShutdownInProgress + + - name: runCommand + object: *database0 + arguments: + commandName: ping + command: { ping: 1 } + expectError: + isError: true + + - name: insertOne + object: *collection0 + arguments: + document: { _id: 2, x: 22 } + + expectEvents: + - client: *client0 + eventType: cmap + events: + - { connectionCheckOutStartedEvent: {} } + - { connectionCheckOutStartedEvent: {} } + - { connectionCheckOutStartedEvent: {} } + - { connectionCheckOutStartedEvent: {} } + - client: *client0 + events: + - commandStartedEvent: + command: + ping: 1 + databaseName: *databaseName + - commandStartedEvent: + command: + insert: *collectionName + documents: [{ _id: 2, x: 22 }] + commandName: insert + databaseName: *databaseName + + outcome: + - collectionName: *collectionName + databaseName: *databaseName + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } # The write was still applied diff --git a/test/spec/retryable-writes/unified/insertOne-serverErrors.json b/test/spec/retryable-writes/unified/insertOne-serverErrors.json new file mode 100644 index 0000000000..77245a8197 --- /dev/null +++ b/test/spec/retryable-writes/unified/insertOne-serverErrors.json @@ -0,0 +1,173 @@ +{ + "description": "retryable-writes insertOne serverErrors", + "schemaVersion": "1.0", + "runOnRequirements": [ + { + "minServerVersion": "3.6", + "topologies": [ + "replicaset" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "retryable-writes-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll" + } + } + ], + "initialData": [ + { + "collectionName": "coll", + "databaseName": "retryable-writes-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + ], + "tests": [ + { + "description": "InsertOne succeeds after retryable writeConcernError", + "runOnRequirements": [ + { + "minServerVersion": "4.0", + "topologies": [ + "replicaset" + ] + }, + { + "minServerVersion": "4.1.7", + "topologies": [ + "sharded-replicaset" + ] + } + ], + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorLabels": [ + "RetryableWriteError" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + } + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + }, + "expectResult": { + "$$unsetOrMatches": { + "insertedId": { + "$$unsetOrMatches": 3 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "coll", + "documents": [ + { + "_id": 3, + "x": 33 + } + ] + }, + "commandName": "insert", + "databaseName": "retryable-writes-tests" + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "coll", + "documents": [ + { + "_id": 3, + "x": 33 + } + ] + }, + "commandName": "insert", + "databaseName": "retryable-writes-tests" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll", + "databaseName": "retryable-writes-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + } + ] +} diff --git a/test/spec/retryable-writes/unified/insertOne-serverErrors.yml b/test/spec/retryable-writes/unified/insertOne-serverErrors.yml new file mode 100644 index 0000000000..3d9672cdca --- /dev/null +++ b/test/spec/retryable-writes/unified/insertOne-serverErrors.yml @@ -0,0 +1,78 @@ +description: "retryable-writes insertOne serverErrors" + +schemaVersion: "1.0" + +runOnRequirements: + - minServerVersion: "3.6" + topologies: [ replicaset ] + +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeEvents: [ commandStartedEvent ] + - database: + id: &database0 database0 + client: *client0 + databaseName: &databaseName retryable-writes-tests + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collectionName coll + +initialData: + - collectionName: *collectionName + databaseName: *databaseName + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + +tests: + - description: "InsertOne succeeds after retryable writeConcernError" + runOnRequirements: + - minServerVersion: "4.0" + topologies: [ replicaset ] + - minServerVersion: "4.1.7" + topologies: [ sharded-replicaset ] + operations: + - name: failPoint + object: testRunner + arguments: + client: *client0 + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: [ insert ] + errorLabels: [RetryableWriteError] # top-level error labels + writeConcernError: + code: 91 # ShutdownInProgress + errmsg: "Replication is being shut down" + - name: insertOne + object: *collection0 + arguments: + document: { _id: 3, x: 33 } + expectResult: + $$unsetOrMatches: { insertedId: { $$unsetOrMatches: 3 } } + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + insert: *collectionName + documents: [{ _id: 3, x: 33 }] + commandName: insert + databaseName: *databaseName + - commandStartedEvent: + command: + insert: *collectionName + documents: [{ _id: 3, x: 33 }] + commandName: insert + databaseName: *databaseName + outcome: + - collectionName: *collectionName + databaseName: *databaseName + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } # The write was still applied diff --git a/test/spec/transactions/README.rst b/test/spec/transactions/README.rst index c3c89a0744..a1b27dcf01 100644 --- a/test/spec/transactions/README.rst +++ b/test/spec/transactions/README.rst @@ -81,6 +81,11 @@ control the fail point's behavior. ``failCommand`` supports the following blocked for. Required when blockConnection is true. `New in mongod 4.3.4 `_. +Speeding Up Tests +================= + +See `Speeding Up Tests <../../retryable-reads/tests/README.rst#speeding-up-tests>`_ in the retryable reads spec tests. + Test Format =========== @@ -107,6 +112,18 @@ Each YAML file has the following keys: and "sharded". If this field is omitted, the default is all topologies (i.e. ``["single", "replicaset", "sharded"]``). + - ``serverless``: Optional string. Whether or not the test should be run on + serverless instances imitating sharded clusters. Valid values are "require", + "forbid", and "allow". If "require", the test MUST only be run on serverless + instances. If "forbid", the test MUST NOT be run on serverless instances. If + omitted or "allow", this option has no effect. + + The test runner MUST be informed whether or not serverless is being used in + order to determine if this requirement is met (e.g. through an environment + variable or configuration option). Since the serverless proxy imitates a + mongos, the runner is not capable of determining this by issuing a server + command such as ``buildInfo`` or ``hello``. + - ``database_name`` and ``collection_name``: The database and collection to use for testing. diff --git a/test/spec/transactions/unified/mongos-unpin.json b/test/spec/transactions/unified/mongos-unpin.json index c01abf3076..4f7ae43794 100644 --- a/test/spec/transactions/unified/mongos-unpin.json +++ b/test/spec/transactions/unified/mongos-unpin.json @@ -49,7 +49,7 @@ }, "tests": [ { - "description": "unpin after TransientTransctionError error on commit", + "description": "unpin after TransientTransactionError error on commit", "runOnRequirements": [ { "serverless": "forbid" @@ -108,6 +108,24 @@ "arguments": { "session": "session0" } + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "x": 1 + }, + "session": "session0" + } + }, + { + "name": "abortTransaction", + "object": "session0" } ] }, @@ -142,7 +160,7 @@ ] }, { - "description": "unpin after TransientTransctionError error on abort", + "description": "unpin after non-transient error on abort", "runOnRequirements": [ { "serverless": "forbid" @@ -192,6 +210,91 @@ "arguments": { "session": "session0" } + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "x": 1 + }, + "session": "session0" + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ] + }, + { + "description": "unpin after TransientTransactionError error on abort", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "x": 1 + }, + "session": "session0" + } + }, + { + "name": "targetedFailPoint", + "object": "testRunner", + "arguments": { + "session": "session0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "abortTransaction", + "object": "session0" + }, + { + "name": "assertSessionUnpinned", + "object": "testRunner", + "arguments": { + "session": "session0" + } + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "x": 1 + }, + "session": "session0" + } + }, + { + "name": "abortTransaction", + "object": "session0" } ] }, diff --git a/test/spec/transactions/unified/mongos-unpin.yml b/test/spec/transactions/unified/mongos-unpin.yml index 46433d8c45..c13798acaa 100644 --- a/test/spec/transactions/unified/mongos-unpin.yml +++ b/test/spec/transactions/unified/mongos-unpin.yml @@ -33,7 +33,7 @@ _yamlAnchors: &lockTimeoutErrorCode 24 tests: - - description: unpin after TransientTransctionError error on commit + - description: unpin after TransientTransactionError error on commit runOnRequirements: # serverless proxy doesn't append error labels to errors in transactions # caused by failpoints (CLOUDP-88216) @@ -70,17 +70,22 @@ tests: object: testRunner arguments: session: *session0 - - - description: unpin on successful abort - operations: + # Cleanup the potentionally open server transaction by starting and + # aborting a new transaction on the same session. - *startTransaction - *insertOne - &abortTransaction name: abortTransaction object: *session0 + + - description: unpin on successful abort + operations: + - *startTransaction + - *insertOne + - *abortTransaction - *assertNoPinnedServer - - description: unpin after TransientTransctionError error on abort + - description: unpin after non-transient error on abort runOnRequirements: # serverless proxy doesn't append error labels to errors in transactions # caused by failpoints (CLOUDP-88216) @@ -100,6 +105,33 @@ tests: errorCode: *lockTimeoutErrorCode - *abortTransaction - *assertNoPinnedServer + # Cleanup the potentionally open server transaction by starting and + # aborting a new transaction on the same session. + - *startTransaction + - *insertOne + - *abortTransaction + + - description: unpin after TransientTransactionError error on abort + operations: + - *startTransaction + - *insertOne + - name: targetedFailPoint + object: testRunner + arguments: + session: *session0 + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: [ abortTransaction ] + errorCode: 91 # ShutdownInProgress + - *abortTransaction + - *assertNoPinnedServer + # Cleanup the potentionally open server transaction by starting and + # aborting a new transaction on the same session. + - *startTransaction + - *insertOne + - *abortTransaction - description: unpin when a new transaction is started operations: diff --git a/test/spec/transactions/unified/retryable-abort-handshake.json b/test/spec/transactions/unified/retryable-abort-handshake.json new file mode 100644 index 0000000000..4ad56e2f2f --- /dev/null +++ b/test/spec/transactions/unified/retryable-abort-handshake.json @@ -0,0 +1,204 @@ +{ + "description": "retryable abortTransaction on handshake errors", + "schemaVersion": "1.4", + "runOnRequirements": [ + { + "minServerVersion": "4.2", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ], + "serverless": "forbid", + "auth": true + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent", + "connectionCheckOutStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "retryable-handshake-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll" + } + }, + { + "session": { + "id": "session0", + "client": "client0" + } + }, + { + "session": { + "id": "session1", + "client": "client0" + } + } + ], + "initialData": [ + { + "collectionName": "coll", + "databaseName": "retryable-handshake-tests", + "documents": [ + { + "_id": 1, + "x": 11 + } + ] + } + ], + "tests": [ + { + "description": "AbortTransaction succeeds after handshake network error", + "skipReason": "DRIVERS-2032: Pinned servers need to be checked if they are still selectable", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "session": "session0", + "document": { + "_id": 2, + "x": 22 + } + } + }, + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client0", + "session": "session1", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "saslContinue", + "ping" + ], + "closeConnection": true + } + } + } + }, + { + "name": "runCommand", + "object": "database0", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + }, + "session": "session1" + }, + "expectError": { + "isError": true + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectEvents": [ + { + "client": "client0", + "eventType": "cmap", + "events": [ + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + } + ] + }, + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "coll", + "documents": [ + { + "_id": 2, + "x": 22 + } + ], + "startTransaction": true + }, + "commandName": "insert", + "databaseName": "retryable-handshake-tests" + } + }, + { + "commandStartedEvent": { + "command": { + "ping": 1 + }, + "databaseName": "retryable-handshake-tests" + } + }, + { + "commandStartedEvent": { + "command": { + "abortTransaction": 1, + "lsid": { + "$$sessionLsid": "session0" + } + }, + "commandName": "abortTransaction", + "databaseName": "admin" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll", + "databaseName": "retryable-handshake-tests", + "documents": [ + { + "_id": 1, + "x": 11 + } + ] + } + ] + } + ] +} diff --git a/test/spec/transactions/unified/retryable-abort-handshake.yml b/test/spec/transactions/unified/retryable-abort-handshake.yml new file mode 100644 index 0000000000..d0e9ec86b4 --- /dev/null +++ b/test/spec/transactions/unified/retryable-abort-handshake.yml @@ -0,0 +1,118 @@ +description: "retryable abortTransaction on handshake errors" + +schemaVersion: "1.4" + +runOnRequirements: + - minServerVersion: "4.2" + topologies: [replicaset, sharded, load-balanced] + serverless: "forbid" + auth: true + +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeEvents: [commandStartedEvent, connectionCheckOutStartedEvent] + - database: + id: &database0 database0 + client: *client0 + databaseName: &databaseName retryable-handshake-tests + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collectionName coll + - session: + # This session will be used to execute the transaction + id: &session0 session0 + client: *client0 + - session: + # This session will be used to create the failPoint, and empty the pool + id: &session1 session1 + client: *client0 + +initialData: + - collectionName: *collectionName + databaseName: *databaseName + documents: + - { _id: 1, x: 11 } + +tests: + - description: "AbortTransaction succeeds after handshake network error" + skipReason: "DRIVERS-2032: Pinned servers need to be checked if they are still selectable" + operations: + + - name: startTransaction + object: *session0 + + - name: insertOne + object: *collection0 + arguments: + session: *session0 + document: { _id: 2, x: 22 } + + # The following failPoint and ping utilize session1 so that + # the transaction won't be failed by the intentional erroring of ping + # and it will have an empty pool when it goes to run abortTransaction + - name: failPoint # fail the next connection establishment + object: testRunner + arguments: + client: *client0 + session: *session1 + failPoint: + configureFailPoint: failCommand + mode: { times: 2 } + data: + # use saslContinue here to avoid SDAM errors + # this failPoint itself will create a usable connection in the connection pool + # so we run a ping (with closeConnection: true) in order to discard the connection + # before testing that abortTransaction will fail a handshake but will get retried + failCommands: [saslContinue, ping] + closeConnection: true + + - name: runCommand + object: *database0 + arguments: + commandName: ping + command: { ping: 1 } + session: *session1 + expectError: + isError: true + + - name: abortTransaction + object: *session0 + + expectEvents: + - client: *client0 + eventType: cmap + events: + - { connectionCheckOutStartedEvent: {} } # startTransaction + - { connectionCheckOutStartedEvent: {} } # insertOne + - { connectionCheckOutStartedEvent: {} } # failPoint + - { connectionCheckOutStartedEvent: {} } # abortTransaction + - { connectionCheckOutStartedEvent: {} } # abortTransaction retry + - client: *client0 + events: + - commandStartedEvent: + command: + insert: *collectionName + documents: [{ _id: 2, x: 22 }] + startTransaction: true + commandName: insert + databaseName: *databaseName + - commandStartedEvent: + command: + ping: 1 + databaseName: *databaseName + - commandStartedEvent: + command: + abortTransaction: 1 + lsid: + $$sessionLsid: *session0 + commandName: abortTransaction + databaseName: admin + + outcome: + - collectionName: *collectionName + databaseName: *databaseName + documents: + - { _id: 1, x: 11 } diff --git a/test/spec/transactions/unified/retryable-commit-handshake.json b/test/spec/transactions/unified/retryable-commit-handshake.json new file mode 100644 index 0000000000..d9315a8fc6 --- /dev/null +++ b/test/spec/transactions/unified/retryable-commit-handshake.json @@ -0,0 +1,211 @@ +{ + "description": "retryable commitTransaction on handshake errors", + "schemaVersion": "1.4", + "runOnRequirements": [ + { + "minServerVersion": "4.2", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ], + "serverless": "forbid", + "auth": true + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent", + "connectionCheckOutStartedEvent" + ], + "uriOptions": { + "retryWrites": false + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "retryable-handshake-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll" + } + }, + { + "session": { + "id": "session0", + "client": "client0" + } + }, + { + "session": { + "id": "session1", + "client": "client0" + } + } + ], + "initialData": [ + { + "collectionName": "coll", + "databaseName": "retryable-handshake-tests", + "documents": [ + { + "_id": 1, + "x": 11 + } + ] + } + ], + "tests": [ + { + "description": "CommitTransaction succeeds after handshake network error", + "skipReason": "DRIVERS-2032: Pinned servers need to be checked if they are still selectable", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "session": "session0", + "document": { + "_id": 2, + "x": 22 + } + } + }, + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client0", + "session": "session1", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "saslContinue", + "ping" + ], + "closeConnection": true + } + } + } + }, + { + "name": "runCommand", + "object": "database0", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + }, + "session": "session1" + }, + "expectError": { + "isError": true + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectEvents": [ + { + "client": "client0", + "eventType": "cmap", + "events": [ + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + } + ] + }, + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "coll", + "documents": [ + { + "_id": 2, + "x": 22 + } + ], + "startTransaction": true + }, + "commandName": "insert", + "databaseName": "retryable-handshake-tests" + } + }, + { + "commandStartedEvent": { + "command": { + "ping": 1 + }, + "databaseName": "retryable-handshake-tests" + } + }, + { + "commandStartedEvent": { + "command": { + "commitTransaction": 1, + "lsid": { + "$$sessionLsid": "session0" + } + }, + "commandName": "commitTransaction", + "databaseName": "admin" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll", + "databaseName": "retryable-handshake-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + ] + } + ] +} diff --git a/test/spec/transactions/unified/retryable-commit-handshake.yml b/test/spec/transactions/unified/retryable-commit-handshake.yml new file mode 100644 index 0000000000..e9904fdf51 --- /dev/null +++ b/test/spec/transactions/unified/retryable-commit-handshake.yml @@ -0,0 +1,118 @@ +description: "retryable commitTransaction on handshake errors" + +schemaVersion: "1.4" + +runOnRequirements: + - minServerVersion: "4.2" + topologies: [replicaset, sharded, load-balanced] + serverless: "forbid" + auth: true + +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeEvents: [commandStartedEvent, connectionCheckOutStartedEvent] + uriOptions: { retryWrites: false } # commitTransaction is retryable regardless of this option being set + - database: + id: &database0 database0 + client: *client0 + databaseName: &databaseName retryable-handshake-tests + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collectionName coll + - session: + id: &session0 session0 + client: *client0 + - session: + id: &session1 session1 + client: *client0 + +initialData: + - collectionName: *collectionName + databaseName: *databaseName + documents: + - { _id: 1, x: 11 } + +tests: + - description: "CommitTransaction succeeds after handshake network error" + skipReason: "DRIVERS-2032: Pinned servers need to be checked if they are still selectable" + operations: + + - name: startTransaction + object: *session0 + + - name: insertOne + object: *collection0 + arguments: + session: *session0 + document: { _id: 2, x: 22 } + + # The following failPoint and ping utilize session1 so that + # the transaction won't be failed by the intentional erroring of ping + # and it will have an empty pool when it goes to run commitTransaction + - name: failPoint # fail the next connection establishment + object: testRunner + arguments: + client: *client0 + session: *session1 + failPoint: + configureFailPoint: failCommand + mode: { times: 2 } + data: + # use saslContinue here to avoid SDAM errors + # this failPoint itself will create a usable connection in the connection pool + # so we run a ping (that also fails) in order to discard the connection + # before testing that commitTransaction gets retried + failCommands: [saslContinue, ping] + closeConnection: true + + - name: runCommand + object: *database0 + arguments: + commandName: ping + command: { ping: 1 } + session: *session1 + expectError: + isError: true + + - name: commitTransaction + object: *session0 + + expectEvents: + - client: *client0 + eventType: cmap + events: + - { connectionCheckOutStartedEvent: {} } # startTransaction + - { connectionCheckOutStartedEvent: {} } # insertOne + - { connectionCheckOutStartedEvent: {} } # failPoint + - { connectionCheckOutStartedEvent: {} } # commitTransaction + - { connectionCheckOutStartedEvent: {} } # commitTransaction retry + - client: *client0 + events: + - commandStartedEvent: + command: + insert: *collectionName + documents: [{ _id: 2, x: 22 }] + startTransaction: true + commandName: insert + databaseName: *databaseName + - commandStartedEvent: + command: + ping: 1 + databaseName: *databaseName + - commandStartedEvent: + command: + commitTransaction: 1 + lsid: + $$sessionLsid: *session0 + commandName: commitTransaction + databaseName: admin + + outcome: + - collectionName: *collectionName + databaseName: *databaseName + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } # The write was still applied From 9bc6814d64337beda91269e8b7fe14cd3a4e6c8c Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 15 Mar 2022 11:26:34 +0100 Subject: [PATCH 02/21] test(NODE-3688): add integration tests --- src/cmap/connect.ts | 3 +- src/error.ts | 15 ---- .../retryable_reads.prose.spec.test.js | 86 +++++++++++++++++++ .../retryable_reads.spec.test.js | 5 +- 4 files changed, 91 insertions(+), 18 deletions(-) create mode 100644 test/integration/retryable-reads/retryable_reads.prose.spec.test.js diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index 0f1ca7fe9a..0520c5d07e 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -10,7 +10,6 @@ import { LEGACY_HELLO_COMMAND } from '../constants'; import { AnyError, MongoCompatibilityError, - MongoHandshakeError, MongoInvalidArgumentError, MongoNetworkError, MongoNetworkTimeoutError, @@ -128,7 +127,7 @@ function performInitialHandshake( const start = new Date().getTime(); conn.command(ns('admin.$cmd'), handshakeDoc, handshakeOptions, (err, response) => { if (err) { - callback(new MongoHandshakeError(err.message)); + callback(err); return; } diff --git a/src/error.ts b/src/error.ts index 443ba61f19..5ea75e84ee 100644 --- a/src/error.ts +++ b/src/error.ts @@ -204,21 +204,6 @@ export class MongoDriverError extends MongoError { } } -/** - * An error during the handshake. - * - * @public - * @category Error - */ -export class MongoHandshakeError extends MongoError { - constructor(message: string) { - super(message); - } - - get name(): string { - return 'MongoHandshakeError'; - } -} /** * An error generated when the driver API is used incorrectly * diff --git a/test/integration/retryable-reads/retryable_reads.prose.spec.test.js b/test/integration/retryable-reads/retryable_reads.prose.spec.test.js new file mode 100644 index 0000000000..9234fa0dc2 --- /dev/null +++ b/test/integration/retryable-reads/retryable_reads.prose.spec.test.js @@ -0,0 +1,86 @@ +'use strict'; + +const { expect } = require('chai'); + +describe('Retryable Reads (prose)', function () { + const dbName = 'retryable-handshake-tests'; + const collName = 'coll'; + const docs = [ + { _id: 1, x: 11 }, + { _id: 2, x: 22 }, + { _id: 3, x: 33 } + ]; + let client; + let db; + let coll; + + beforeEach(function () { + client = this.configuration.newClient({}); + db = client.db(dbName); + coll = db.collection(collName); + }); + + afterEach(function (done) { + db.admin().command( + { + configureFailPoint: 'failCommand', + mode: 'off' + }, + () => { + coll.drop(() => { + client.close(done); + }); + } + ); + }); + + context('when the handshake fails with a network error', function () { + it('retries the read', function (done) { + client.connect(() => { + coll.insertMany(docs, () => { + db.admin().command( + { + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['saslContinue', 'ping'], + closeConnection: true + } + }, + () => { + coll.find().toArray((error, documents) => { + expect(documents).to.deep.equal(docs); + done(); + }); + } + ); + }); + }); + }); + }); + + context('when the handshake fails with shutdown in progress', function () { + it('retries the read', function (done) { + client.connect(() => { + coll.insertMany(docs, () => { + db.admin().command( + { + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['saslContinue', 'ping'], + errorCode: 91 // ShutdownInProgress + } + }, + () => { + coll.find().toArray((error, documents) => { + expect(documents).to.deep.equal(docs); + done(); + }); + } + ); + }); + }); + }); + }); +}); diff --git a/test/integration/retryable-reads/retryable_reads.spec.test.js b/test/integration/retryable-reads/retryable_reads.spec.test.js index 166563ae10..a5f594fc11 100644 --- a/test/integration/retryable-reads/retryable_reads.spec.test.js +++ b/test/integration/retryable-reads/retryable_reads.spec.test.js @@ -31,6 +31,9 @@ describe('Retryable Reads (legacy)', function () { }); }); -describe('Retryable Reads (unified)', function () { +// These tests are skipped because the driver 1) executes a ping when connecting to +// an authenticated server and 2) command monitoring is at the connection level so +// when the handshake fails no command started event is emitted. +describe.skip('Retryable Reads (unified)', function () { runUnifiedSuite(loadSpecTests(path.join('retryable-reads', 'unified'))); }); From fdbd2a30e3cf6eda12bb78b1e74771cbca39c055 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 15 Mar 2022 12:04:50 +0100 Subject: [PATCH 03/21] test(NODE-3688): add retryable writes tests --- .../retryable_writes.prose.spec.test.js | 84 +++++++ .../retryable_writes.spec.test.js | 213 ++++++++++++++++++ 2 files changed, 297 insertions(+) create mode 100644 test/integration/retryable-writes/retryable_writes.prose.spec.test.js create mode 100644 test/integration/retryable-writes/retryable_writes.spec.test.js diff --git a/test/integration/retryable-writes/retryable_writes.prose.spec.test.js b/test/integration/retryable-writes/retryable_writes.prose.spec.test.js new file mode 100644 index 0000000000..218cab57a7 --- /dev/null +++ b/test/integration/retryable-writes/retryable_writes.prose.spec.test.js @@ -0,0 +1,84 @@ +'use strict'; + +const { expect } = require('chai'); + +describe('Retryable Writes (prose)', function () { + const dbName = 'retryable-handshake-tests'; + const collName = 'coll'; + const docs = [{ _id: 1, x: 11 }]; + let client; + let db; + let coll; + + beforeEach(function () { + client = this.configuration.newClient({}); + db = client.db(dbName); + coll = db.collection(collName); + }); + + afterEach(function (done) { + db.admin().command( + { + configureFailPoint: 'failCommand', + mode: 'off' + }, + () => { + coll.drop(() => { + client.close(done); + }); + } + ); + }); + + context('when the handshake fails with a network error', function () { + it('retries the write', function (done) { + client.connect(() => { + coll.insertMany(docs, () => { + db.admin().command( + { + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['saslContinue', 'ping'], + closeConnection: true + } + }, + () => { + coll.insertOne({ _id: 2, x: 22 }, (error, result) => { + if (error) return error; + expect(result.insertedId).to.equal(2); + done(); + }); + } + ); + }); + }); + }); + }); + + context('when the handshake fails with shutdown in progress', function () { + it('retries the write', function (done) { + client.connect(() => { + coll.insertMany(docs, () => { + db.admin().command( + { + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['saslContinue', 'ping'], + errorCode: 91 // ShutdownInProgress + } + }, + () => { + coll.insertOne({ _id: 2, x: 22 }, (error, result) => { + if (error) return error; + expect(result.insertedId).to.equal(2); + done(); + }); + } + ); + }); + }); + }); + }); +}); diff --git a/test/integration/retryable-writes/retryable_writes.spec.test.js b/test/integration/retryable-writes/retryable_writes.spec.test.js new file mode 100644 index 0000000000..21426fcb5f --- /dev/null +++ b/test/integration/retryable-writes/retryable_writes.spec.test.js @@ -0,0 +1,213 @@ +'use strict'; + +const path = require('path'); +const { expect } = require('chai'); +const { loadSpecTests } = require('../../spec'); +const { legacyRunOnToRunOnRequirement } = require('../../tools/spec-runner'); +const { runUnifiedSuite } = require('../../tools/unified-spec-runner/runner'); +const { isAnyRequirementSatisfied } = require('../../tools/unified-spec-runner/unified-utils'); + +describe('Legacy Retryable Writes specs', function () { + let ctx = {}; + const retryableWrites = loadSpecTests(path.join('retryable-writes', 'legacy')); + + for (const suite of retryableWrites) { + describe(suite.name, function () { + beforeEach(async function () { + let utilClient; + if (this.configuration.isLoadBalanced) { + // The util client can always point at the single mongos LB frontend. + utilClient = this.configuration.newClient(this.configuration.singleMongosLoadBalancerUri); + } else { + utilClient = this.configuration.newClient(); + } + + await utilClient.connect(); + + const allRequirements = suite.runOn.map(legacyRunOnToRunOnRequirement); + + const someRequirementMet = + !allRequirements.length || + (await isAnyRequirementSatisfied(this.currentTest.ctx, allRequirements, utilClient)); + + await utilClient.close(); + + if (!someRequirementMet) this.skip(); + }); + + afterEach(async function () { + // Step 3: Test Teardown. Turn off failpoints, and close client + if (!ctx.db || !ctx.client) { + return; + } + + if (ctx.failPointName) { + await turnOffFailPoint(ctx.client, ctx.failPointName); + } + await ctx.client.close(); + ctx = {}; // reset context + }); + + for (const test of suite.tests) { + it(test.description, async function () { + // Step 1: Test Setup. Includes a lot of boilerplate stuff + // like creating a client, dropping and refilling data collections, + // and enabling failpoints + await executeScenarioSetup(suite, test, this.configuration, ctx); + // Step 2: Run the test + await executeScenarioTest(test, ctx); + }); + } + }); + } +}); + +function executeScenarioSetup(scenario, test, config, ctx) { + const url = config.url(); + const options = Object.assign({}, test.clientOptions, { + heartbeatFrequencyMS: 100, + monitorCommands: true, + minPoolSize: 10 + }); + + ctx.failPointName = test.failPoint && test.failPoint.configureFailPoint; + + const client = config.newClient(url, options); + return client + .connect() + .then(client => (ctx.client = client)) + .then(() => (ctx.db = ctx.client.db(config.db))) + .then( + () => + (ctx.collection = ctx.db.collection( + `retryable_writes_test_${config.name}_${test.operation.name}` + )) + ) + .then(() => ctx.collection.drop()) + .catch(err => { + if (!err.message.match(/ns not found/)) { + throw err; + } + }) + .then(() => + Array.isArray(scenario.data) && scenario.data.length + ? ctx.collection.insertMany(scenario.data) + : {} + ) + .then(() => (test.failPoint ? ctx.client.db('admin').command(test.failPoint) : {})); +} + +function executeScenarioTest(test, ctx) { + return Promise.resolve() + .then(() => { + const args = generateArguments(test); + + let result = ctx.collection[test.operation.name].apply(ctx.collection, args); + const outcome = test.outcome && test.outcome.result; + const errorLabelsContain = outcome && outcome.errorLabelsContain; + const errorLabelsOmit = outcome && outcome.errorLabelsOmit; + const hasResult = outcome && !errorLabelsContain && !errorLabelsOmit; + if (test.outcome.error) { + result = result + .then(() => expect(false).to.be.true) + .catch(err => { + expect(err).to.exist; + expect(err.message, 'expected operations to fail, but they succeeded').to.not.match( + /expected false to be true/ + ); + if (hasResult) expect(err.result).to.matchMongoSpec(test.outcome.result); + if (errorLabelsContain) expect(err.errorLabels).to.include.members(errorLabelsContain); + if (errorLabelsOmit) { + errorLabelsOmit.forEach(label => { + expect(err.errorLabels).to.not.contain(label); + }); + } + }); + } else if (test.outcome.result) { + const expected = test.outcome.result; + result = result.then(transformToResultValue).then(r => expect(r).to.deep.include(expected)); + } + + return result; + }) + .then(() => { + if (test.outcome.collection) { + return ctx.collection + .find({}) + .toArray() + .then(collectionResults => { + expect(collectionResults).to.eql(test.outcome.collection.data); + }); + } + }); +} + +// Helper Functions + +/** + * Transforms the arguments from a test into actual arguments for our function calls + * + * @param {any} test + */ +function generateArguments(test) { + const args = []; + + if (test.operation.arguments) { + const options = {}; + Object.keys(test.operation.arguments).forEach(arg => { + if (arg === 'requests') { + args.push(test.operation.arguments[arg].map(convertBulkWriteOperation)); + } else if (arg === 'upsert') { + options.upsert = test.operation.arguments[arg]; + } else if (arg === 'returnDocument') { + options.returnDocument = test.operation.arguments[arg].toLowerCase(); + } else { + args.push(test.operation.arguments[arg]); + } + }); + + if (Object.keys(options).length > 0) { + args.push(options); + } + } + + return args; +} + +/** + * Transforms a request arg into a bulk write operation + * + * @param {any} op + */ +function convertBulkWriteOperation(op) { + return { [op.name]: op.arguments }; +} + +/** + * Transforms output of a bulk write to conform to the test format + * + * @param {any} result + */ +function transformToResultValue(result) { + return result && result.value ? result.value : result; +} + +/** Runs a command that turns off a fail point */ +function turnOffFailPoint(client, name) { + return client.db('admin').command({ + configureFailPoint: name, + mode: 'off' + }); +} + +// These tests are skipped because the driver 1) executes a ping when connecting to +// an authenticated server and 2) command monitoring is at the connection level so +// when the handshake fails no command started event is emitted. +const SKIP = [ + 'InsertOne succeeds after retryable handshake error', + 'InsertOne succeeds after retryable handshake error ShutdownInProgress' +]; + +describe('Retryable Writes (unified)', function () { + runUnifiedSuite(loadSpecTests(path.join('retryable-writes', 'unified')), SKIP); +}); From 9f8ead427b2ac7199cf11976097e45ceed2ba9f6 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 15 Mar 2022 12:27:06 +0100 Subject: [PATCH 04/21] test(NODE-3688): add txn prose tests --- .../retryable_reads.prose.spec.test.js | 9 +- .../retryable_writes.prose.spec.test.js | 9 +- .../transactions.prose.spec.test.js | 95 +++++++++++++++++++ .../transactions/transactions.spec.test.js | 8 ++ 4 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 test/integration/transactions/transactions.prose.spec.test.js diff --git a/test/integration/retryable-reads/retryable_reads.prose.spec.test.js b/test/integration/retryable-reads/retryable_reads.prose.spec.test.js index 9234fa0dc2..54dedbb4c7 100644 --- a/test/integration/retryable-reads/retryable_reads.prose.spec.test.js +++ b/test/integration/retryable-reads/retryable_reads.prose.spec.test.js @@ -2,7 +2,14 @@ const { expect } = require('chai'); -describe('Retryable Reads (prose)', function () { +const metadata = { + requires: { + mongodb: '>=4.2.0', + topology: ['replicaset', 'sharded', 'load-balanced'] + } +}; + +describe('Retryable Reads (prose)', metadata, function () { const dbName = 'retryable-handshake-tests'; const collName = 'coll'; const docs = [ diff --git a/test/integration/retryable-writes/retryable_writes.prose.spec.test.js b/test/integration/retryable-writes/retryable_writes.prose.spec.test.js index 218cab57a7..824c73dbdd 100644 --- a/test/integration/retryable-writes/retryable_writes.prose.spec.test.js +++ b/test/integration/retryable-writes/retryable_writes.prose.spec.test.js @@ -2,7 +2,14 @@ const { expect } = require('chai'); -describe('Retryable Writes (prose)', function () { +const metadata = { + requires: { + mongodb: '>=4.2.0', + topology: ['replicaset', 'sharded', 'load-balanced'] + } +}; + +describe('Retryable Writes (prose)', metadata, function () { const dbName = 'retryable-handshake-tests'; const collName = 'coll'; const docs = [{ _id: 1, x: 11 }]; diff --git a/test/integration/transactions/transactions.prose.spec.test.js b/test/integration/transactions/transactions.prose.spec.test.js new file mode 100644 index 0000000000..9c12354739 --- /dev/null +++ b/test/integration/transactions/transactions.prose.spec.test.js @@ -0,0 +1,95 @@ +'use strict'; + +const { expect } = require('chai'); + +const metadata = { + requires: { + mongodb: '>=4.2.0', + topology: ['replicaset', 'sharded', 'load-balanced'] + } +}; + +describe('Transactions (prose)', metadata, function () { + const dbName = 'retryable-handshake-tests'; + const collName = 'coll'; + const docs = [{ _id: 1, x: 11 }]; + let client; + let db; + let coll; + + beforeEach(function () { + client = this.configuration.newClient({}); + db = client.db(dbName); + coll = db.collection(collName); + }); + + afterEach(function (done) { + db.admin().command( + { + configureFailPoint: 'failCommand', + mode: 'off' + }, + () => { + coll.drop(() => { + client.close(done); + }); + } + ); + }); + + context('when the handshake fails with a network error', function () { + it('retries the abort', function (done) { + client.connect(() => { + coll.insertMany(docs, () => { + const session = client.startSession(); + session.startTransaction(); + db.admin().command( + { + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['saslContinue', 'ping'], + closeConnection: true + } + }, + () => { + coll.insertOne({ _id: 2, x: 22 }, (error, result) => { + session.abortTransaction((error) => { + if (error) return error; + session.endSession(done); + }); + }); + } + ); + }); + }); + }); + + it('retries the commit', function (done) { + client.connect(() => { + coll.insertMany(docs, () => { + const session = client.startSession(); + session.startTransaction(); + db.admin().command( + { + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['saslContinue', 'ping'], + closeConnection: true + } + }, + () => { + coll.insertOne({ _id: 2, x: 22 }, (error, result) => { + session.commitTransaction((error) => { + if (error) return error; + session.endSession(done); + }); + }); + } + ); + }); + }); + }); + }); +}); diff --git a/test/integration/transactions/transactions.spec.test.js b/test/integration/transactions/transactions.spec.test.js index 05b829324a..65e1ccc30a 100644 --- a/test/integration/transactions/transactions.spec.test.js +++ b/test/integration/transactions/transactions.spec.test.js @@ -78,6 +78,14 @@ class TransactionsRunnerContext extends TestRunnerContext { } } +// These tests are skipped because the driver 1) executes a ping when connecting to +// an authenticated server and 2) command monitoring is at the connection level so +// when the handshake fails no command started event is emitted. +const SKIP = [ + 'AbortTransaction succeeds after handshake network error', + 'CommitTransaction succeeds after handshake network error' +]; + describe('Transactions Spec Unified Tests', function () { runUnifiedSuite(loadSpecTests(path.join('transactions', 'unified'))); }); From 343f6ce9831e84b8c7cab12a98a6f76dd0b5d425 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 15 Mar 2022 13:08:19 +0100 Subject: [PATCH 05/21] refactor(NODE-3688): new tests to ts and async --- .../retryable_reads.prose.spec.test.js | 93 -------- .../retryable_reads.prose.spec.test.ts | 76 +++++++ .../retryable_writes.prose.spec.test.js | 91 -------- .../retryable_writes.spec.prose.test.ts | 171 ++++++++++---- .../retryable_writes.spec.test.js | 213 ------------------ .../retryable_writes.spec.test.ts | 14 ++ .../transactions.prose.spec.test.js | 95 -------- .../transactions.prose.spec.test.ts | 80 +++++++ 8 files changed, 292 insertions(+), 541 deletions(-) delete mode 100644 test/integration/retryable-reads/retryable_reads.prose.spec.test.js create mode 100644 test/integration/retryable-reads/retryable_reads.prose.spec.test.ts delete mode 100644 test/integration/retryable-writes/retryable_writes.prose.spec.test.js delete mode 100644 test/integration/retryable-writes/retryable_writes.spec.test.js delete mode 100644 test/integration/transactions/transactions.prose.spec.test.js create mode 100644 test/integration/transactions/transactions.prose.spec.test.ts diff --git a/test/integration/retryable-reads/retryable_reads.prose.spec.test.js b/test/integration/retryable-reads/retryable_reads.prose.spec.test.js deleted file mode 100644 index 54dedbb4c7..0000000000 --- a/test/integration/retryable-reads/retryable_reads.prose.spec.test.js +++ /dev/null @@ -1,93 +0,0 @@ -'use strict'; - -const { expect } = require('chai'); - -const metadata = { - requires: { - mongodb: '>=4.2.0', - topology: ['replicaset', 'sharded', 'load-balanced'] - } -}; - -describe('Retryable Reads (prose)', metadata, function () { - const dbName = 'retryable-handshake-tests'; - const collName = 'coll'; - const docs = [ - { _id: 1, x: 11 }, - { _id: 2, x: 22 }, - { _id: 3, x: 33 } - ]; - let client; - let db; - let coll; - - beforeEach(function () { - client = this.configuration.newClient({}); - db = client.db(dbName); - coll = db.collection(collName); - }); - - afterEach(function (done) { - db.admin().command( - { - configureFailPoint: 'failCommand', - mode: 'off' - }, - () => { - coll.drop(() => { - client.close(done); - }); - } - ); - }); - - context('when the handshake fails with a network error', function () { - it('retries the read', function (done) { - client.connect(() => { - coll.insertMany(docs, () => { - db.admin().command( - { - configureFailPoint: 'failCommand', - mode: { times: 1 }, - data: { - failCommands: ['saslContinue', 'ping'], - closeConnection: true - } - }, - () => { - coll.find().toArray((error, documents) => { - expect(documents).to.deep.equal(docs); - done(); - }); - } - ); - }); - }); - }); - }); - - context('when the handshake fails with shutdown in progress', function () { - it('retries the read', function (done) { - client.connect(() => { - coll.insertMany(docs, () => { - db.admin().command( - { - configureFailPoint: 'failCommand', - mode: { times: 1 }, - data: { - failCommands: ['saslContinue', 'ping'], - errorCode: 91 // ShutdownInProgress - } - }, - () => { - coll.find().toArray((error, documents) => { - expect(documents).to.deep.equal(docs); - done(); - }); - } - ); - }); - }); - }); - }); -}); diff --git a/test/integration/retryable-reads/retryable_reads.prose.spec.test.ts b/test/integration/retryable-reads/retryable_reads.prose.spec.test.ts new file mode 100644 index 0000000000..5ec280de2a --- /dev/null +++ b/test/integration/retryable-reads/retryable_reads.prose.spec.test.ts @@ -0,0 +1,76 @@ +import { expect } from 'chai'; + +const metadata = { + requires: { + mongodb: '>=4.2.0', + topology: ['replicaset', 'sharded', 'load-balanced'] + } +}; + +describe.only('Retryable Reads (prose)', metadata, function () { + const dbName = 'retryable-handshake-tests'; + const collName = 'coll'; + const docs = [ + { _id: 1, x: 11 }, + { _id: 2, x: 22 }, + { _id: 3, x: 33 } + ]; + let client; + let db; + let coll; + + beforeEach(function () { + client = this.configuration.newClient({}); + db = client.db(dbName); + coll = db.collection(collName); + }); + + afterEach(async function () { + await db.admin().command( + { + configureFailPoint: 'failCommand', + mode: 'off' + } + ); + await coll.drop(); + await client.close(); + }); + + context('when the handshake fails with a network error', function () { + it('retries the read', async function () { + await client.connect(); + await coll.insertMany(docs); + await db.admin().command( + { + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['saslContinue', 'ping'], + closeConnection: true + } + } + ); + const documents = await coll.find().toArray(); + expect(documents).to.deep.equal(docs); + }); + }); + + context('when the handshake fails with shutdown in progress', function () { + it('retries the read', async function () { + await client.connect(); + await coll.insertMany(docs); + await db.admin().command( + { + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['saslContinue', 'ping'], + errorCode: 91 // ShutdownInProgress + } + } + ); + const documents = await coll.find().toArray(); + expect(documents).to.deep.equal(docs); + }); + }); +}); diff --git a/test/integration/retryable-writes/retryable_writes.prose.spec.test.js b/test/integration/retryable-writes/retryable_writes.prose.spec.test.js deleted file mode 100644 index 824c73dbdd..0000000000 --- a/test/integration/retryable-writes/retryable_writes.prose.spec.test.js +++ /dev/null @@ -1,91 +0,0 @@ -'use strict'; - -const { expect } = require('chai'); - -const metadata = { - requires: { - mongodb: '>=4.2.0', - topology: ['replicaset', 'sharded', 'load-balanced'] - } -}; - -describe('Retryable Writes (prose)', metadata, function () { - const dbName = 'retryable-handshake-tests'; - const collName = 'coll'; - const docs = [{ _id: 1, x: 11 }]; - let client; - let db; - let coll; - - beforeEach(function () { - client = this.configuration.newClient({}); - db = client.db(dbName); - coll = db.collection(collName); - }); - - afterEach(function (done) { - db.admin().command( - { - configureFailPoint: 'failCommand', - mode: 'off' - }, - () => { - coll.drop(() => { - client.close(done); - }); - } - ); - }); - - context('when the handshake fails with a network error', function () { - it('retries the write', function (done) { - client.connect(() => { - coll.insertMany(docs, () => { - db.admin().command( - { - configureFailPoint: 'failCommand', - mode: { times: 1 }, - data: { - failCommands: ['saslContinue', 'ping'], - closeConnection: true - } - }, - () => { - coll.insertOne({ _id: 2, x: 22 }, (error, result) => { - if (error) return error; - expect(result.insertedId).to.equal(2); - done(); - }); - } - ); - }); - }); - }); - }); - - context('when the handshake fails with shutdown in progress', function () { - it('retries the write', function (done) { - client.connect(() => { - coll.insertMany(docs, () => { - db.admin().command( - { - configureFailPoint: 'failCommand', - mode: { times: 1 }, - data: { - failCommands: ['saslContinue', 'ping'], - errorCode: 91 // ShutdownInProgress - } - }, - () => { - coll.insertOne({ _id: 2, x: 22 }, (error, result) => { - if (error) return error; - expect(result.insertedId).to.equal(2); - done(); - }); - } - ); - }); - }); - }); - }); -}); diff --git a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts index 2171885f85..8ab8838282 100644 --- a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts +++ b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts @@ -2,63 +2,136 @@ import { expect } from 'chai'; import { MongoError, MongoServerError, TopologyType } from '../../../src'; +const metadata = { + requires: { + mongodb: '>=4.2.0', + topology: ['replicaset', 'sharded', 'load-balanced'] + } +}; + describe('Retryable Writes Spec Prose', () => { - /** - * 1 Test that retryable writes raise an exception when using the MMAPv1 storage engine. - * For this test, execute a write operation, such as insertOne, which should generate an exception and the error code is 20. - * Assert that the error message is the replacement error message: - * - * ``` - * This MongoDB deployment does not support retryable writes. Please add - * retryWrites=false to your connection string. - * ``` - * Note: Drivers that rely on serverStatus to determine the storage engine in use MAY skip this test for sharded clusters, since mongos does not report this information in its serverStatus response. - */ - let client; + context('when checking against mmapv1', () => { + /** + * 1 Test that retryable writes raise an exception when using the MMAPv1 storage engine. + * For this test, execute a write operation, such as insertOne, which should generate an exception and the error code is 20. + * Assert that the error message is the replacement error message: + * + * ``` + * This MongoDB deployment does not support retryable writes. Please add + * retryWrites=false to your connection string. + * ``` + * Note: Drivers that rely on serverStatus to determine the storage engine in use MAY skip this test for sharded clusters, since mongos does not report this information in its serverStatus response. + */ + let client; - beforeEach(async function () { - if ( - this.configuration.buildInfo.versionArray[0] < 4 || - this.configuration.topologyType !== TopologyType.ReplicaSetWithPrimary - ) { - this.currentTest.skipReason = - 'configureFailPoint only works on server versions greater than 4'; - this.skip(); - } - client = this.configuration.newClient(); - await client.connect(); - }); + beforeEach(async function () { + if ( + this.configuration.buildInfo.versionArray[0] < 4 || + this.configuration.topologyType !== TopologyType.ReplicaSetWithPrimary + ) { + this.currentTest.skipReason = + 'configureFailPoint only works on server versions greater than 4'; + this.skip(); + } + client = this.configuration.newClient(); + await client.connect(); + }); + + afterEach(async () => { + await client?.close(); + }); + + it('retryable writes raise an exception when using the MMAPv1 storage engine', async () => { + const failPoint = await client.db('admin').command({ + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['insert'], + errorCode: 20, // MMAP Error code, + closeConnection: false + } + }); + + expect(failPoint).to.have.property('ok', 1); - afterEach(async () => { - await client?.close(); + const error = await client + .db('test') + .collection('test') + .insertOne({ a: 1 }) + .catch(error => error); + + expect(error).to.exist; + expect(error).that.is.instanceOf(MongoServerError); + expect(error).to.have.property('originalError').that.instanceOf(MongoError); + expect(error.originalError).to.have.property('code', 20); + expect(error).to.have.property( + 'message', + 'This MongoDB deployment does not support retryable writes. Please add retryWrites=false to your connection string.' + ); + }); }); - it('retryable writes raise an exception when using the MMAPv1 storage engine', async () => { - const failPoint = await client.db('admin').command({ - configureFailPoint: 'failCommand', - mode: { times: 1 }, - data: { - failCommands: ['insert'], - errorCode: 20, // MMAP Error code, - closeConnection: false - } + context('when errors occur in the handshake', metadata, function () { + const dbName = 'retryable-handshake-tests'; + const collName = 'coll'; + const docs = [{ _id: 1, x: 11 }]; + let client; + let db; + let coll; + + beforeEach(function () { + client = this.configuration.newClient({}); + db = client.db(dbName); + coll = db.collection(collName); }); - expect(failPoint).to.have.property('ok', 1); + afterEach(async function () { + await db.admin().command( + { + configureFailPoint: 'failCommand', + mode: 'off' + } + ); + await coll.drop(); + await client.close(); + }); - const error = await client - .db('test') - .collection('test') - .insertOne({ a: 1 }) - .catch(error => error); + context('when the handshake fails with a network error', function () { + it('retries the write', async function () { + await client.connect(); + await coll.insertMany(docs); + await db.admin().command( + { + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['saslContinue', 'ping'], + closeConnection: true + } + } + ); + const result = await coll.insertOne({ _id: 2, x: 22 }); + expect(result.insertedId).to.equal(2); + }); + }); - expect(error).to.exist; - expect(error).that.is.instanceOf(MongoServerError); - expect(error).to.have.property('originalError').that.instanceOf(MongoError); - expect(error.originalError).to.have.property('code', 20); - expect(error).to.have.property( - 'message', - 'This MongoDB deployment does not support retryable writes. Please add retryWrites=false to your connection string.' - ); + context('when the handshake fails with shutdown in progress', function () { + it('retries the write', async function () { + await client.connect(); + await coll.insertMany(docs); + await db.admin().command( + { + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['saslContinue', 'ping'], + errorCode: 91 // ShutdownInProgress + } + } + ); + const result = await coll.insertOne({ _id: 2, x: 22 }); + expect(result.insertedId).to.equal(2); + }); + }); }); }); diff --git a/test/integration/retryable-writes/retryable_writes.spec.test.js b/test/integration/retryable-writes/retryable_writes.spec.test.js deleted file mode 100644 index 21426fcb5f..0000000000 --- a/test/integration/retryable-writes/retryable_writes.spec.test.js +++ /dev/null @@ -1,213 +0,0 @@ -'use strict'; - -const path = require('path'); -const { expect } = require('chai'); -const { loadSpecTests } = require('../../spec'); -const { legacyRunOnToRunOnRequirement } = require('../../tools/spec-runner'); -const { runUnifiedSuite } = require('../../tools/unified-spec-runner/runner'); -const { isAnyRequirementSatisfied } = require('../../tools/unified-spec-runner/unified-utils'); - -describe('Legacy Retryable Writes specs', function () { - let ctx = {}; - const retryableWrites = loadSpecTests(path.join('retryable-writes', 'legacy')); - - for (const suite of retryableWrites) { - describe(suite.name, function () { - beforeEach(async function () { - let utilClient; - if (this.configuration.isLoadBalanced) { - // The util client can always point at the single mongos LB frontend. - utilClient = this.configuration.newClient(this.configuration.singleMongosLoadBalancerUri); - } else { - utilClient = this.configuration.newClient(); - } - - await utilClient.connect(); - - const allRequirements = suite.runOn.map(legacyRunOnToRunOnRequirement); - - const someRequirementMet = - !allRequirements.length || - (await isAnyRequirementSatisfied(this.currentTest.ctx, allRequirements, utilClient)); - - await utilClient.close(); - - if (!someRequirementMet) this.skip(); - }); - - afterEach(async function () { - // Step 3: Test Teardown. Turn off failpoints, and close client - if (!ctx.db || !ctx.client) { - return; - } - - if (ctx.failPointName) { - await turnOffFailPoint(ctx.client, ctx.failPointName); - } - await ctx.client.close(); - ctx = {}; // reset context - }); - - for (const test of suite.tests) { - it(test.description, async function () { - // Step 1: Test Setup. Includes a lot of boilerplate stuff - // like creating a client, dropping and refilling data collections, - // and enabling failpoints - await executeScenarioSetup(suite, test, this.configuration, ctx); - // Step 2: Run the test - await executeScenarioTest(test, ctx); - }); - } - }); - } -}); - -function executeScenarioSetup(scenario, test, config, ctx) { - const url = config.url(); - const options = Object.assign({}, test.clientOptions, { - heartbeatFrequencyMS: 100, - monitorCommands: true, - minPoolSize: 10 - }); - - ctx.failPointName = test.failPoint && test.failPoint.configureFailPoint; - - const client = config.newClient(url, options); - return client - .connect() - .then(client => (ctx.client = client)) - .then(() => (ctx.db = ctx.client.db(config.db))) - .then( - () => - (ctx.collection = ctx.db.collection( - `retryable_writes_test_${config.name}_${test.operation.name}` - )) - ) - .then(() => ctx.collection.drop()) - .catch(err => { - if (!err.message.match(/ns not found/)) { - throw err; - } - }) - .then(() => - Array.isArray(scenario.data) && scenario.data.length - ? ctx.collection.insertMany(scenario.data) - : {} - ) - .then(() => (test.failPoint ? ctx.client.db('admin').command(test.failPoint) : {})); -} - -function executeScenarioTest(test, ctx) { - return Promise.resolve() - .then(() => { - const args = generateArguments(test); - - let result = ctx.collection[test.operation.name].apply(ctx.collection, args); - const outcome = test.outcome && test.outcome.result; - const errorLabelsContain = outcome && outcome.errorLabelsContain; - const errorLabelsOmit = outcome && outcome.errorLabelsOmit; - const hasResult = outcome && !errorLabelsContain && !errorLabelsOmit; - if (test.outcome.error) { - result = result - .then(() => expect(false).to.be.true) - .catch(err => { - expect(err).to.exist; - expect(err.message, 'expected operations to fail, but they succeeded').to.not.match( - /expected false to be true/ - ); - if (hasResult) expect(err.result).to.matchMongoSpec(test.outcome.result); - if (errorLabelsContain) expect(err.errorLabels).to.include.members(errorLabelsContain); - if (errorLabelsOmit) { - errorLabelsOmit.forEach(label => { - expect(err.errorLabels).to.not.contain(label); - }); - } - }); - } else if (test.outcome.result) { - const expected = test.outcome.result; - result = result.then(transformToResultValue).then(r => expect(r).to.deep.include(expected)); - } - - return result; - }) - .then(() => { - if (test.outcome.collection) { - return ctx.collection - .find({}) - .toArray() - .then(collectionResults => { - expect(collectionResults).to.eql(test.outcome.collection.data); - }); - } - }); -} - -// Helper Functions - -/** - * Transforms the arguments from a test into actual arguments for our function calls - * - * @param {any} test - */ -function generateArguments(test) { - const args = []; - - if (test.operation.arguments) { - const options = {}; - Object.keys(test.operation.arguments).forEach(arg => { - if (arg === 'requests') { - args.push(test.operation.arguments[arg].map(convertBulkWriteOperation)); - } else if (arg === 'upsert') { - options.upsert = test.operation.arguments[arg]; - } else if (arg === 'returnDocument') { - options.returnDocument = test.operation.arguments[arg].toLowerCase(); - } else { - args.push(test.operation.arguments[arg]); - } - }); - - if (Object.keys(options).length > 0) { - args.push(options); - } - } - - return args; -} - -/** - * Transforms a request arg into a bulk write operation - * - * @param {any} op - */ -function convertBulkWriteOperation(op) { - return { [op.name]: op.arguments }; -} - -/** - * Transforms output of a bulk write to conform to the test format - * - * @param {any} result - */ -function transformToResultValue(result) { - return result && result.value ? result.value : result; -} - -/** Runs a command that turns off a fail point */ -function turnOffFailPoint(client, name) { - return client.db('admin').command({ - configureFailPoint: name, - mode: 'off' - }); -} - -// These tests are skipped because the driver 1) executes a ping when connecting to -// an authenticated server and 2) command monitoring is at the connection level so -// when the handshake fails no command started event is emitted. -const SKIP = [ - 'InsertOne succeeds after retryable handshake error', - 'InsertOne succeeds after retryable handshake error ShutdownInProgress' -]; - -describe('Retryable Writes (unified)', function () { - runUnifiedSuite(loadSpecTests(path.join('retryable-writes', 'unified')), SKIP); -}); diff --git a/test/integration/retryable-writes/retryable_writes.spec.test.ts b/test/integration/retryable-writes/retryable_writes.spec.test.ts index 17dbf62145..e1dcfc4540 100644 --- a/test/integration/retryable-writes/retryable_writes.spec.test.ts +++ b/test/integration/retryable-writes/retryable_writes.spec.test.ts @@ -1,8 +1,10 @@ import { expect } from 'chai'; +import * as path from 'path'; import type { Collection, Db, MongoClient } from '../../../src'; import { loadSpecTests } from '../../spec'; import { legacyRunOnToRunOnRequirement } from '../../tools/spec-runner'; +import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner'; import { isAnyRequirementSatisfied } from '../../tools/unified-spec-runner/unified-utils'; interface RetryableWriteTestContext { @@ -196,3 +198,15 @@ async function turnOffFailPoint(client, name) { mode: 'off' }); } + +// These tests are skipped because the driver 1) executes a ping when connecting to +// an authenticated server and 2) command monitoring is at the connection level so +// when the handshake fails no command started event is emitted. +const SKIP = [ + 'InsertOne succeeds after retryable handshake error', + 'InsertOne succeeds after retryable handshake error ShutdownInProgress' +]; + +describe('Retryable Writes (unified)', function () { + runUnifiedSuite(loadSpecTests(path.join('retryable-writes', 'unified')), SKIP); +}); diff --git a/test/integration/transactions/transactions.prose.spec.test.js b/test/integration/transactions/transactions.prose.spec.test.js deleted file mode 100644 index 9c12354739..0000000000 --- a/test/integration/transactions/transactions.prose.spec.test.js +++ /dev/null @@ -1,95 +0,0 @@ -'use strict'; - -const { expect } = require('chai'); - -const metadata = { - requires: { - mongodb: '>=4.2.0', - topology: ['replicaset', 'sharded', 'load-balanced'] - } -}; - -describe('Transactions (prose)', metadata, function () { - const dbName = 'retryable-handshake-tests'; - const collName = 'coll'; - const docs = [{ _id: 1, x: 11 }]; - let client; - let db; - let coll; - - beforeEach(function () { - client = this.configuration.newClient({}); - db = client.db(dbName); - coll = db.collection(collName); - }); - - afterEach(function (done) { - db.admin().command( - { - configureFailPoint: 'failCommand', - mode: 'off' - }, - () => { - coll.drop(() => { - client.close(done); - }); - } - ); - }); - - context('when the handshake fails with a network error', function () { - it('retries the abort', function (done) { - client.connect(() => { - coll.insertMany(docs, () => { - const session = client.startSession(); - session.startTransaction(); - db.admin().command( - { - configureFailPoint: 'failCommand', - mode: { times: 1 }, - data: { - failCommands: ['saslContinue', 'ping'], - closeConnection: true - } - }, - () => { - coll.insertOne({ _id: 2, x: 22 }, (error, result) => { - session.abortTransaction((error) => { - if (error) return error; - session.endSession(done); - }); - }); - } - ); - }); - }); - }); - - it('retries the commit', function (done) { - client.connect(() => { - coll.insertMany(docs, () => { - const session = client.startSession(); - session.startTransaction(); - db.admin().command( - { - configureFailPoint: 'failCommand', - mode: { times: 1 }, - data: { - failCommands: ['saslContinue', 'ping'], - closeConnection: true - } - }, - () => { - coll.insertOne({ _id: 2, x: 22 }, (error, result) => { - session.commitTransaction((error) => { - if (error) return error; - session.endSession(done); - }); - }); - } - ); - }); - }); - }); - }); -}); diff --git a/test/integration/transactions/transactions.prose.spec.test.ts b/test/integration/transactions/transactions.prose.spec.test.ts new file mode 100644 index 0000000000..1c7e76e62b --- /dev/null +++ b/test/integration/transactions/transactions.prose.spec.test.ts @@ -0,0 +1,80 @@ +import { expect } from 'chai'; + +const metadata = { + requires: { + mongodb: '>=4.2.0', + topology: ['replicaset', 'sharded', 'load-balanced'] + } +}; + +describe.only('Transactions (prose)', metadata, function () { + const dbName = 'retryable-handshake-tests'; + const collName = 'coll'; + const docs = [{ _id: 1, x: 11 }]; + let client; + let db; + let coll; + + beforeEach(function () { + client = this.configuration.newClient({}); + db = client.db(dbName); + coll = db.collection(collName); + }); + + afterEach(async function () { + await db.admin().command( + { + configureFailPoint: 'failCommand', + mode: 'off' + } + ); + await coll.drop(); + await client.close(); + }); + + context('when the handshake fails with a network error', function () { + it('retries the abort', async function () { + await client.connect(); + await coll.insertMany(docs); + const session = client.startSession(); + session.startTransaction(); + await db.admin().command( + { + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['saslContinue', 'ping'], + closeConnection: true + } + } + ); + const result = await coll.insertOne({ _id: 2, x: 22 }); + await session.abortTransaction(); + const res = await session.endSession(); + const doc = await coll.findOne({ _id: 2 }); + expect(doc).to.not.exist; + }); + + it('retries the commit', async function () { + await client.connect(); + await coll.insertMany(docs); + const session = client.startSession(); + session.startTransaction(); + await db.admin().command( + { + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['saslContinue', 'ping'], + closeConnection: true + } + } + ); + const result = await coll.insertOne({ _id: 2, x: 22 }); + await session.commitTransaction(); + await session.endSession(); + const doc = await coll.findOne({ _id: 2 }); + expect(doc.x).to.equal(22); + }); + }); +}); From 504911e17b38e9a0f1150337ad1aadb7ed2ca23b Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 15 Mar 2022 13:17:31 +0100 Subject: [PATCH 06/21] fix(NODE-3688): fix lint errors --- .../retryable_reads.prose.spec.test.ts | 44 +++++++--------- .../retryable_writes.spec.prose.test.ts | 42 +++++++--------- .../retryable_writes.spec.test.ts | 2 +- .../transactions.prose.spec.test.ts | 50 ++++++++----------- .../transactions/transactions.spec.test.js | 2 +- 5 files changed, 61 insertions(+), 79 deletions(-) diff --git a/test/integration/retryable-reads/retryable_reads.prose.spec.test.ts b/test/integration/retryable-reads/retryable_reads.prose.spec.test.ts index 5ec280de2a..6bf3997318 100644 --- a/test/integration/retryable-reads/retryable_reads.prose.spec.test.ts +++ b/test/integration/retryable-reads/retryable_reads.prose.spec.test.ts @@ -7,7 +7,7 @@ const metadata = { } }; -describe.only('Retryable Reads (prose)', metadata, function () { +describe('Retryable Reads (prose)', metadata, function () { const dbName = 'retryable-handshake-tests'; const collName = 'coll'; const docs = [ @@ -26,12 +26,10 @@ describe.only('Retryable Reads (prose)', metadata, function () { }); afterEach(async function () { - await db.admin().command( - { - configureFailPoint: 'failCommand', - mode: 'off' - } - ); + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); await coll.drop(); await client.close(); }); @@ -40,16 +38,14 @@ describe.only('Retryable Reads (prose)', metadata, function () { it('retries the read', async function () { await client.connect(); await coll.insertMany(docs); - await db.admin().command( - { - configureFailPoint: 'failCommand', - mode: { times: 1 }, - data: { - failCommands: ['saslContinue', 'ping'], - closeConnection: true - } + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['saslContinue', 'ping'], + closeConnection: true } - ); + }); const documents = await coll.find().toArray(); expect(documents).to.deep.equal(docs); }); @@ -59,16 +55,14 @@ describe.only('Retryable Reads (prose)', metadata, function () { it('retries the read', async function () { await client.connect(); await coll.insertMany(docs); - await db.admin().command( - { - configureFailPoint: 'failCommand', - mode: { times: 1 }, - data: { - failCommands: ['saslContinue', 'ping'], - errorCode: 91 // ShutdownInProgress - } + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['saslContinue', 'ping'], + errorCode: 91 // ShutdownInProgress } - ); + }); const documents = await coll.find().toArray(); expect(documents).to.deep.equal(docs); }); diff --git a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts index 8ab8838282..b786fd6699 100644 --- a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts +++ b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts @@ -86,12 +86,10 @@ describe('Retryable Writes Spec Prose', () => { }); afterEach(async function () { - await db.admin().command( - { - configureFailPoint: 'failCommand', - mode: 'off' - } - ); + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); await coll.drop(); await client.close(); }); @@ -100,16 +98,14 @@ describe('Retryable Writes Spec Prose', () => { it('retries the write', async function () { await client.connect(); await coll.insertMany(docs); - await db.admin().command( - { - configureFailPoint: 'failCommand', - mode: { times: 1 }, - data: { - failCommands: ['saslContinue', 'ping'], - closeConnection: true - } + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['saslContinue', 'ping'], + closeConnection: true } - ); + }); const result = await coll.insertOne({ _id: 2, x: 22 }); expect(result.insertedId).to.equal(2); }); @@ -119,16 +115,14 @@ describe('Retryable Writes Spec Prose', () => { it('retries the write', async function () { await client.connect(); await coll.insertMany(docs); - await db.admin().command( - { - configureFailPoint: 'failCommand', - mode: { times: 1 }, - data: { - failCommands: ['saslContinue', 'ping'], - errorCode: 91 // ShutdownInProgress - } + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['saslContinue', 'ping'], + errorCode: 91 // ShutdownInProgress } - ); + }); const result = await coll.insertOne({ _id: 2, x: 22 }); expect(result.insertedId).to.equal(2); }); diff --git a/test/integration/retryable-writes/retryable_writes.spec.test.ts b/test/integration/retryable-writes/retryable_writes.spec.test.ts index e1dcfc4540..05b349eaa5 100644 --- a/test/integration/retryable-writes/retryable_writes.spec.test.ts +++ b/test/integration/retryable-writes/retryable_writes.spec.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; - import * as path from 'path'; + import type { Collection, Db, MongoClient } from '../../../src'; import { loadSpecTests } from '../../spec'; import { legacyRunOnToRunOnRequirement } from '../../tools/spec-runner'; diff --git a/test/integration/transactions/transactions.prose.spec.test.ts b/test/integration/transactions/transactions.prose.spec.test.ts index 1c7e76e62b..02c263cf77 100644 --- a/test/integration/transactions/transactions.prose.spec.test.ts +++ b/test/integration/transactions/transactions.prose.spec.test.ts @@ -7,7 +7,7 @@ const metadata = { } }; -describe.only('Transactions (prose)', metadata, function () { +describe('Transactions (prose)', metadata, function () { const dbName = 'retryable-handshake-tests'; const collName = 'coll'; const docs = [{ _id: 1, x: 11 }]; @@ -22,12 +22,10 @@ describe.only('Transactions (prose)', metadata, function () { }); afterEach(async function () { - await db.admin().command( - { - configureFailPoint: 'failCommand', - mode: 'off' - } - ); + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); await coll.drop(); await client.close(); }); @@ -38,19 +36,17 @@ describe.only('Transactions (prose)', metadata, function () { await coll.insertMany(docs); const session = client.startSession(); session.startTransaction(); - await db.admin().command( - { - configureFailPoint: 'failCommand', - mode: { times: 1 }, - data: { - failCommands: ['saslContinue', 'ping'], - closeConnection: true - } + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['saslContinue', 'ping'], + closeConnection: true } - ); - const result = await coll.insertOne({ _id: 2, x: 22 }); + }); + await coll.insertOne({ _id: 2, x: 22 }); await session.abortTransaction(); - const res = await session.endSession(); + await session.endSession(); const doc = await coll.findOne({ _id: 2 }); expect(doc).to.not.exist; }); @@ -60,17 +56,15 @@ describe.only('Transactions (prose)', metadata, function () { await coll.insertMany(docs); const session = client.startSession(); session.startTransaction(); - await db.admin().command( - { - configureFailPoint: 'failCommand', - mode: { times: 1 }, - data: { - failCommands: ['saslContinue', 'ping'], - closeConnection: true - } + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['saslContinue', 'ping'], + closeConnection: true } - ); - const result = await coll.insertOne({ _id: 2, x: 22 }); + }); + await coll.insertOne({ _id: 2, x: 22 }); await session.commitTransaction(); await session.endSession(); const doc = await coll.findOne({ _id: 2 }); diff --git a/test/integration/transactions/transactions.spec.test.js b/test/integration/transactions/transactions.spec.test.js index 65e1ccc30a..a024e23cd8 100644 --- a/test/integration/transactions/transactions.spec.test.js +++ b/test/integration/transactions/transactions.spec.test.js @@ -87,7 +87,7 @@ const SKIP = [ ]; describe('Transactions Spec Unified Tests', function () { - runUnifiedSuite(loadSpecTests(path.join('transactions', 'unified'))); + runUnifiedSuite(loadSpecTests(path.join('transactions', 'unified')), SKIP); }); const SKIP_TESTS = [ From 0397e2de389bfd0adff401908bed81e44b1a5043 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 15 Mar 2022 13:41:20 +0100 Subject: [PATCH 07/21] test(NODE-3688): fix txn prose test --- .../retryable-reads/retryable_reads.prose.spec.test.ts | 6 +++--- .../retryable_writes.spec.prose.test.ts | 4 ++-- .../transactions/transactions.prose.spec.test.ts | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test/integration/retryable-reads/retryable_reads.prose.spec.test.ts b/test/integration/retryable-reads/retryable_reads.prose.spec.test.ts index 6bf3997318..7c65c7fb06 100644 --- a/test/integration/retryable-reads/retryable_reads.prose.spec.test.ts +++ b/test/integration/retryable-reads/retryable_reads.prose.spec.test.ts @@ -7,7 +7,7 @@ const metadata = { } }; -describe('Retryable Reads (prose)', metadata, function () { +describe.only('Retryable Reads (prose)', metadata, function () { const dbName = 'retryable-handshake-tests'; const collName = 'coll'; const docs = [ @@ -40,7 +40,7 @@ describe('Retryable Reads (prose)', metadata, function () { await coll.insertMany(docs); await db.admin().command({ configureFailPoint: 'failCommand', - mode: { times: 1 }, + mode: { times: 2 }, data: { failCommands: ['saslContinue', 'ping'], closeConnection: true @@ -57,7 +57,7 @@ describe('Retryable Reads (prose)', metadata, function () { await coll.insertMany(docs); await db.admin().command({ configureFailPoint: 'failCommand', - mode: { times: 1 }, + mode: { times: 2 }, data: { failCommands: ['saslContinue', 'ping'], errorCode: 91 // ShutdownInProgress diff --git a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts index b786fd6699..6b4a7295e9 100644 --- a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts +++ b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts @@ -100,7 +100,7 @@ describe('Retryable Writes Spec Prose', () => { await coll.insertMany(docs); await db.admin().command({ configureFailPoint: 'failCommand', - mode: { times: 1 }, + mode: { times: 2 }, data: { failCommands: ['saslContinue', 'ping'], closeConnection: true @@ -117,7 +117,7 @@ describe('Retryable Writes Spec Prose', () => { await coll.insertMany(docs); await db.admin().command({ configureFailPoint: 'failCommand', - mode: { times: 1 }, + mode: { times: 2 }, data: { failCommands: ['saslContinue', 'ping'], errorCode: 91 // ShutdownInProgress diff --git a/test/integration/transactions/transactions.prose.spec.test.ts b/test/integration/transactions/transactions.prose.spec.test.ts index 02c263cf77..63b1fb1ade 100644 --- a/test/integration/transactions/transactions.prose.spec.test.ts +++ b/test/integration/transactions/transactions.prose.spec.test.ts @@ -38,13 +38,13 @@ describe('Transactions (prose)', metadata, function () { session.startTransaction(); await db.admin().command({ configureFailPoint: 'failCommand', - mode: { times: 1 }, + mode: { times: 2 }, data: { failCommands: ['saslContinue', 'ping'], closeConnection: true - } + }, }); - await coll.insertOne({ _id: 2, x: 22 }); + await coll.insertOne({ _id: 2, x: 22 }, { session }); await session.abortTransaction(); await session.endSession(); const doc = await coll.findOne({ _id: 2 }); @@ -58,13 +58,13 @@ describe('Transactions (prose)', metadata, function () { session.startTransaction(); await db.admin().command({ configureFailPoint: 'failCommand', - mode: { times: 1 }, + mode: { times: 2 }, data: { failCommands: ['saslContinue', 'ping'], closeConnection: true } }); - await coll.insertOne({ _id: 2, x: 22 }); + await coll.insertOne({ _id: 2, x: 22 }, { session }); await session.commitTransaction(); await session.endSession(); const doc = await coll.findOne({ _id: 2 }); From 45226195da2737026c965fd3c7a832db5124a5a7 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 15 Mar 2022 13:42:32 +0100 Subject: [PATCH 08/21] fix(NNODE-3688): fix lint errors --- src/cmap/connect.ts | 9 +++++- .../retryable_reads.prose.spec.test.ts | 30 +++++++++++------ .../retryable_writes.spec.prose.test.ts | 28 ++++++++++------ .../transactions.prose.spec.test.ts | 32 ++++++++++++------- 4 files changed, 67 insertions(+), 32 deletions(-) diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index 0520c5d07e..7d8d5d81fd 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -10,6 +10,8 @@ import { LEGACY_HELLO_COMMAND } from '../constants'; import { AnyError, MongoCompatibilityError, + MongoError, + MongoErrorLabel, MongoInvalidArgumentError, MongoNetworkError, MongoNetworkTimeoutError, @@ -182,7 +184,12 @@ function performInitialHandshake( ); } provider.auth(authContext, err => { - if (err) return callback(err); + if (err) { + if (err instanceof MongoError) { + err.addErrorLabel(MongoErrorLabel.RetryableWriteError); + } + return callback(err); + } callback(undefined, conn); }); diff --git a/test/integration/retryable-reads/retryable_reads.prose.spec.test.ts b/test/integration/retryable-reads/retryable_reads.prose.spec.test.ts index 7c65c7fb06..37c4a7d32e 100644 --- a/test/integration/retryable-reads/retryable_reads.prose.spec.test.ts +++ b/test/integration/retryable-reads/retryable_reads.prose.spec.test.ts @@ -1,13 +1,15 @@ import { expect } from 'chai'; +import * as semver from 'semver'; -const metadata = { - requires: { - mongodb: '>=4.2.0', - topology: ['replicaset', 'sharded', 'load-balanced'] - } -}; +import { TopologyType } from '../../../src'; -describe.only('Retryable Reads (prose)', metadata, function () { +const VALID_TOPOLOGIES = [ + TopologyType.ReplicaSetWithPrimary, + TopologyType.Sharded, + TopologyType.LoadBalanced +]; + +describe('Retryable Reads (prose)', function () { const dbName = 'retryable-handshake-tests'; const collName = 'coll'; const docs = [ @@ -20,18 +22,26 @@ describe.only('Retryable Reads (prose)', metadata, function () { let coll; beforeEach(function () { + if ( + semver.lt(this.configuration.buildInfo.version, '4.2.0') || + !VALID_TOPOLOGIES.includes(this.configuration.topologyType) + ) { + this.currentTest.skipReason = + 'Retryable reads tests require MongoDB 4.2 and higher and no standalone'; + this.skip(); + } client = this.configuration.newClient({}); db = client.db(dbName); coll = db.collection(collName); }); afterEach(async function () { - await db.admin().command({ + await db?.admin().command({ configureFailPoint: 'failCommand', mode: 'off' }); - await coll.drop(); - await client.close(); + await coll?.drop(); + await client?.close(); }); context('when the handshake fails with a network error', function () { diff --git a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts index 6b4a7295e9..adc0f8a4a0 100644 --- a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts +++ b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts @@ -1,13 +1,13 @@ import { expect } from 'chai'; +import * as semver from 'semver'; import { MongoError, MongoServerError, TopologyType } from '../../../src'; -const metadata = { - requires: { - mongodb: '>=4.2.0', - topology: ['replicaset', 'sharded', 'load-balanced'] - } -}; +const VALID_TOPOLOGIES = [ + TopologyType.ReplicaSetWithPrimary, + TopologyType.Sharded, + TopologyType.LoadBalanced +]; describe('Retryable Writes Spec Prose', () => { context('when checking against mmapv1', () => { @@ -71,7 +71,7 @@ describe('Retryable Writes Spec Prose', () => { }); }); - context('when errors occur in the handshake', metadata, function () { + context('when errors occur in the handshake', function () { const dbName = 'retryable-handshake-tests'; const collName = 'coll'; const docs = [{ _id: 1, x: 11 }]; @@ -80,18 +80,26 @@ describe('Retryable Writes Spec Prose', () => { let coll; beforeEach(function () { + if ( + semver.lt(this.configuration.buildInfo.version, '4.2.0') || + !VALID_TOPOLOGIES.includes(this.configuration.topologyType) + ) { + this.currentTest.skipReason = + 'Retryable writes tests require MongoDB 4.2 and higher and no standalone'; + this.skip(); + } client = this.configuration.newClient({}); db = client.db(dbName); coll = db.collection(collName); }); afterEach(async function () { - await db.admin().command({ + await db?.admin().command({ configureFailPoint: 'failCommand', mode: 'off' }); - await coll.drop(); - await client.close(); + await coll?.drop(); + await client?.close(); }); context('when the handshake fails with a network error', function () { diff --git a/test/integration/transactions/transactions.prose.spec.test.ts b/test/integration/transactions/transactions.prose.spec.test.ts index 63b1fb1ade..28b793d85d 100644 --- a/test/integration/transactions/transactions.prose.spec.test.ts +++ b/test/integration/transactions/transactions.prose.spec.test.ts @@ -1,13 +1,15 @@ import { expect } from 'chai'; +import * as semver from 'semver'; -const metadata = { - requires: { - mongodb: '>=4.2.0', - topology: ['replicaset', 'sharded', 'load-balanced'] - } -}; +import { TopologyType } from '../../../src'; -describe('Transactions (prose)', metadata, function () { +const VALID_TOPOLOGIES = [ + TopologyType.ReplicaSetWithPrimary, + TopologyType.Sharded, + TopologyType.LoadBalanced +]; + +describe('Transactions (prose)', function () { const dbName = 'retryable-handshake-tests'; const collName = 'coll'; const docs = [{ _id: 1, x: 11 }]; @@ -16,18 +18,26 @@ describe('Transactions (prose)', metadata, function () { let coll; beforeEach(function () { + if ( + semver.lt(this.configuration.buildInfo.version, '4.2.0') || + !VALID_TOPOLOGIES.includes(this.configuration.topologyType) + ) { + this.currentTest.skipReason = + 'Transaction tests require MongoDB 4.2 and higher and no standalone'; + this.skip(); + } client = this.configuration.newClient({}); db = client.db(dbName); coll = db.collection(collName); }); afterEach(async function () { - await db.admin().command({ + await db?.admin().command({ configureFailPoint: 'failCommand', mode: 'off' }); - await coll.drop(); - await client.close(); + await coll?.drop(); + await client?.close(); }); context('when the handshake fails with a network error', function () { @@ -42,7 +52,7 @@ describe('Transactions (prose)', metadata, function () { data: { failCommands: ['saslContinue', 'ping'], closeConnection: true - }, + } }); await coll.insertOne({ _id: 2, x: 22 }, { session }); await session.abortTransaction(); From e9997f25e57e0ac4f0f8ba0caad64b438fc38643 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 17 Mar 2022 14:25:58 +0100 Subject: [PATCH 09/21] test(NODE-3688): reorganise tests --- .../retryable_reads.prose.spec.test.ts | 80 -------- .../retryable_reads.spec.test.js | 92 +++++++++- .../retryable_writes.spec.prose.test.ts | 171 +++++------------- .../retryable_writes.spec.test.ts | 74 ++++++++ .../transactions.prose.spec.test.ts | 84 --------- .../transactions/transactions.spec.test.js | 84 +++++++++ 6 files changed, 294 insertions(+), 291 deletions(-) delete mode 100644 test/integration/retryable-reads/retryable_reads.prose.spec.test.ts delete mode 100644 test/integration/transactions/transactions.prose.spec.test.ts diff --git a/test/integration/retryable-reads/retryable_reads.prose.spec.test.ts b/test/integration/retryable-reads/retryable_reads.prose.spec.test.ts deleted file mode 100644 index 37c4a7d32e..0000000000 --- a/test/integration/retryable-reads/retryable_reads.prose.spec.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { expect } from 'chai'; -import * as semver from 'semver'; - -import { TopologyType } from '../../../src'; - -const VALID_TOPOLOGIES = [ - TopologyType.ReplicaSetWithPrimary, - TopologyType.Sharded, - TopologyType.LoadBalanced -]; - -describe('Retryable Reads (prose)', function () { - const dbName = 'retryable-handshake-tests'; - const collName = 'coll'; - const docs = [ - { _id: 1, x: 11 }, - { _id: 2, x: 22 }, - { _id: 3, x: 33 } - ]; - let client; - let db; - let coll; - - beforeEach(function () { - if ( - semver.lt(this.configuration.buildInfo.version, '4.2.0') || - !VALID_TOPOLOGIES.includes(this.configuration.topologyType) - ) { - this.currentTest.skipReason = - 'Retryable reads tests require MongoDB 4.2 and higher and no standalone'; - this.skip(); - } - client = this.configuration.newClient({}); - db = client.db(dbName); - coll = db.collection(collName); - }); - - afterEach(async function () { - await db?.admin().command({ - configureFailPoint: 'failCommand', - mode: 'off' - }); - await coll?.drop(); - await client?.close(); - }); - - context('when the handshake fails with a network error', function () { - it('retries the read', async function () { - await client.connect(); - await coll.insertMany(docs); - await db.admin().command({ - configureFailPoint: 'failCommand', - mode: { times: 2 }, - data: { - failCommands: ['saslContinue', 'ping'], - closeConnection: true - } - }); - const documents = await coll.find().toArray(); - expect(documents).to.deep.equal(docs); - }); - }); - - context('when the handshake fails with shutdown in progress', function () { - it('retries the read', async function () { - await client.connect(); - await coll.insertMany(docs); - await db.admin().command({ - configureFailPoint: 'failCommand', - mode: { times: 2 }, - data: { - failCommands: ['saslContinue', 'ping'], - errorCode: 91 // ShutdownInProgress - } - }); - const documents = await coll.find().toArray(); - expect(documents).to.deep.equal(docs); - }); - }); -}); diff --git a/test/integration/retryable-reads/retryable_reads.spec.test.js b/test/integration/retryable-reads/retryable_reads.spec.test.js index a5f594fc11..152839d4c6 100644 --- a/test/integration/retryable-reads/retryable_reads.spec.test.js +++ b/test/integration/retryable-reads/retryable_reads.spec.test.js @@ -1,9 +1,16 @@ -'use strict'; - +const { expect } = require('chai'); const path = require('path'); +const semver = require('semver'); const { TestRunnerContext, generateTopologyTests } = require('../../tools/spec-runner'); const { loadSpecTests } = require('../../spec'); const { runUnifiedSuite } = require('../../tools/unified-spec-runner/runner'); +const { TopologyType } = require('../../../src'); + +const VALID_TOPOLOGIES = [ + TopologyType.ReplicaSetWithPrimary, + TopologyType.Sharded, + TopologyType.LoadBalanced +]; describe('Retryable Reads (legacy)', function () { const testContext = new TestRunnerContext(); @@ -34,6 +41,83 @@ describe('Retryable Reads (legacy)', function () { // These tests are skipped because the driver 1) executes a ping when connecting to // an authenticated server and 2) command monitoring is at the connection level so // when the handshake fails no command started event is emitted. -describe.skip('Retryable Reads (unified)', function () { - runUnifiedSuite(loadSpecTests(path.join('retryable-reads', 'unified'))); +const SKIP = [ + 'find succeeds after retryable handshake network error', + 'find succeeds after retryable handshake network error (ShutdownInProgress)' +]; + +describe('Retryable Reads (unified)', function () { + runUnifiedSuite(loadSpecTests(path.join('retryable-reads', 'unified')), SKIP); +}); + +describe('Retryable Reads', function () { + const dbName = 'retryable-handshake-tests'; + const collName = 'coll'; + const docs = [ + { _id: 1, x: 11 }, + { _id: 2, x: 22 }, + { _id: 3, x: 33 } + ]; + let client; + let db; + let coll; + + beforeEach(function () { + if ( + semver.lt(this.configuration.buildInfo.version, '4.2.0') || + !VALID_TOPOLOGIES.includes(this.configuration.topologyType) + ) { + this.currentTest.skipReason = + 'Retryable reads tests require MongoDB 4.2 and higher and no standalone'; + this.skip(); + } + client = this.configuration.newClient({}); + db = client.db(dbName); + coll = db.collection(collName); + }); + + afterEach(async function () { + if (db) { + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); + await coll.drop(); + await client.close(); + } + }); + + context('when the handshake fails with a network error', function () { + it('retries the read', async function () { + await client.connect(); + await coll.insertMany(docs); + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: { times: 2 }, + data: { + failCommands: ['saslContinue', 'ping'], + closeConnection: true + } + }); + const documents = await coll.find().toArray(); + expect(documents).to.deep.equal(docs); + }); + }); + + context('when the handshake fails with shutdown in progress', function () { + it('retries the read', async function () { + await client.connect(); + await coll.insertMany(docs); + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: { times: 2 }, + data: { + failCommands: ['saslContinue', 'ping'], + errorCode: 91 // ShutdownInProgress + } + }); + const documents = await coll.find().toArray(); + expect(documents).to.deep.equal(docs); + }); + }); }); diff --git a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts index adc0f8a4a0..2171885f85 100644 --- a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts +++ b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts @@ -1,139 +1,64 @@ import { expect } from 'chai'; -import * as semver from 'semver'; import { MongoError, MongoServerError, TopologyType } from '../../../src'; -const VALID_TOPOLOGIES = [ - TopologyType.ReplicaSetWithPrimary, - TopologyType.Sharded, - TopologyType.LoadBalanced -]; - describe('Retryable Writes Spec Prose', () => { - context('when checking against mmapv1', () => { - /** - * 1 Test that retryable writes raise an exception when using the MMAPv1 storage engine. - * For this test, execute a write operation, such as insertOne, which should generate an exception and the error code is 20. - * Assert that the error message is the replacement error message: - * - * ``` - * This MongoDB deployment does not support retryable writes. Please add - * retryWrites=false to your connection string. - * ``` - * Note: Drivers that rely on serverStatus to determine the storage engine in use MAY skip this test for sharded clusters, since mongos does not report this information in its serverStatus response. - */ - let client; - - beforeEach(async function () { - if ( - this.configuration.buildInfo.versionArray[0] < 4 || - this.configuration.topologyType !== TopologyType.ReplicaSetWithPrimary - ) { - this.currentTest.skipReason = - 'configureFailPoint only works on server versions greater than 4'; - this.skip(); - } - client = this.configuration.newClient(); - await client.connect(); - }); - - afterEach(async () => { - await client?.close(); - }); - - it('retryable writes raise an exception when using the MMAPv1 storage engine', async () => { - const failPoint = await client.db('admin').command({ - configureFailPoint: 'failCommand', - mode: { times: 1 }, - data: { - failCommands: ['insert'], - errorCode: 20, // MMAP Error code, - closeConnection: false - } - }); + /** + * 1 Test that retryable writes raise an exception when using the MMAPv1 storage engine. + * For this test, execute a write operation, such as insertOne, which should generate an exception and the error code is 20. + * Assert that the error message is the replacement error message: + * + * ``` + * This MongoDB deployment does not support retryable writes. Please add + * retryWrites=false to your connection string. + * ``` + * Note: Drivers that rely on serverStatus to determine the storage engine in use MAY skip this test for sharded clusters, since mongos does not report this information in its serverStatus response. + */ + let client; - expect(failPoint).to.have.property('ok', 1); - - const error = await client - .db('test') - .collection('test') - .insertOne({ a: 1 }) - .catch(error => error); - - expect(error).to.exist; - expect(error).that.is.instanceOf(MongoServerError); - expect(error).to.have.property('originalError').that.instanceOf(MongoError); - expect(error.originalError).to.have.property('code', 20); - expect(error).to.have.property( - 'message', - 'This MongoDB deployment does not support retryable writes. Please add retryWrites=false to your connection string.' - ); - }); + beforeEach(async function () { + if ( + this.configuration.buildInfo.versionArray[0] < 4 || + this.configuration.topologyType !== TopologyType.ReplicaSetWithPrimary + ) { + this.currentTest.skipReason = + 'configureFailPoint only works on server versions greater than 4'; + this.skip(); + } + client = this.configuration.newClient(); + await client.connect(); }); - context('when errors occur in the handshake', function () { - const dbName = 'retryable-handshake-tests'; - const collName = 'coll'; - const docs = [{ _id: 1, x: 11 }]; - let client; - let db; - let coll; + afterEach(async () => { + await client?.close(); + }); - beforeEach(function () { - if ( - semver.lt(this.configuration.buildInfo.version, '4.2.0') || - !VALID_TOPOLOGIES.includes(this.configuration.topologyType) - ) { - this.currentTest.skipReason = - 'Retryable writes tests require MongoDB 4.2 and higher and no standalone'; - this.skip(); + it('retryable writes raise an exception when using the MMAPv1 storage engine', async () => { + const failPoint = await client.db('admin').command({ + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['insert'], + errorCode: 20, // MMAP Error code, + closeConnection: false } - client = this.configuration.newClient({}); - db = client.db(dbName); - coll = db.collection(collName); }); - afterEach(async function () { - await db?.admin().command({ - configureFailPoint: 'failCommand', - mode: 'off' - }); - await coll?.drop(); - await client?.close(); - }); + expect(failPoint).to.have.property('ok', 1); - context('when the handshake fails with a network error', function () { - it('retries the write', async function () { - await client.connect(); - await coll.insertMany(docs); - await db.admin().command({ - configureFailPoint: 'failCommand', - mode: { times: 2 }, - data: { - failCommands: ['saslContinue', 'ping'], - closeConnection: true - } - }); - const result = await coll.insertOne({ _id: 2, x: 22 }); - expect(result.insertedId).to.equal(2); - }); - }); + const error = await client + .db('test') + .collection('test') + .insertOne({ a: 1 }) + .catch(error => error); - context('when the handshake fails with shutdown in progress', function () { - it('retries the write', async function () { - await client.connect(); - await coll.insertMany(docs); - await db.admin().command({ - configureFailPoint: 'failCommand', - mode: { times: 2 }, - data: { - failCommands: ['saslContinue', 'ping'], - errorCode: 91 // ShutdownInProgress - } - }); - const result = await coll.insertOne({ _id: 2, x: 22 }); - expect(result.insertedId).to.equal(2); - }); - }); + expect(error).to.exist; + expect(error).that.is.instanceOf(MongoServerError); + expect(error).to.have.property('originalError').that.instanceOf(MongoError); + expect(error.originalError).to.have.property('code', 20); + expect(error).to.have.property( + 'message', + 'This MongoDB deployment does not support retryable writes. Please add retryWrites=false to your connection string.' + ); }); }); diff --git a/test/integration/retryable-writes/retryable_writes.spec.test.ts b/test/integration/retryable-writes/retryable_writes.spec.test.ts index 05b349eaa5..5f36042912 100644 --- a/test/integration/retryable-writes/retryable_writes.spec.test.ts +++ b/test/integration/retryable-writes/retryable_writes.spec.test.ts @@ -1,12 +1,20 @@ import { expect } from 'chai'; import * as path from 'path'; +import * as semver from 'semver'; import type { Collection, Db, MongoClient } from '../../../src'; +import { TopologyType } from '../../../src'; import { loadSpecTests } from '../../spec'; import { legacyRunOnToRunOnRequirement } from '../../tools/spec-runner'; import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner'; import { isAnyRequirementSatisfied } from '../../tools/unified-spec-runner/unified-utils'; +const VALID_TOPOLOGIES = [ + TopologyType.ReplicaSetWithPrimary, + TopologyType.Sharded, + TopologyType.LoadBalanced +]; + interface RetryableWriteTestContext { client?: MongoClient; db?: Db; @@ -210,3 +218,69 @@ const SKIP = [ describe('Retryable Writes (unified)', function () { runUnifiedSuite(loadSpecTests(path.join('retryable-writes', 'unified')), SKIP); }); + +describe('Retryable Writes', function () { + const dbName = 'retryable-handshake-tests'; + const collName = 'coll'; + const docs = [{ _id: 1, x: 11 }]; + let client; + let db; + let coll; + + beforeEach(function () { + if ( + semver.lt(this.configuration.buildInfo.version, '4.2.0') || + !VALID_TOPOLOGIES.includes(this.configuration.topologyType) + ) { + this.currentTest.skipReason = + 'Retryable writes tests require MongoDB 4.2 and higher and no standalone'; + this.skip(); + } + client = this.configuration.newClient({}); + db = client.db(dbName); + coll = db.collection(collName); + }); + + afterEach(async function () { + await db?.admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); + await coll?.drop(); + await client?.close(); + }); + + context('when the handshake fails with a network error', function () { + it('retries the write', async function () { + await client.connect(); + await coll.insertMany(docs); + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: { times: 2 }, + data: { + failCommands: ['saslContinue', 'ping'], + closeConnection: true + } + }); + const result = await coll.insertOne({ _id: 2, x: 22 }); + expect(result.insertedId).to.equal(2); + }); + }); + + context('when the handshake fails with shutdown in progress', function () { + it('retries the write', async function () { + await client.connect(); + await coll.insertMany(docs); + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: { times: 2 }, + data: { + failCommands: ['saslContinue', 'ping'], + errorCode: 91 // ShutdownInProgress + } + }); + const result = await coll.insertOne({ _id: 2, x: 22 }); + expect(result.insertedId).to.equal(2); + }); + }); +}); diff --git a/test/integration/transactions/transactions.prose.spec.test.ts b/test/integration/transactions/transactions.prose.spec.test.ts deleted file mode 100644 index 28b793d85d..0000000000 --- a/test/integration/transactions/transactions.prose.spec.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { expect } from 'chai'; -import * as semver from 'semver'; - -import { TopologyType } from '../../../src'; - -const VALID_TOPOLOGIES = [ - TopologyType.ReplicaSetWithPrimary, - TopologyType.Sharded, - TopologyType.LoadBalanced -]; - -describe('Transactions (prose)', function () { - const dbName = 'retryable-handshake-tests'; - const collName = 'coll'; - const docs = [{ _id: 1, x: 11 }]; - let client; - let db; - let coll; - - beforeEach(function () { - if ( - semver.lt(this.configuration.buildInfo.version, '4.2.0') || - !VALID_TOPOLOGIES.includes(this.configuration.topologyType) - ) { - this.currentTest.skipReason = - 'Transaction tests require MongoDB 4.2 and higher and no standalone'; - this.skip(); - } - client = this.configuration.newClient({}); - db = client.db(dbName); - coll = db.collection(collName); - }); - - afterEach(async function () { - await db?.admin().command({ - configureFailPoint: 'failCommand', - mode: 'off' - }); - await coll?.drop(); - await client?.close(); - }); - - context('when the handshake fails with a network error', function () { - it('retries the abort', async function () { - await client.connect(); - await coll.insertMany(docs); - const session = client.startSession(); - session.startTransaction(); - await db.admin().command({ - configureFailPoint: 'failCommand', - mode: { times: 2 }, - data: { - failCommands: ['saslContinue', 'ping'], - closeConnection: true - } - }); - await coll.insertOne({ _id: 2, x: 22 }, { session }); - await session.abortTransaction(); - await session.endSession(); - const doc = await coll.findOne({ _id: 2 }); - expect(doc).to.not.exist; - }); - - it('retries the commit', async function () { - await client.connect(); - await coll.insertMany(docs); - const session = client.startSession(); - session.startTransaction(); - await db.admin().command({ - configureFailPoint: 'failCommand', - mode: { times: 2 }, - data: { - failCommands: ['saslContinue', 'ping'], - closeConnection: true - } - }); - await coll.insertOne({ _id: 2, x: 22 }, { session }); - await session.commitTransaction(); - await session.endSession(); - const doc = await coll.findOne({ _id: 2 }); - expect(doc.x).to.equal(22); - }); - }); -}); diff --git a/test/integration/transactions/transactions.spec.test.js b/test/integration/transactions/transactions.spec.test.js index a024e23cd8..552258de86 100644 --- a/test/integration/transactions/transactions.spec.test.js +++ b/test/integration/transactions/transactions.spec.test.js @@ -1,10 +1,18 @@ 'use strict'; const path = require('path'); +const semver = require('semver'); const { expect } = require('chai'); const { TestRunnerContext, generateTopologyTests } = require('../../tools/spec-runner'); const { runUnifiedSuite } = require('../../tools/unified-spec-runner/runner'); const { loadSpecTests } = require('../../spec'); +const { TopologyType } = require('../../../src'); + +const VALID_TOPOLOGIES = [ + TopologyType.ReplicaSetWithPrimary, + TopologyType.Sharded, + TopologyType.LoadBalanced +]; function ignoreNsNotFoundForListIndexes(err) { if (err.code !== 26) { @@ -127,3 +135,79 @@ describe('Transactions Spec Legacy Tests', function () { generateTopologyTests(testSuites, testContext, testFilter); }); + +describe('Transactions', function () { + const dbName = 'retryable-handshake-tests'; + const collName = 'coll'; + const docs = [{ _id: 1, x: 11 }]; + let client; + let db; + let coll; + + beforeEach(function () { + if ( + semver.lt(this.configuration.buildInfo.version, '4.2.0') || + !VALID_TOPOLOGIES.includes(this.configuration.topologyType) + ) { + this.currentTest.skipReason = + 'Transaction tests require MongoDB 4.2 and higher and no standalone'; + this.skip(); + } + client = this.configuration.newClient({}); + db = client.db(dbName); + coll = db.collection(collName); + }); + + afterEach(async function () { + if (db) { + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); + await coll.drop(); + await client.close(); + } + }); + + context('when the handshake fails with a network error', function () { + it('retries the abort', async function () { + await client.connect(); + await coll.insertMany(docs); + const session = client.startSession(); + session.startTransaction(); + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: { times: 2 }, + data: { + failCommands: ['saslContinue', 'ping'], + closeConnection: true + } + }); + await coll.insertOne({ _id: 2, x: 22 }, { session }); + await session.abortTransaction(); + await session.endSession(); + const doc = await coll.findOne({ _id: 2 }); + expect(doc).to.not.exist; + }); + + it('retries the commit', async function () { + await client.connect(); + await coll.insertMany(docs); + const session = client.startSession(); + session.startTransaction(); + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: { times: 2 }, + data: { + failCommands: ['saslContinue', 'ping'], + closeConnection: true + } + }); + await coll.insertOne({ _id: 2, x: 22 }, { session }); + await session.commitTransaction(); + await session.endSession(); + const doc = await coll.findOne({ _id: 2 }); + expect(doc.x).to.equal(22); + }); + }); +}); From 39f94cfda74fea26a2a6c1190940ab3a6ff2930c Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 18 Mar 2022 12:59:58 +0100 Subject: [PATCH 10/21] test(NODE-3688): reorganise tests, add comments --- .../retryable_reads.spec.test.js | 23 ++++++++------- .../retryable_writes.spec.test.ts | 17 ++++++----- .../transactions/transactions.spec.test.js | 28 +++++++++---------- 3 files changed, 37 insertions(+), 31 deletions(-) diff --git a/test/integration/retryable-reads/retryable_reads.spec.test.js b/test/integration/retryable-reads/retryable_reads.spec.test.js index 152839d4c6..7025cde153 100644 --- a/test/integration/retryable-reads/retryable_reads.spec.test.js +++ b/test/integration/retryable-reads/retryable_reads.spec.test.js @@ -50,7 +50,7 @@ describe('Retryable Reads (unified)', function () { runUnifiedSuite(loadSpecTests(path.join('retryable-reads', 'unified')), SKIP); }); -describe('Retryable Reads', function () { +describe('Retryable Reads Spec Manual Tests', function () { const dbName = 'retryable-handshake-tests'; const collName = 'coll'; const docs = [ @@ -62,18 +62,22 @@ describe('Retryable Reads', function () { let db; let coll; - beforeEach(function () { + beforeEach(async function () { if ( semver.lt(this.configuration.buildInfo.version, '4.2.0') || - !VALID_TOPOLOGIES.includes(this.configuration.topologyType) + !VALID_TOPOLOGIES.includes(this.configuration.topologyType) || + !this.configuration.options.auth || + !!process.env.SERVERLESS ) { this.currentTest.skipReason = - 'Retryable reads tests require MongoDB 4.2 and higher and no standalone'; + 'Retryable reads tests requires authenticated MongoDB 4.2 and higher and no standalone'; this.skip(); } client = this.configuration.newClient({}); db = client.db(dbName); coll = db.collection(collName); + await client.connect(); + await coll.insertMany(docs); }); afterEach(async function () { @@ -88,9 +92,8 @@ describe('Retryable Reads', function () { }); context('when the handshake fails with a network error', function () { + // Manual implementation for: 'find succeeds after retryable handshake network error' it('retries the read', async function () { - await client.connect(); - await coll.insertMany(docs); await db.admin().command({ configureFailPoint: 'failCommand', mode: { times: 2 }, @@ -99,15 +102,15 @@ describe('Retryable Reads', function () { closeConnection: true } }); - const documents = await coll.find().toArray(); - expect(documents).to.deep.equal(docs); + const doc = await coll.findOne({ _id: 2 }); + expect(doc).to.deep.equal(docs[1]); }); }); context('when the handshake fails with shutdown in progress', function () { + // Manual implementation for: + // 'find succeeds after retryable handshake network error (ShutdownInProgress)' it('retries the read', async function () { - await client.connect(); - await coll.insertMany(docs); await db.admin().command({ configureFailPoint: 'failCommand', mode: { times: 2 }, diff --git a/test/integration/retryable-writes/retryable_writes.spec.test.ts b/test/integration/retryable-writes/retryable_writes.spec.test.ts index 5f36042912..c0d6624c05 100644 --- a/test/integration/retryable-writes/retryable_writes.spec.test.ts +++ b/test/integration/retryable-writes/retryable_writes.spec.test.ts @@ -219,7 +219,7 @@ describe('Retryable Writes (unified)', function () { runUnifiedSuite(loadSpecTests(path.join('retryable-writes', 'unified')), SKIP); }); -describe('Retryable Writes', function () { +describe('Retryable Writes Spec Manual Tests', function () { const dbName = 'retryable-handshake-tests'; const collName = 'coll'; const docs = [{ _id: 1, x: 11 }]; @@ -227,10 +227,12 @@ describe('Retryable Writes', function () { let db; let coll; - beforeEach(function () { + beforeEach(async function () { if ( semver.lt(this.configuration.buildInfo.version, '4.2.0') || - !VALID_TOPOLOGIES.includes(this.configuration.topologyType) + !VALID_TOPOLOGIES.includes(this.configuration.topologyType) || + !this.configuration.options.auth || + !!process.env.SERVERLESS ) { this.currentTest.skipReason = 'Retryable writes tests require MongoDB 4.2 and higher and no standalone'; @@ -239,6 +241,8 @@ describe('Retryable Writes', function () { client = this.configuration.newClient({}); db = client.db(dbName); coll = db.collection(collName); + await client.connect(); + await coll.insertMany(docs); }); afterEach(async function () { @@ -251,9 +255,9 @@ describe('Retryable Writes', function () { }); context('when the handshake fails with a network error', function () { + // Manual implementation for: + // 'InsertOne succeeds after retryable handshake error ShutdownInProgress' it('retries the write', async function () { - await client.connect(); - await coll.insertMany(docs); await db.admin().command({ configureFailPoint: 'failCommand', mode: { times: 2 }, @@ -268,9 +272,8 @@ describe('Retryable Writes', function () { }); context('when the handshake fails with shutdown in progress', function () { + // Manual implementation for: 'InsertOne succeeds after retryable handshake error' it('retries the write', async function () { - await client.connect(); - await coll.insertMany(docs); await db.admin().command({ configureFailPoint: 'failCommand', mode: { times: 2 }, diff --git a/test/integration/transactions/transactions.spec.test.js b/test/integration/transactions/transactions.spec.test.js index 552258de86..5aeb0c75f3 100644 --- a/test/integration/transactions/transactions.spec.test.js +++ b/test/integration/transactions/transactions.spec.test.js @@ -136,26 +136,34 @@ describe('Transactions Spec Legacy Tests', function () { generateTopologyTests(testSuites, testContext, testFilter); }); -describe('Transactions', function () { +describe('Transactions Spec Manual Tests', function () { const dbName = 'retryable-handshake-tests'; const collName = 'coll'; const docs = [{ _id: 1, x: 11 }]; let client; let db; let coll; + let session; - beforeEach(function () { + beforeEach(async function () { if ( semver.lt(this.configuration.buildInfo.version, '4.2.0') || - !VALID_TOPOLOGIES.includes(this.configuration.topologyType) + !VALID_TOPOLOGIES.includes(this.configuration.topologyType) || + !this.configuration.options.auth || + !!process.env.SERVERLESS ) { this.currentTest.skipReason = - 'Transaction tests require MongoDB 4.2 and higher and no standalone'; + 'Transaction tests require authenticated MongoDB 4.2 and higher and no standalone'; this.skip(); } client = this.configuration.newClient({}); db = client.db(dbName); coll = db.collection(collName); + await client.connect(); + await coll.insertMany(docs); + session = client.startSession(); + session.startTransaction(); + await coll.insertOne({ _id: 2, x: 22 }, { session }); }); afterEach(async function () { @@ -170,11 +178,8 @@ describe('Transactions', function () { }); context('when the handshake fails with a network error', function () { + // Manual implementation for: 'AbortTransaction succeeds after handshake network error' it('retries the abort', async function () { - await client.connect(); - await coll.insertMany(docs); - const session = client.startSession(); - session.startTransaction(); await db.admin().command({ configureFailPoint: 'failCommand', mode: { times: 2 }, @@ -183,18 +188,14 @@ describe('Transactions', function () { closeConnection: true } }); - await coll.insertOne({ _id: 2, x: 22 }, { session }); await session.abortTransaction(); await session.endSession(); const doc = await coll.findOne({ _id: 2 }); expect(doc).to.not.exist; }); + // Manual implementation for: 'CommitTransaction succeeds after handshake network error' it('retries the commit', async function () { - await client.connect(); - await coll.insertMany(docs); - const session = client.startSession(); - session.startTransaction(); await db.admin().command({ configureFailPoint: 'failCommand', mode: { times: 2 }, @@ -203,7 +204,6 @@ describe('Transactions', function () { closeConnection: true } }); - await coll.insertOne({ _id: 2, x: 22 }, { session }); await session.commitTransaction(); await session.endSession(); const doc = await coll.findOne({ _id: 2 }); From f0f21c2e198eacdca57196c2e254e9a89a6133eb Mon Sep 17 00:00:00 2001 From: Daria Pardue Date: Tue, 22 Mar 2022 16:42:34 -0400 Subject: [PATCH 11/21] test: fixup transaction test requirements --- .../transactions/transactions.spec.test.js | 85 +++++++++---------- 1 file changed, 40 insertions(+), 45 deletions(-) diff --git a/test/integration/transactions/transactions.spec.test.js b/test/integration/transactions/transactions.spec.test.js index 5aeb0c75f3..89416f622e 100644 --- a/test/integration/transactions/transactions.spec.test.js +++ b/test/integration/transactions/transactions.spec.test.js @@ -1,18 +1,10 @@ 'use strict'; const path = require('path'); -const semver = require('semver'); const { expect } = require('chai'); const { TestRunnerContext, generateTopologyTests } = require('../../tools/spec-runner'); const { runUnifiedSuite } = require('../../tools/unified-spec-runner/runner'); const { loadSpecTests } = require('../../spec'); -const { TopologyType } = require('../../../src'); - -const VALID_TOPOLOGIES = [ - TopologyType.ReplicaSetWithPrimary, - TopologyType.Sharded, - TopologyType.LoadBalanced -]; function ignoreNsNotFoundForListIndexes(err) { if (err.code !== 26) { @@ -89,6 +81,7 @@ class TransactionsRunnerContext extends TestRunnerContext { // These tests are skipped because the driver 1) executes a ping when connecting to // an authenticated server and 2) command monitoring is at the connection level so // when the handshake fails no command started event is emitted. +// NOTE: these tests are skipped in the spec itself due to DRIVERS-2032 (unrelated to the above) const SKIP = [ 'AbortTransaction succeeds after handshake network error', 'CommitTransaction succeeds after handshake network error' @@ -137,49 +130,52 @@ describe('Transactions Spec Legacy Tests', function () { }); describe('Transactions Spec Manual Tests', function () { - const dbName = 'retryable-handshake-tests'; - const collName = 'coll'; - const docs = [{ _id: 1, x: 11 }]; - let client; - let db; - let coll; - let session; - - beforeEach(async function () { - if ( - semver.lt(this.configuration.buildInfo.version, '4.2.0') || - !VALID_TOPOLOGIES.includes(this.configuration.topologyType) || - !this.configuration.options.auth || - !!process.env.SERVERLESS - ) { - this.currentTest.skipReason = - 'Transaction tests require authenticated MongoDB 4.2 and higher and no standalone'; - this.skip(); - } - client = this.configuration.newClient({}); - db = client.db(dbName); - coll = db.collection(collName); - await client.connect(); - await coll.insertMany(docs); - session = client.startSession(); - session.startTransaction(); - await coll.insertOne({ _id: 2, x: 22 }, { session }); - }); + context('when the handshake fails with a network error', function () { + const metadata = { + requires: { + mongodb: '>=4.2.0', + auth: 'enabled', + topology: '!single' + } + }; + + const dbName = 'retryable-handshake-tests'; + const collName = 'coll'; + const docs = [{ _id: 1, x: 11 }]; + let client; + let db; + let coll; + let session; + + beforeEach(async function () { + if (process.env.SERVERLESS) { + this.currentTest.skipReason = 'Transaction tests cannot run against serverless'; + this.skip(); + } + client = this.configuration.newClient({}); + db = client.db(dbName); + coll = db.collection(collName); + await client.connect(); + await coll.insertMany(docs); + session = client.startSession(); + session.startTransaction(); + await coll.insertOne({ _id: 2, x: 22 }, { session }); + }); + + afterEach(async function () { + await session.endSession(); - afterEach(async function () { - if (db) { await db.admin().command({ configureFailPoint: 'failCommand', mode: 'off' }); await coll.drop(); await client.close(); - } - }); + }); - context('when the handshake fails with a network error', function () { // Manual implementation for: 'AbortTransaction succeeds after handshake network error' - it('retries the abort', async function () { + // NOTE: tests are skipped in the spec itself due to DRIVERS-2032 (unrelated to our reasons) + it('retries the abort', metadata, async function () { await db.admin().command({ configureFailPoint: 'failCommand', mode: { times: 2 }, @@ -189,13 +185,13 @@ describe('Transactions Spec Manual Tests', function () { } }); await session.abortTransaction(); - await session.endSession(); const doc = await coll.findOne({ _id: 2 }); expect(doc).to.not.exist; }); // Manual implementation for: 'CommitTransaction succeeds after handshake network error' - it('retries the commit', async function () { + // NOTE: tests are skipped in the spec itself due to DRIVERS-2032 (unrelated to our reasons) + it('retries the commit', metadata, async function () { await db.admin().command({ configureFailPoint: 'failCommand', mode: { times: 2 }, @@ -205,7 +201,6 @@ describe('Transactions Spec Manual Tests', function () { } }); await session.commitTransaction(); - await session.endSession(); const doc = await coll.findOne({ _id: 2 }); expect(doc.x).to.equal(22); }); From a02e8051fd79d8160a676215248f32205d9a36ab Mon Sep 17 00:00:00 2001 From: Daria Pardue Date: Tue, 22 Mar 2022 16:55:34 -0400 Subject: [PATCH 12/21] test: fixup retryable reads spec tests --- .../retryable_reads.spec.test.js | 120 ++++++++---------- 1 file changed, 55 insertions(+), 65 deletions(-) diff --git a/test/integration/retryable-reads/retryable_reads.spec.test.js b/test/integration/retryable-reads/retryable_reads.spec.test.js index 7025cde153..e018ba67a8 100644 --- a/test/integration/retryable-reads/retryable_reads.spec.test.js +++ b/test/integration/retryable-reads/retryable_reads.spec.test.js @@ -1,16 +1,8 @@ const { expect } = require('chai'); const path = require('path'); -const semver = require('semver'); const { TestRunnerContext, generateTopologyTests } = require('../../tools/spec-runner'); const { loadSpecTests } = require('../../spec'); const { runUnifiedSuite } = require('../../tools/unified-spec-runner/runner'); -const { TopologyType } = require('../../../src'); - -const VALID_TOPOLOGIES = [ - TopologyType.ReplicaSetWithPrimary, - TopologyType.Sharded, - TopologyType.LoadBalanced -]; describe('Retryable Reads (legacy)', function () { const testContext = new TestRunnerContext(); @@ -51,76 +43,74 @@ describe('Retryable Reads (unified)', function () { }); describe('Retryable Reads Spec Manual Tests', function () { - const dbName = 'retryable-handshake-tests'; - const collName = 'coll'; - const docs = [ - { _id: 1, x: 11 }, - { _id: 2, x: 22 }, - { _id: 3, x: 33 } - ]; - let client; - let db; - let coll; + context('retryable reads handshake failures', function () { + const metadata = { + requires: { + mongodb: '>=4.2.0', + auth: 'enabled', + topology: '!single' + } + }; - beforeEach(async function () { - if ( - semver.lt(this.configuration.buildInfo.version, '4.2.0') || - !VALID_TOPOLOGIES.includes(this.configuration.topologyType) || - !this.configuration.options.auth || - !!process.env.SERVERLESS - ) { - this.currentTest.skipReason = - 'Retryable reads tests requires authenticated MongoDB 4.2 and higher and no standalone'; - this.skip(); - } - client = this.configuration.newClient({}); - db = client.db(dbName); - coll = db.collection(collName); - await client.connect(); - await coll.insertMany(docs); - }); + const dbName = 'retryable-handshake-tests'; + const collName = 'coll'; + const docs = [ + { _id: 1, x: 11 }, + { _id: 2, x: 22 }, + { _id: 3, x: 33 } + ]; + let client; + let db; + let coll; - afterEach(async function () { - if (db) { + beforeEach(async function () { + client = this.configuration.newClient({}); + db = client.db(dbName); + coll = db.collection(collName); + await client.connect(); + await coll.insertMany(docs); + }); + + afterEach(async function () { await db.admin().command({ configureFailPoint: 'failCommand', mode: 'off' }); await coll.drop(); await client.close(); - } - }); + }); - context('when the handshake fails with a network error', function () { - // Manual implementation for: 'find succeeds after retryable handshake network error' - it('retries the read', async function () { - await db.admin().command({ - configureFailPoint: 'failCommand', - mode: { times: 2 }, - data: { - failCommands: ['saslContinue', 'ping'], - closeConnection: true - } + context('when the handshake fails with a network error', function () { + // Manual implementation for: 'find succeeds after retryable handshake network error' + it('retries the read', metadata, async function () { + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: { times: 2 }, + data: { + failCommands: ['saslContinue', 'ping'], + closeConnection: true + } + }); + const doc = await coll.find({ _id: 2 }).toArray(); + expect(doc).to.deep.equal(docs[1]); }); - const doc = await coll.findOne({ _id: 2 }); - expect(doc).to.deep.equal(docs[1]); }); - }); - context('when the handshake fails with shutdown in progress', function () { - // Manual implementation for: - // 'find succeeds after retryable handshake network error (ShutdownInProgress)' - it('retries the read', async function () { - await db.admin().command({ - configureFailPoint: 'failCommand', - mode: { times: 2 }, - data: { - failCommands: ['saslContinue', 'ping'], - errorCode: 91 // ShutdownInProgress - } + context('when the handshake fails with shutdown in progress', function () { + // Manual implementation for: + // 'find succeeds after retryable handshake network error (ShutdownInProgress)' + it('retries the read', metadata, async function () { + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: { times: 2 }, + data: { + failCommands: ['saslContinue', 'ping'], + errorCode: 91 // ShutdownInProgress + } + }); + const doc = await coll.find({ _id: 2 }).toArray(); + expect(doc).to.deep.equal(docs[1]); }); - const documents = await coll.find().toArray(); - expect(documents).to.deep.equal(docs); }); }); }); From 87b3906b77d346a081dc8d1d51cf63b2fc402939 Mon Sep 17 00:00:00 2001 From: Daria Pardue Date: Tue, 22 Mar 2022 17:22:02 -0400 Subject: [PATCH 13/21] test: fixup retryable writes spec tests --- .../retryable_writes.spec.test.ts | 120 ++++++++---------- 1 file changed, 56 insertions(+), 64 deletions(-) diff --git a/test/integration/retryable-writes/retryable_writes.spec.test.ts b/test/integration/retryable-writes/retryable_writes.spec.test.ts index c0d6624c05..51d743d015 100644 --- a/test/integration/retryable-writes/retryable_writes.spec.test.ts +++ b/test/integration/retryable-writes/retryable_writes.spec.test.ts @@ -1,20 +1,12 @@ import { expect } from 'chai'; import * as path from 'path'; -import * as semver from 'semver'; import type { Collection, Db, MongoClient } from '../../../src'; -import { TopologyType } from '../../../src'; import { loadSpecTests } from '../../spec'; import { legacyRunOnToRunOnRequirement } from '../../tools/spec-runner'; import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner'; import { isAnyRequirementSatisfied } from '../../tools/unified-spec-runner/unified-utils'; -const VALID_TOPOLOGIES = [ - TopologyType.ReplicaSetWithPrimary, - TopologyType.Sharded, - TopologyType.LoadBalanced -]; - interface RetryableWriteTestContext { client?: MongoClient; db?: Db; @@ -220,70 +212,70 @@ describe('Retryable Writes (unified)', function () { }); describe('Retryable Writes Spec Manual Tests', function () { - const dbName = 'retryable-handshake-tests'; - const collName = 'coll'; - const docs = [{ _id: 1, x: 11 }]; - let client; - let db; - let coll; - - beforeEach(async function () { - if ( - semver.lt(this.configuration.buildInfo.version, '4.2.0') || - !VALID_TOPOLOGIES.includes(this.configuration.topologyType) || - !this.configuration.options.auth || - !!process.env.SERVERLESS - ) { - this.currentTest.skipReason = - 'Retryable writes tests require MongoDB 4.2 and higher and no standalone'; - this.skip(); - } - client = this.configuration.newClient({}); - db = client.db(dbName); - coll = db.collection(collName); - await client.connect(); - await coll.insertMany(docs); - }); - - afterEach(async function () { - await db?.admin().command({ - configureFailPoint: 'failCommand', - mode: 'off' + context('retryable writes handshake failures', function () { + const metadata = { + requires: { + mongodb: '>=4.2.0', + auth: 'enabled', + topology: '!single' + } + }; + + const dbName = 'retryable-handshake-tests'; + const collName = 'coll'; + const docs = [{ _id: 1, x: 11 }]; + let client; + let db; + let coll; + + beforeEach(async function () { + client = this.configuration.newClient({}); + db = client.db(dbName); + coll = db.collection(collName); + await client.connect(); + await coll.insertMany(docs); }); - await coll?.drop(); - await client?.close(); - }); - context('when the handshake fails with a network error', function () { - // Manual implementation for: - // 'InsertOne succeeds after retryable handshake error ShutdownInProgress' - it('retries the write', async function () { + afterEach(async function () { await db.admin().command({ configureFailPoint: 'failCommand', - mode: { times: 2 }, - data: { - failCommands: ['saslContinue', 'ping'], - closeConnection: true - } + mode: 'off' }); - const result = await coll.insertOne({ _id: 2, x: 22 }); - expect(result.insertedId).to.equal(2); + await coll.drop(); + await client.close(); }); - }); - context('when the handshake fails with shutdown in progress', function () { - // Manual implementation for: 'InsertOne succeeds after retryable handshake error' - it('retries the write', async function () { - await db.admin().command({ - configureFailPoint: 'failCommand', - mode: { times: 2 }, - data: { - failCommands: ['saslContinue', 'ping'], - errorCode: 91 // ShutdownInProgress - } + context('when the handshake fails with a network error', function () { + // Manual implementation for: 'InsertOne succeeds after retryable handshake error' + (it as any)('retries the write', metadata, async function () { + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: { times: 2 }, + data: { + failCommands: ['saslContinue', 'ping'], + closeConnection: true + } + }); + const result = await coll.insertOne({ _id: 2, x: 22 }); + expect(result.insertedId).to.equal(2); + }); + }); + + context('when the handshake fails with shutdown in progress', function () { + // Manual implementation for: + // 'InsertOne succeeds after retryable handshake error ShutdownInProgress' + (it as any)('retries the write', metadata, async function () { + await db.admin().command({ + configureFailPoint: 'failCommand', + mode: { times: 2 }, + data: { + failCommands: ['saslContinue', 'ping'], + errorCode: 91 // ShutdownInProgress + } + }); + const result = await coll.insertOne({ _id: 2, x: 22 }); + expect(result.insertedId).to.equal(2); }); - const result = await coll.insertOne({ _id: 2, x: 22 }); - expect(result.insertedId).to.equal(2); }); }); }); From 804299ee4cfa5466fe7afe87247de65c53e92364 Mon Sep 17 00:00:00 2001 From: Daria Pardue Date: Tue, 22 Mar 2022 17:49:54 -0400 Subject: [PATCH 14/21] test: fix assertion format --- .../retryable-reads/retryable_reads.spec.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/integration/retryable-reads/retryable_reads.spec.test.js b/test/integration/retryable-reads/retryable_reads.spec.test.js index e018ba67a8..426de35b13 100644 --- a/test/integration/retryable-reads/retryable_reads.spec.test.js +++ b/test/integration/retryable-reads/retryable_reads.spec.test.js @@ -91,8 +91,8 @@ describe('Retryable Reads Spec Manual Tests', function () { closeConnection: true } }); - const doc = await coll.find({ _id: 2 }).toArray(); - expect(doc).to.deep.equal(docs[1]); + const docs = await coll.find({ _id: 2 }).toArray(); + expect(docs).to.deep.equal([docs[1]]); }); }); @@ -108,8 +108,8 @@ describe('Retryable Reads Spec Manual Tests', function () { errorCode: 91 // ShutdownInProgress } }); - const doc = await coll.find({ _id: 2 }).toArray(); - expect(doc).to.deep.equal(docs[1]); + const docs = await coll.find({ _id: 2 }).toArray(); + expect(docs).to.deep.equal([docs[1]]); }); }); }); From 8a06a647cff3dd623d65020bf630f7d09e494d78 Mon Sep 17 00:00:00 2001 From: Daria Pardue Date: Tue, 22 Mar 2022 18:18:29 -0400 Subject: [PATCH 15/21] test: fix accidentally shadowed var --- .../retryable-reads/retryable_reads.spec.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/integration/retryable-reads/retryable_reads.spec.test.js b/test/integration/retryable-reads/retryable_reads.spec.test.js index 426de35b13..ea212f9776 100644 --- a/test/integration/retryable-reads/retryable_reads.spec.test.js +++ b/test/integration/retryable-reads/retryable_reads.spec.test.js @@ -54,7 +54,7 @@ describe('Retryable Reads Spec Manual Tests', function () { const dbName = 'retryable-handshake-tests'; const collName = 'coll'; - const docs = [ + const inputDocs = [ { _id: 1, x: 11 }, { _id: 2, x: 22 }, { _id: 3, x: 33 } @@ -68,7 +68,7 @@ describe('Retryable Reads Spec Manual Tests', function () { db = client.db(dbName); coll = db.collection(collName); await client.connect(); - await coll.insertMany(docs); + await coll.insertMany(inputDocs); }); afterEach(async function () { @@ -91,8 +91,8 @@ describe('Retryable Reads Spec Manual Tests', function () { closeConnection: true } }); - const docs = await coll.find({ _id: 2 }).toArray(); - expect(docs).to.deep.equal([docs[1]]); + const documents = await coll.find({ _id: 2 }).toArray(); + expect(documents).to.deep.equal([inputDocs[1]]); }); }); @@ -108,8 +108,8 @@ describe('Retryable Reads Spec Manual Tests', function () { errorCode: 91 // ShutdownInProgress } }); - const docs = await coll.find({ _id: 2 }).toArray(); - expect(docs).to.deep.equal([docs[1]]); + const documents = await coll.find({ _id: 2 }).toArray(); + expect(documents).to.deep.equal([inputDocs[1]]); }); }); }); From 52d9cf1f30cb0622a82987c7212124032cb93804 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 31 Mar 2022 16:37:57 +0200 Subject: [PATCH 16/21] test(NODE-3688): add cmap ignore ping events --- src/cmap/connect.ts | 8 +- src/error.ts | 15 ++-- .../retryable_reads.spec.test.js | 84 +---------------- .../retryable_writes.spec.test.ts | 79 +--------------- .../transactions/transactions.spec.test.js | 89 +------------------ .../unified/handshakeError.json | 5 +- .../unified/handshakeError.yml | 2 +- test/tools/unified-spec-runner/runner.ts | 11 +++ test/unit/error.test.ts | 2 +- 9 files changed, 32 insertions(+), 263 deletions(-) diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index 7d8d5d81fd..d15aa63d36 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -16,7 +16,8 @@ import { MongoNetworkError, MongoNetworkTimeoutError, MongoRuntimeError, - MongoServerError + MongoServerError, + needsRetryableWriteLabel } from '../error'; import { Callback, ClientMetadata, HostAddress, makeClientMetadata, ns } from '../utils'; import { AuthContext, AuthProvider } from './auth/auth_provider'; @@ -185,7 +186,10 @@ function performInitialHandshake( } provider.auth(authContext, err => { if (err) { - if (err instanceof MongoError) { + if ( + err instanceof MongoError && + needsRetryableWriteLabel(err, response.maxWireVersion) + ) { err.addErrorLabel(MongoErrorLabel.RetryableWriteError); } return callback(err); diff --git a/src/error.ts b/src/error.ts index 5ea75e84ee..5f4d3f3773 100644 --- a/src/error.ts +++ b/src/error.ts @@ -744,21 +744,18 @@ const RETRYABLE_WRITE_ERROR_CODES = new Set([ ]); export function needsRetryableWriteLabel(error: Error, maxWireVersion: number): boolean { - if (maxWireVersion >= 9) { - // 4.4+ servers attach their own retryable write error - return false; - } // pre-4.4 server, then the driver adds an error label for every valid case // execute operation will only inspect the label, code/message logic is handled here - if (error instanceof MongoNetworkError) { return true; } - if (error instanceof MongoError && error.hasErrorLabel(MongoErrorLabel.RetryableWriteError)) { - // Before 4.4 the error label can be one way of identifying retry - // so we can return true if we have the label, but fall back to code checking below - return true; + if ( + maxWireVersion >= 9 || + (error instanceof MongoError && error.hasErrorLabel(MongoErrorLabel.RetryableWriteError)) + ) { + // If we already have the error label no need to add it again. 4.4+ servers add the label. + return false; } if (error instanceof MongoWriteConcernError) { diff --git a/test/integration/retryable-reads/retryable_reads.spec.test.js b/test/integration/retryable-reads/retryable_reads.spec.test.js index ea212f9776..24f231df0e 100644 --- a/test/integration/retryable-reads/retryable_reads.spec.test.js +++ b/test/integration/retryable-reads/retryable_reads.spec.test.js @@ -1,4 +1,3 @@ -const { expect } = require('chai'); const path = require('path'); const { TestRunnerContext, generateTopologyTests } = require('../../tools/spec-runner'); const { loadSpecTests } = require('../../spec'); @@ -30,87 +29,6 @@ describe('Retryable Reads (legacy)', function () { }); }); -// These tests are skipped because the driver 1) executes a ping when connecting to -// an authenticated server and 2) command monitoring is at the connection level so -// when the handshake fails no command started event is emitted. -const SKIP = [ - 'find succeeds after retryable handshake network error', - 'find succeeds after retryable handshake network error (ShutdownInProgress)' -]; - describe('Retryable Reads (unified)', function () { - runUnifiedSuite(loadSpecTests(path.join('retryable-reads', 'unified')), SKIP); -}); - -describe('Retryable Reads Spec Manual Tests', function () { - context('retryable reads handshake failures', function () { - const metadata = { - requires: { - mongodb: '>=4.2.0', - auth: 'enabled', - topology: '!single' - } - }; - - const dbName = 'retryable-handshake-tests'; - const collName = 'coll'; - const inputDocs = [ - { _id: 1, x: 11 }, - { _id: 2, x: 22 }, - { _id: 3, x: 33 } - ]; - let client; - let db; - let coll; - - beforeEach(async function () { - client = this.configuration.newClient({}); - db = client.db(dbName); - coll = db.collection(collName); - await client.connect(); - await coll.insertMany(inputDocs); - }); - - afterEach(async function () { - await db.admin().command({ - configureFailPoint: 'failCommand', - mode: 'off' - }); - await coll.drop(); - await client.close(); - }); - - context('when the handshake fails with a network error', function () { - // Manual implementation for: 'find succeeds after retryable handshake network error' - it('retries the read', metadata, async function () { - await db.admin().command({ - configureFailPoint: 'failCommand', - mode: { times: 2 }, - data: { - failCommands: ['saslContinue', 'ping'], - closeConnection: true - } - }); - const documents = await coll.find({ _id: 2 }).toArray(); - expect(documents).to.deep.equal([inputDocs[1]]); - }); - }); - - context('when the handshake fails with shutdown in progress', function () { - // Manual implementation for: - // 'find succeeds after retryable handshake network error (ShutdownInProgress)' - it('retries the read', metadata, async function () { - await db.admin().command({ - configureFailPoint: 'failCommand', - mode: { times: 2 }, - data: { - failCommands: ['saslContinue', 'ping'], - errorCode: 91 // ShutdownInProgress - } - }); - const documents = await coll.find({ _id: 2 }).toArray(); - expect(documents).to.deep.equal([inputDocs[1]]); - }); - }); - }); + runUnifiedSuite(loadSpecTests(path.join('retryable-reads', 'unified'))); }); diff --git a/test/integration/retryable-writes/retryable_writes.spec.test.ts b/test/integration/retryable-writes/retryable_writes.spec.test.ts index 51d743d015..de506b79cf 100644 --- a/test/integration/retryable-writes/retryable_writes.spec.test.ts +++ b/test/integration/retryable-writes/retryable_writes.spec.test.ts @@ -199,83 +199,6 @@ async function turnOffFailPoint(client, name) { }); } -// These tests are skipped because the driver 1) executes a ping when connecting to -// an authenticated server and 2) command monitoring is at the connection level so -// when the handshake fails no command started event is emitted. -const SKIP = [ - 'InsertOne succeeds after retryable handshake error', - 'InsertOne succeeds after retryable handshake error ShutdownInProgress' -]; - describe('Retryable Writes (unified)', function () { - runUnifiedSuite(loadSpecTests(path.join('retryable-writes', 'unified')), SKIP); -}); - -describe('Retryable Writes Spec Manual Tests', function () { - context('retryable writes handshake failures', function () { - const metadata = { - requires: { - mongodb: '>=4.2.0', - auth: 'enabled', - topology: '!single' - } - }; - - const dbName = 'retryable-handshake-tests'; - const collName = 'coll'; - const docs = [{ _id: 1, x: 11 }]; - let client; - let db; - let coll; - - beforeEach(async function () { - client = this.configuration.newClient({}); - db = client.db(dbName); - coll = db.collection(collName); - await client.connect(); - await coll.insertMany(docs); - }); - - afterEach(async function () { - await db.admin().command({ - configureFailPoint: 'failCommand', - mode: 'off' - }); - await coll.drop(); - await client.close(); - }); - - context('when the handshake fails with a network error', function () { - // Manual implementation for: 'InsertOne succeeds after retryable handshake error' - (it as any)('retries the write', metadata, async function () { - await db.admin().command({ - configureFailPoint: 'failCommand', - mode: { times: 2 }, - data: { - failCommands: ['saslContinue', 'ping'], - closeConnection: true - } - }); - const result = await coll.insertOne({ _id: 2, x: 22 }); - expect(result.insertedId).to.equal(2); - }); - }); - - context('when the handshake fails with shutdown in progress', function () { - // Manual implementation for: - // 'InsertOne succeeds after retryable handshake error ShutdownInProgress' - (it as any)('retries the write', metadata, async function () { - await db.admin().command({ - configureFailPoint: 'failCommand', - mode: { times: 2 }, - data: { - failCommands: ['saslContinue', 'ping'], - errorCode: 91 // ShutdownInProgress - } - }); - const result = await coll.insertOne({ _id: 2, x: 22 }); - expect(result.insertedId).to.equal(2); - }); - }); - }); + runUnifiedSuite(loadSpecTests(path.join('retryable-writes', 'unified'))); }); diff --git a/test/integration/transactions/transactions.spec.test.js b/test/integration/transactions/transactions.spec.test.js index 89416f622e..05b829324a 100644 --- a/test/integration/transactions/transactions.spec.test.js +++ b/test/integration/transactions/transactions.spec.test.js @@ -78,17 +78,8 @@ class TransactionsRunnerContext extends TestRunnerContext { } } -// These tests are skipped because the driver 1) executes a ping when connecting to -// an authenticated server and 2) command monitoring is at the connection level so -// when the handshake fails no command started event is emitted. -// NOTE: these tests are skipped in the spec itself due to DRIVERS-2032 (unrelated to the above) -const SKIP = [ - 'AbortTransaction succeeds after handshake network error', - 'CommitTransaction succeeds after handshake network error' -]; - describe('Transactions Spec Unified Tests', function () { - runUnifiedSuite(loadSpecTests(path.join('transactions', 'unified')), SKIP); + runUnifiedSuite(loadSpecTests(path.join('transactions', 'unified'))); }); const SKIP_TESTS = [ @@ -128,81 +119,3 @@ describe('Transactions Spec Legacy Tests', function () { generateTopologyTests(testSuites, testContext, testFilter); }); - -describe('Transactions Spec Manual Tests', function () { - context('when the handshake fails with a network error', function () { - const metadata = { - requires: { - mongodb: '>=4.2.0', - auth: 'enabled', - topology: '!single' - } - }; - - const dbName = 'retryable-handshake-tests'; - const collName = 'coll'; - const docs = [{ _id: 1, x: 11 }]; - let client; - let db; - let coll; - let session; - - beforeEach(async function () { - if (process.env.SERVERLESS) { - this.currentTest.skipReason = 'Transaction tests cannot run against serverless'; - this.skip(); - } - client = this.configuration.newClient({}); - db = client.db(dbName); - coll = db.collection(collName); - await client.connect(); - await coll.insertMany(docs); - session = client.startSession(); - session.startTransaction(); - await coll.insertOne({ _id: 2, x: 22 }, { session }); - }); - - afterEach(async function () { - await session.endSession(); - - await db.admin().command({ - configureFailPoint: 'failCommand', - mode: 'off' - }); - await coll.drop(); - await client.close(); - }); - - // Manual implementation for: 'AbortTransaction succeeds after handshake network error' - // NOTE: tests are skipped in the spec itself due to DRIVERS-2032 (unrelated to our reasons) - it('retries the abort', metadata, async function () { - await db.admin().command({ - configureFailPoint: 'failCommand', - mode: { times: 2 }, - data: { - failCommands: ['saslContinue', 'ping'], - closeConnection: true - } - }); - await session.abortTransaction(); - const doc = await coll.findOne({ _id: 2 }); - expect(doc).to.not.exist; - }); - - // Manual implementation for: 'CommitTransaction succeeds after handshake network error' - // NOTE: tests are skipped in the spec itself due to DRIVERS-2032 (unrelated to our reasons) - it('retries the commit', metadata, async function () { - await db.admin().command({ - configureFailPoint: 'failCommand', - mode: { times: 2 }, - data: { - failCommands: ['saslContinue', 'ping'], - closeConnection: true - } - }); - await session.commitTransaction(); - const doc = await coll.findOne({ _id: 2 }); - expect(doc.x).to.equal(22); - }); - }); -}); diff --git a/test/spec/retryable-writes/unified/handshakeError.json b/test/spec/retryable-writes/unified/handshakeError.json index 6d6b4ac491..39357f6047 100644 --- a/test/spec/retryable-writes/unified/handshakeError.json +++ b/test/spec/retryable-writes/unified/handshakeError.json @@ -181,7 +181,10 @@ "saslContinue", "ping" ], - "errorCode": 91 + "errorCode": 91, + "errorLabels": [ + "RetryableWriteError" + ] } } } diff --git a/test/spec/retryable-writes/unified/handshakeError.yml b/test/spec/retryable-writes/unified/handshakeError.yml index bac3a7dba3..31e9cb46fe 100644 --- a/test/spec/retryable-writes/unified/handshakeError.yml +++ b/test/spec/retryable-writes/unified/handshakeError.yml @@ -94,7 +94,7 @@ tests: data: failCommands: [saslContinue, ping] errorCode: 91 # ShutdownInProgress - + errorLabels: ["RetryableWriteError"] - name: runCommand object: *database0 arguments: diff --git a/test/tools/unified-spec-runner/runner.ts b/test/tools/unified-spec-runner/runner.ts index f057a2070e..7923fa754a 100644 --- a/test/tools/unified-spec-runner/runner.ts +++ b/test/tools/unified-spec-runner/runner.ts @@ -163,8 +163,14 @@ export async function runUnifiedTest( } } + let testPing = false; for (const operation of test.operations) { trace(operation.name); + // TODO: NODE-2149: Making connect optional should get rid of the initial ping in + // the driver so this block can then be removed. + if (operation.name === 'runCommand' && operation.arguments.commandName === 'ping') { + testPing = true; + } try { await executeOperationAndCheck(operation, entities, utilClient); } catch (e) { @@ -192,6 +198,11 @@ export async function runUnifiedTest( const actualEvents = eventType === 'cmap' ? clientCmapEvents.get(clientId) : clientCommandEvents.get(clientId); + // TODO: NODE-2149: Making connect optional should get rid of the initial ping in + // the driver so this block can then be removed. + if (eventType === 'cmap' && testPing) { + expectedEventList.events.push({ connectionCheckOutStartedEvent: {} }); + } expect(actualEvents, `No client entity found with id ${clientId}`).to.exist; matchesEvents(expectedEventList.events, actualEvents, entities); } diff --git a/test/unit/error.test.ts b/test/unit/error.test.ts index 9c66b92ed2..ba48765dfd 100644 --- a/test/unit/error.test.ts +++ b/test/unit/error.test.ts @@ -462,7 +462,7 @@ describe('MongoErrors', () => { }, { description: 'a MongoWriteConcernError with a RetryableWriteError label below server 4.4', - result: true, + result: false, error: new MongoWriteConcernError({}, { errorLabels: ['RetryableWriteError'] }), maxWireVersion: BELOW_4_4 }, From bb3e5bad6de34c8cf9077a8d21863d565a15d83a Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 20 Apr 2022 14:19:01 +0200 Subject: [PATCH 17/21] fix(NODE-3688): remove wire version check --- src/cmap/connect.ts | 5 +---- src/error.ts | 12 ++++-------- src/sdam/server.ts | 2 +- .../retryable-writes/unified/handshakeError.json | 5 +---- .../spec/retryable-writes/unified/handshakeError.yml | 1 - test/unit/error.test.ts | 8 ++++---- 6 files changed, 11 insertions(+), 22 deletions(-) diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index d15aa63d36..611fc668ab 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -186,10 +186,7 @@ function performInitialHandshake( } provider.auth(authContext, err => { if (err) { - if ( - err instanceof MongoError && - needsRetryableWriteLabel(err, response.maxWireVersion) - ) { + if (err instanceof MongoError && needsRetryableWriteLabel(err)) { err.addErrorLabel(MongoErrorLabel.RetryableWriteError); } return callback(err); diff --git a/src/error.ts b/src/error.ts index 5f4d3f3773..855a3277c5 100644 --- a/src/error.ts +++ b/src/error.ts @@ -743,18 +743,14 @@ const RETRYABLE_WRITE_ERROR_CODES = new Set([ MONGODB_ERROR_CODES.ExceededTimeLimit ]); -export function needsRetryableWriteLabel(error: Error, maxWireVersion: number): boolean { - // pre-4.4 server, then the driver adds an error label for every valid case - // execute operation will only inspect the label, code/message logic is handled here +export function needsRetryableWriteLabel(error: Error): boolean { + // Network errors are always retryable. if (error instanceof MongoNetworkError) { return true; } - if ( - maxWireVersion >= 9 || - (error instanceof MongoError && error.hasErrorLabel(MongoErrorLabel.RetryableWriteError)) - ) { - // If we already have the error label no need to add it again. 4.4+ servers add the label. + // We don't need to apply the label if it's already there. + if (error instanceof MongoError && error.hasErrorLabel(MongoErrorLabel.RetryableWriteError)) { return false; } diff --git a/src/sdam/server.ts b/src/sdam/server.ts index 3637c6bfd9..78b4a1f7d3 100644 --- a/src/sdam/server.ts +++ b/src/sdam/server.ts @@ -554,7 +554,7 @@ function makeOperationHandler( } else { if ( (isRetryableWritesEnabled(server.s.topology) || isTransactionCommand(cmd)) && - needsRetryableWriteLabel(error, maxWireVersion(server)) && + needsRetryableWriteLabel(error) && !inActiveTransaction(session, cmd) ) { error.addErrorLabel(MongoErrorLabel.RetryableWriteError); diff --git a/test/spec/retryable-writes/unified/handshakeError.json b/test/spec/retryable-writes/unified/handshakeError.json index 39357f6047..6d6b4ac491 100644 --- a/test/spec/retryable-writes/unified/handshakeError.json +++ b/test/spec/retryable-writes/unified/handshakeError.json @@ -181,10 +181,7 @@ "saslContinue", "ping" ], - "errorCode": 91, - "errorLabels": [ - "RetryableWriteError" - ] + "errorCode": 91 } } } diff --git a/test/spec/retryable-writes/unified/handshakeError.yml b/test/spec/retryable-writes/unified/handshakeError.yml index 31e9cb46fe..e1b69e8263 100644 --- a/test/spec/retryable-writes/unified/handshakeError.yml +++ b/test/spec/retryable-writes/unified/handshakeError.yml @@ -94,7 +94,6 @@ tests: data: failCommands: [saslContinue, ping] errorCode: 91 # ShutdownInProgress - errorLabels: ["RetryableWriteError"] - name: runCommand object: *database0 arguments: diff --git a/test/unit/error.test.ts b/test/unit/error.test.ts index ba48765dfd..7eb4ea4bf7 100644 --- a/test/unit/error.test.ts +++ b/test/unit/error.test.ts @@ -449,8 +449,8 @@ describe('MongoErrors', () => { maxWireVersion: BELOW_4_4 }, { - description: 'a MongoWriteConcernError with a retryable code above server 4.4', - result: false, + description: 'a MongoWriteConcernError with a retryable code but no label above 4.4', + result: true, error: new MongoWriteConcernError({}, { code: 262 }), maxWireVersion: ABOVE_4_4 }, @@ -485,9 +485,9 @@ describe('MongoErrors', () => { maxWireVersion: ABOVE_4_4 } ]; - for (const { description, result, error, maxWireVersion } of tests) { + for (const { description, result, error } of tests) { it(`${description} ${result ? 'needs' : 'does not need'} a retryable write label`, () => { - expect(needsRetryableWriteLabel(error, maxWireVersion)).to.be.equal(result); + expect(needsRetryableWriteLabel(error)).to.be.equal(result); }); } }); From 14ebf7f3906f83805a83a9b1814c3b5216596c78 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 20 Apr 2022 14:56:53 +0200 Subject: [PATCH 18/21] Revert "fix(NODE-3688): remove wire version check" This reverts commit bb3e5bad6de34c8cf9077a8d21863d565a15d83a. --- src/cmap/connect.ts | 5 ++++- src/error.ts | 12 ++++++++---- src/sdam/server.ts | 2 +- .../retryable-writes/unified/handshakeError.json | 5 ++++- .../spec/retryable-writes/unified/handshakeError.yml | 1 + test/unit/error.test.ts | 8 ++++---- 6 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index 611fc668ab..d15aa63d36 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -186,7 +186,10 @@ function performInitialHandshake( } provider.auth(authContext, err => { if (err) { - if (err instanceof MongoError && needsRetryableWriteLabel(err)) { + if ( + err instanceof MongoError && + needsRetryableWriteLabel(err, response.maxWireVersion) + ) { err.addErrorLabel(MongoErrorLabel.RetryableWriteError); } return callback(err); diff --git a/src/error.ts b/src/error.ts index 855a3277c5..5f4d3f3773 100644 --- a/src/error.ts +++ b/src/error.ts @@ -743,14 +743,18 @@ const RETRYABLE_WRITE_ERROR_CODES = new Set([ MONGODB_ERROR_CODES.ExceededTimeLimit ]); -export function needsRetryableWriteLabel(error: Error): boolean { - // Network errors are always retryable. +export function needsRetryableWriteLabel(error: Error, maxWireVersion: number): boolean { + // pre-4.4 server, then the driver adds an error label for every valid case + // execute operation will only inspect the label, code/message logic is handled here if (error instanceof MongoNetworkError) { return true; } - // We don't need to apply the label if it's already there. - if (error instanceof MongoError && error.hasErrorLabel(MongoErrorLabel.RetryableWriteError)) { + if ( + maxWireVersion >= 9 || + (error instanceof MongoError && error.hasErrorLabel(MongoErrorLabel.RetryableWriteError)) + ) { + // If we already have the error label no need to add it again. 4.4+ servers add the label. return false; } diff --git a/src/sdam/server.ts b/src/sdam/server.ts index 78b4a1f7d3..3637c6bfd9 100644 --- a/src/sdam/server.ts +++ b/src/sdam/server.ts @@ -554,7 +554,7 @@ function makeOperationHandler( } else { if ( (isRetryableWritesEnabled(server.s.topology) || isTransactionCommand(cmd)) && - needsRetryableWriteLabel(error) && + needsRetryableWriteLabel(error, maxWireVersion(server)) && !inActiveTransaction(session, cmd) ) { error.addErrorLabel(MongoErrorLabel.RetryableWriteError); diff --git a/test/spec/retryable-writes/unified/handshakeError.json b/test/spec/retryable-writes/unified/handshakeError.json index 6d6b4ac491..39357f6047 100644 --- a/test/spec/retryable-writes/unified/handshakeError.json +++ b/test/spec/retryable-writes/unified/handshakeError.json @@ -181,7 +181,10 @@ "saslContinue", "ping" ], - "errorCode": 91 + "errorCode": 91, + "errorLabels": [ + "RetryableWriteError" + ] } } } diff --git a/test/spec/retryable-writes/unified/handshakeError.yml b/test/spec/retryable-writes/unified/handshakeError.yml index e1b69e8263..31e9cb46fe 100644 --- a/test/spec/retryable-writes/unified/handshakeError.yml +++ b/test/spec/retryable-writes/unified/handshakeError.yml @@ -94,6 +94,7 @@ tests: data: failCommands: [saslContinue, ping] errorCode: 91 # ShutdownInProgress + errorLabels: ["RetryableWriteError"] - name: runCommand object: *database0 arguments: diff --git a/test/unit/error.test.ts b/test/unit/error.test.ts index 7eb4ea4bf7..ba48765dfd 100644 --- a/test/unit/error.test.ts +++ b/test/unit/error.test.ts @@ -449,8 +449,8 @@ describe('MongoErrors', () => { maxWireVersion: BELOW_4_4 }, { - description: 'a MongoWriteConcernError with a retryable code but no label above 4.4', - result: true, + description: 'a MongoWriteConcernError with a retryable code above server 4.4', + result: false, error: new MongoWriteConcernError({}, { code: 262 }), maxWireVersion: ABOVE_4_4 }, @@ -485,9 +485,9 @@ describe('MongoErrors', () => { maxWireVersion: ABOVE_4_4 } ]; - for (const { description, result, error } of tests) { + for (const { description, result, error, maxWireVersion } of tests) { it(`${description} ${result ? 'needs' : 'does not need'} a retryable write label`, () => { - expect(needsRetryableWriteLabel(error)).to.be.equal(result); + expect(needsRetryableWriteLabel(error, maxWireVersion)).to.be.equal(result); }); } }); From 5b28e916ac1c8fb0019623cf0743fba916befc46 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 20 Apr 2022 15:04:19 +0200 Subject: [PATCH 19/21] fix(NODE-3688): apply new label to handshake error --- src/cmap/connect.ts | 10 +++------- src/error.ts | 10 +++++++++- src/operations/execute_operation.ts | 4 ++-- test/spec/retryable-writes/unified/handshakeError.json | 5 +---- test/spec/retryable-writes/unified/handshakeError.yml | 1 - 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index d15aa63d36..3e8a829a1c 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -16,8 +16,7 @@ import { MongoNetworkError, MongoNetworkTimeoutError, MongoRuntimeError, - MongoServerError, - needsRetryableWriteLabel + MongoServerError } from '../error'; import { Callback, ClientMetadata, HostAddress, makeClientMetadata, ns } from '../utils'; import { AuthContext, AuthProvider } from './auth/auth_provider'; @@ -186,11 +185,8 @@ function performInitialHandshake( } provider.auth(authContext, err => { if (err) { - if ( - err instanceof MongoError && - needsRetryableWriteLabel(err, response.maxWireVersion) - ) { - err.addErrorLabel(MongoErrorLabel.RetryableWriteError); + if (err instanceof MongoError) { + err.addErrorLabel(MongoErrorLabel.HandshakeError); } return callback(err); } diff --git a/src/error.ts b/src/error.ts index 5f4d3f3773..c18ac08bc0 100644 --- a/src/error.ts +++ b/src/error.ts @@ -88,7 +88,8 @@ export const MongoErrorLabel = Object.freeze({ RetryableWriteError: 'RetryableWriteError', TransientTransactionError: 'TransientTransactionError', UnknownTransactionCommitResult: 'UnknownTransactionCommitResult', - ResumableChangeStreamError: 'ResumableChangeStreamError' + ResumableChangeStreamError: 'ResumableChangeStreamError', + HandshakeError: 'HandshakeError' } as const); /** @public */ @@ -779,6 +780,13 @@ export function needsRetryableWriteLabel(error: Error, maxWireVersion: number): return false; } +export function isRetryableWriteError(error: MongoError): boolean { + return ( + error.hasErrorLabel(MongoErrorLabel.RetryableWriteError) || + error.hasErrorLabel(MongoErrorLabel.HandshakeError) + ); +} + /** Determines whether an error is something the driver should attempt to retry */ export function isRetryableReadError(error: MongoError): boolean { const hasRetryableErrorCode = diff --git a/src/operations/execute_operation.ts b/src/operations/execute_operation.ts index 9327f4beb9..22b69996c0 100644 --- a/src/operations/execute_operation.ts +++ b/src/operations/execute_operation.ts @@ -1,10 +1,10 @@ import type { Document } from '../bson'; import { isRetryableReadError, + isRetryableWriteError, MongoCompatibilityError, MONGODB_ERROR_CODES, MongoError, - MongoErrorLabel, MongoExpiredSessionError, MongoNetworkError, MongoRuntimeError, @@ -195,7 +195,7 @@ function executeWithServerSelection( ); } - if (isWriteOperation && !originalError.hasErrorLabel(MongoErrorLabel.RetryableWriteError)) { + if (isWriteOperation && !isRetryableWriteError(originalError)) { return callback(originalError); } diff --git a/test/spec/retryable-writes/unified/handshakeError.json b/test/spec/retryable-writes/unified/handshakeError.json index 39357f6047..6d6b4ac491 100644 --- a/test/spec/retryable-writes/unified/handshakeError.json +++ b/test/spec/retryable-writes/unified/handshakeError.json @@ -181,10 +181,7 @@ "saslContinue", "ping" ], - "errorCode": 91, - "errorLabels": [ - "RetryableWriteError" - ] + "errorCode": 91 } } } diff --git a/test/spec/retryable-writes/unified/handshakeError.yml b/test/spec/retryable-writes/unified/handshakeError.yml index 31e9cb46fe..e1b69e8263 100644 --- a/test/spec/retryable-writes/unified/handshakeError.yml +++ b/test/spec/retryable-writes/unified/handshakeError.yml @@ -94,7 +94,6 @@ tests: data: failCommands: [saslContinue, ping] errorCode: 91 # ShutdownInProgress - errorLabels: ["RetryableWriteError"] - name: runCommand object: *database0 arguments: From fecb10451f75d2e8cda1410b5c5fd30d521ce3b0 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 20 Apr 2022 15:13:54 +0200 Subject: [PATCH 20/21] fix(NODE-3688): check handshake error when applying label --- src/cmap/connect.ts | 6 +++++- src/error.ts | 21 +++++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index 3e8a829a1c..21556324aa 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -16,7 +16,8 @@ import { MongoNetworkError, MongoNetworkTimeoutError, MongoRuntimeError, - MongoServerError + MongoServerError, + needsRetryableWriteLabel } from '../error'; import { Callback, ClientMetadata, HostAddress, makeClientMetadata, ns } from '../utils'; import { AuthContext, AuthProvider } from './auth/auth_provider'; @@ -187,6 +188,9 @@ function performInitialHandshake( if (err) { if (err instanceof MongoError) { err.addErrorLabel(MongoErrorLabel.HandshakeError); + if (needsRetryableWriteLabel(err, response.maxWireVersion)) { + err.addErrorLabel(MongoErrorLabel.RetryableWriteError); + } } return callback(err); } diff --git a/src/error.ts b/src/error.ts index c18ac08bc0..1297d841d2 100644 --- a/src/error.ts +++ b/src/error.ts @@ -751,12 +751,16 @@ export function needsRetryableWriteLabel(error: Error, maxWireVersion: number): return true; } - if ( - maxWireVersion >= 9 || - (error instanceof MongoError && error.hasErrorLabel(MongoErrorLabel.RetryableWriteError)) - ) { - // If we already have the error label no need to add it again. 4.4+ servers add the label. - return false; + if (error instanceof MongoError) { + if ( + (maxWireVersion >= 9 || error.hasErrorLabel(MongoErrorLabel.RetryableWriteError)) && + !error.hasErrorLabel(MongoErrorLabel.HandshakeError) + ) { + // If we already have the error label no need to add it again. 4.4+ servers add the label. + // In the case where we have a handshake error, need to fall down to the logic checking + // the codes. + return false; + } } if (error instanceof MongoWriteConcernError) { @@ -781,10 +785,7 @@ export function needsRetryableWriteLabel(error: Error, maxWireVersion: number): } export function isRetryableWriteError(error: MongoError): boolean { - return ( - error.hasErrorLabel(MongoErrorLabel.RetryableWriteError) || - error.hasErrorLabel(MongoErrorLabel.HandshakeError) - ); + return error.hasErrorLabel(MongoErrorLabel.RetryableWriteError); } /** Determines whether an error is something the driver should attempt to retry */ From e33f2b2679959b66a0eada374ce085da08626d3e Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 20 Apr 2022 17:50:25 +0200 Subject: [PATCH 21/21] test(NODE-3688): remove extra cmap event from ping --- test/tools/unified-spec-runner/runner.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/tools/unified-spec-runner/runner.ts b/test/tools/unified-spec-runner/runner.ts index 7923fa754a..f057a2070e 100644 --- a/test/tools/unified-spec-runner/runner.ts +++ b/test/tools/unified-spec-runner/runner.ts @@ -163,14 +163,8 @@ export async function runUnifiedTest( } } - let testPing = false; for (const operation of test.operations) { trace(operation.name); - // TODO: NODE-2149: Making connect optional should get rid of the initial ping in - // the driver so this block can then be removed. - if (operation.name === 'runCommand' && operation.arguments.commandName === 'ping') { - testPing = true; - } try { await executeOperationAndCheck(operation, entities, utilClient); } catch (e) { @@ -198,11 +192,6 @@ export async function runUnifiedTest( const actualEvents = eventType === 'cmap' ? clientCmapEvents.get(clientId) : clientCommandEvents.get(clientId); - // TODO: NODE-2149: Making connect optional should get rid of the initial ping in - // the driver so this block can then be removed. - if (eventType === 'cmap' && testPing) { - expectedEventList.events.push({ connectionCheckOutStartedEvent: {} }); - } expect(actualEvents, `No client entity found with id ${clientId}`).to.exist; matchesEvents(expectedEventList.events, actualEvents, entities); }