`. It will also create a new changelog entry with today's date marking the
+conversion.
+
+### Prerequisites
+
+```bash
+brew install pandoc
+brew install pre-commit
+brew install python # or get python through your preferred channel
+pre-commit install
+```
+
+### Usage
+
+- Run the script as:
+
+```bash
+python3 scripts/migrate_to_md.py "source/
"
+```
+
+- Address any errors that were printed during the run.
+
+- Ensure that the generated markdown file is properly formatted.
+
+- Ensure that the links in the new file are working, by running `pre-commit run markdown-link-check` and addressing
+ failures until that passes.
+
+- Remove the rst file using `git rm`.
+
+- Create a PR. When you commit the changes, the `mdformat` `pre-commit` hook will update the formatting as appropriate.
+
+## generate_index
+
+Use this file to generate the top level Markdown index file. It is independent to be used as a pre-commit hook.
diff --git a/scripts/check_links.py b/scripts/check_links.py
new file mode 100644
index 0000000000..5dc25d2f68
--- /dev/null
+++ b/scripts/check_links.py
@@ -0,0 +1,29 @@
+import sys, re
+
+fname = sys.argv[-1]
+
+# Roughly detect fenced code even inside block quotes
+fenced_code = re.compile(r"^\s*(>\s+)*```")
+
+# Check for markdown links that got improperly line wrapped.
+in_code_block = False
+with open(fname) as fid:
+ for line in fid:
+ # Ignore code blocks.
+ if fenced_code.match(line):
+ in_code_block = not in_code_block
+ if in_code_block:
+ continue
+ id0 = line.index("[") if "[" in line else -1
+ id1 = line.index("]") if "]" in line else -1
+ id2 = line.index("(") if "(" in line else -1
+ id3 = line.index(")") if ")" in line else -1
+ if id1 == -1 or id2 == -1 or id3 == -1:
+ continue
+ if id2 < id1 or id3 < id2:
+ continue
+ if id0 == -1:
+ print("*** Malformed link in line:", line, fname)
+ sys.exit(1)
+
+assert not in_code_block
diff --git a/scripts/check_md_html.py b/scripts/check_md_html.py
new file mode 100644
index 0000000000..cd4a296c11
--- /dev/null
+++ b/scripts/check_md_html.py
@@ -0,0 +1,55 @@
+import sys, re
+
+fname = sys.argv[-1]
+
+# Check for allowed HTML elements in markdown.
+# Ignores inline and fenced code, but intentionally doesn't ignore backslash
+# escaping. (For compatibility, we want to avoid unintentional inline HTML
+# even on markdown implementations where "\<" escapes are not supported.)
+
+disallowed_re = re.compile(
+ r"""
+ [^`]*(`[^`]+`)*
+ <(?!
+ - |
+ /p> |
+ /span> |
+ /sub> |
+ /sup> |
+ /table> |
+ /td> |
+ /tr> |
+ \d |
+ \s |
+ \w+@(\w+\.)+\w+> | # Cover email addresses in license files
+ = |
+ br> |
+ https:// | # Cover HTTPS links but not HTTP
+ p> |
+ span[\s>] |
+ sub> |
+ sup> |
+ table[\s>] |
+ td[\s>] |
+ tr> |
+ !-- )
+ """,
+ re.VERBOSE,
+)
+
+# Roughly detect fenced code even inside block quotes
+fenced_code = re.compile(r"^\s*(>\s+)*```")
+
+in_code_block = False
+with open(fname) as fid:
+ for line in fid:
+ # Ignore code blocks.
+ if fenced_code.match(line):
+ in_code_block = not in_code_block
+ if in_code_block:
+ continue
+ if disallowed_re.match(line):
+ print("*** Markdown contains unexpected HTML in line:", line, fname)
+ sys.exit(1)
+
+assert not in_code_block
diff --git a/scripts/generate_index.py b/scripts/generate_index.py
new file mode 100644
index 0000000000..94c1ca253a
--- /dev/null
+++ b/scripts/generate_index.py
@@ -0,0 +1,30 @@
+import os
+from pathlib import Path
+
+source = Path(__file__).resolve().parent.parent / "source"
+source = source.resolve()
+info = {}
+for p in Path(source).rglob("*.md"):
+ relpath = os.path.relpath(p.parent, start=source)
+ if "tests" in relpath:
+ continue
+ if "node_modules" in relpath:
+ continue
+ if p.name in ["index.md"]:
+ continue
+ fpath = relpath + "/" + p.name
+ name = None
+ with p.open() as fid:
+ for line in fid:
+ if line.startswith("# "):
+ name = line.replace("# ", "").strip()
+ break
+ if name is None:
+ raise ValueError(f"Could not find name for {fpath}")
+ info[name] = fpath
+
+index_file = source / "index.md"
+with index_file.open("w") as fid:
+ fid.write("# MongoDB Specifications\n\n")
+ for name in sorted(info):
+ fid.write(f"- [{name}]({info[name]})\n")
diff --git a/scripts/migrate_to_md.py b/scripts/migrate_to_md.py
new file mode 100644
index 0000000000..2b87db29ef
--- /dev/null
+++ b/scripts/migrate_to_md.py
@@ -0,0 +1,178 @@
+import subprocess
+import os
+import re
+import sys
+from pathlib import Path
+import datetime
+import subprocess
+
+if len(sys.argv) < 2:
+ print("Must provide a path to an RST file")
+ sys.exit(1)
+
+path = Path(sys.argv[1])
+
+# Ensure git history for the md file.
+md_file = str(path).replace(".rst", ".md")
+subprocess.check_call(["git", "mv", path, md_file])
+subprocess.check_call(["git", "add", md_file])
+subprocess.check_call(
+ ["git", "commit", "--no-verify", "-m", f"Rename {path} to {md_file}"]
+)
+subprocess.check_call(["git", "checkout", "HEAD~1", path])
+subprocess.check_call(["git", "add", path])
+
+# Get the contents of the file.
+with path.open() as fid:
+ lines = fid.readlines()
+
+TEMPLATE = """
+.. note::
+ This specification has been converted to Markdown and renamed to
+ `{0} <{0}>`_.
+"""
+
+# Update the RST file with a stub pointer to the MD file.
+if not path.name == "README.rst":
+ new_body = TEMPLATE.format(os.path.basename(md_file))
+ with path.open("w") as fid:
+ fid.write("".join(new_body))
+
+# Pre-process the file.
+for i, line in enumerate(lines):
+ # Replace curly quotes with regular quotes.
+ line = line.replace("”", '"')
+ line = line.replace("“", '"')
+ line = line.replace("’", "'")
+ line = line.replace("‘", "'")
+ lines[i] = line
+
+ # Replace the colon fence blocks with bullets,
+ # e.g. :Status:, :deprecated:, :changed:.
+ # This also includes the changelog entries.
+ match = re.match(r":(\S+):(.*)", line)
+ if match:
+ name, value = match.groups()
+ lines[i] = f"- {name.capitalize()}:{value}\n"
+
+ # Handle "":Minimum Server Version:"" as a block quote.
+ if line.strip().startswith(":Minimum Server Version:"):
+ lines[i] = "- " + line.strip()[1:] + ""
+
+ # Remove the "".. contents::" block - handled by GitHub UI.
+ if line.strip() == ".. contents::":
+ lines[i] = ""
+
+# Run pandoc and capture output.
+proc = subprocess.Popen(
+ ["pandoc", "-f", "rst", "-t", "gfm"], stdin=subprocess.PIPE, stdout=subprocess.PIPE
+)
+data = "".join(lines).encode("utf8")
+outs, _ = proc.communicate(data)
+data = outs.decode("utf8")
+
+# Fix the strings that were missing backticks.
+data = re.sub(r'', "`", data, flags=re.MULTILINE)
+data = data.replace("", "`")
+
+# Handle div blocks that were created.
+# These are admonition blocks, convert to new GFM format.
+# Also add a changelog entry.
+in_block_outer = False
+in_block_inner = False
+in_changelog_first = False
+lines = data.splitlines()
+new_lines = []
+for i, line in enumerate(lines):
+ match = re.match(r'', line)
+ if not in_block_outer and match:
+ in_block_outer = True
+ new_lines.append(f"> [!{match.groups()[0].upper()}]")
+ continue
+ if line.strip() == "
":
+ if in_block_outer:
+ in_block_outer = False
+ in_block_inner = True
+ elif in_block_inner:
+ in_block_inner = False
+ continue
+ if in_block_inner:
+ line = "> " + line.strip()
+
+ if in_changelog_first:
+ today = datetime.date.today().strftime("%Y-%m-%d")
+ line = f"\n- {today}: Migrated from reStructuredText to Markdown."
+ in_changelog_first = False
+
+ if line.strip() == "## Changelog":
+ in_changelog_first = True
+
+ if not in_block_outer:
+ new_lines.append(line)
+
+
+# Write the new content to the markdown file.
+with open(md_file, "w") as fid:
+ fid.write("\n".join(new_lines))
+
+# Handle links in other files.
+# We accept relative path links or links to master
+# (https://github.com/mongodb/specifications/blob/master/source/...)
+# and rewrite them to use appropriate md links.
+# If the link is malformed we ignore and print an error.
+target = path.name
+curr = path
+while curr.parent.name != "source":
+ target = f"{curr.parent.name}/{target}"
+ curr = curr.parent
+suffix = rf"\S*/{target}"
+rel_pattern = re.compile(rf"(\.\.{suffix})")
+md_pattern = re.compile(rf"(\(http{suffix})")
+html_pattern = re.compile(f"(http{suffix})")
+abs_pattern = re.compile(f"(/source{suffix})")
+for p in Path("source").rglob("*"):
+ if p.suffix not in [".rst", ".md"]:
+ continue
+ with p.open() as fid:
+ lines = fid.readlines()
+ new_lines = []
+ changed_lines = []
+ relpath = os.path.relpath(md_file, start=p.parent)
+ for line in lines:
+ new_line = line
+ if re.search(rel_pattern, line):
+ matchstr = re.search(rel_pattern, line).groups()[0]
+ new_line = line.replace(matchstr, relpath)
+ elif re.search(md_pattern, line):
+ matchstr = re.search(md_pattern, line).groups()[0]
+ if not matchstr.startswith(
+ "(https://github.com/mongodb/specifications/blob/master/source"
+ ):
+ print("*** Error in link: ", matchstr, p)
+ else:
+ new_line = line.replace(matchstr, f"({relpath}")
+ elif re.search(html_pattern, line):
+ matchstr = re.search(html_pattern, line).groups()[0]
+ if not matchstr.startswith(
+ "https://github.com/mongodb/specifications/blob/master/source"
+ ):
+ print("*** Error in link: ", matchstr, p)
+ else:
+ new_line = line.replace(matchstr, f"{relpath}")
+ elif re.search(abs_pattern, line):
+ matchstr = re.search(abs_pattern, line).groups()[0]
+ new_line = line.replace(matchstr, relpath)
+
+ if new_line != line:
+ changed_lines.append(new_line)
+ new_lines.append(new_line)
+
+ if changed_lines:
+ with p.open("w") as fid:
+ fid.writelines(new_lines)
+ print("-" * 80)
+ print(f"Updated link(s) in {p}...")
+ print(" " + "\n ".join(changed_lines))
+
+print("Created markdown file:")
+print(md_file)
diff --git a/scripts/yaml2json.mjs b/scripts/yaml2json.mjs
new file mode 100644
index 0000000000..471f6d446b
--- /dev/null
+++ b/scripts/yaml2json.mjs
@@ -0,0 +1,17 @@
+#!/usr/bin/env node
+// Usage: node yaml2json.mjs
+// Converts a YAML file to JSON, with YAML 1.1 merge key (<<:) support for legacy tests.
+// TODO(DRIVERS-3178): after removing legacy tests, consider removing this script and invoking `js-yaml` directly.
+import { readFileSync } from 'fs';
+import { execSync } from 'child_process';
+
+// Import globally installed `js-yaml` package:
+const globalRoot = execSync('npm root -g').toString().trim();
+const pkg = JSON.parse(readFileSync(`${globalRoot}/js-yaml/package.json`, 'utf8'));
+const entry = pkg.exports?.['.']?.import ?? pkg.module ?? pkg.main;
+const { load, CORE_SCHEMA, mergeTag } = await import(`${globalRoot}/js-yaml/${entry}`);
+
+const file = process.argv[2];
+const content = readFileSync(file, 'utf8');
+const data = load(content, { schema: CORE_SCHEMA.withTags(mergeTag) });
+process.stdout.write(JSON.stringify(data, null, 2) + '\n');
diff --git a/source/Makefile b/source/Makefile
index eaa584eb6b..51ef248a36 100644
--- a/source/Makefile
+++ b/source/Makefile
@@ -4,12 +4,12 @@ JSON_FILES=$(patsubst %.yml,%.json,$(YAML_FILES))
all: $(JSON_FILES) HAS_JSYAML
%.json: %.yml
- tmpfile=$$(mktemp); \
- if js-yaml $< > "$$tmpfile"; then \
- mv "$$tmpfile" $@; \
- else \
- rm "$$tmpfile"; \
- exit 1; \
+ tmpfile=$$(mktemp); \
+ if node $(dir $(lastword $(MAKEFILE_LIST)))/../scripts/yaml2json.mjs $< > "$$tmpfile"; then \
+ mv "$$tmpfile" $@; \
+ else \
+ rm "$$tmpfile"; \
+ exit 1; \
fi
HAS_JSYAML:
@@ -17,3 +17,16 @@ HAS_JSYAML:
echo 'Error: need "npm install -g js-yaml"' 1>&2; \
exit 1; \
fi
+
+VERSION := $(shell \
+ find unified-test-format -maxdepth 1 -type f -name 'schema-1.*.json' \
+ | sed -E 's:.*/schema-1\.([0-9]+)\.json:\1:' \
+ | sort -n \
+ | tail -n1 \
+)
+
+.PHONY: update-schema-latest
+update-schema-latest:
+ cp unified-test-format/schema-1.$(VERSION).json \
+ unified-test-format/schema-latest.json
+
diff --git a/source/atlas-sfp-testing/atlas-sfp-testing.md b/source/atlas-sfp-testing/atlas-sfp-testing.md
new file mode 100644
index 0000000000..b687161362
--- /dev/null
+++ b/source/atlas-sfp-testing/atlas-sfp-testing.md
@@ -0,0 +1,120 @@
+# Atlas Secure Frontend Processor (SFP) Testing
+
+- Status: Accepted
+- Minimum Server Version: 7.0
+
+______________________________________________________________________
+
+## Abstract
+
+This specification defines the tests that drivers MUST run to verify connectivity and authentication through an Atlas
+Secure Frontend Processor (SFP). The SFP is a proxy that sits in front of Atlas clusters to provide additional security
+capabilities.
+
+## META
+
+The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
+"OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt).
+
+## Specification
+
+### Terms
+
+#### SFP
+
+Secure Frontend Processor - a proxy service that sits in front of Atlas clusters, providing TLS termination,
+authentication forwarding, and additional security features.
+
+### Test Environment
+
+SFP clusters are **preconfigured** and do not require provisioning or teardown as part of the test run. Drivers will be
+provided with connection URIs and credentials via environment variables.
+
+The SFP proxy is fully transparent to drivers - all standard MongoDB operations should work exactly as they would
+against a normal Atlas cluster.
+
+### Required Environment Variables
+
+The following environment variables will be available to run the tests:
+
+| Variable | Description |
+| -------------------- | -------------------------------------------------- |
+| `SFP_ATLAS_URI` | MongoDB connection URI for the SFP-proxied cluster |
+| `SFP_ATLAS_USER` | Username for SCRAM authentication |
+| `SFP_ATLAS_PASSWORD` | Password for SCRAM authentication |
+
+For X.509 authentication tests, the following additional variables are required:
+
+| Variable | Description |
+| --------------------- | ----------------------------------------------- |
+| `SFP_ATLAS_X509_URI` | MongoDB connection URI for X.509 authentication |
+| `SFP_ATLAS_X509_CERT` | Path to client certificate (PEM format) |
+
+### Test Isolation and Cleanup
+
+To prevent conflicts between concurrent test runs and avoid unbounded collection growth:
+
+1. Drivers MUST use a unique collection name for each test run, e.g., `sfp_test_` where `` is an
+ ObjectID string representation
+2. Drivers MUST drop the test collection after all tests complete, regardless of test success or failure
+
+## Required Tests
+
+Drivers MUST implement and run the following tests against SFP-proxied clusters.
+
+### Common Assertions
+
+The following assertions are used across multiple tests:
+
+#### Assertion: Ping
+
+1. Execute a `ping` command against the `admin` database
+2. Assert that the command succeeds with `ok: 1`
+
+#### Assertion: Connection Status
+
+1. Execute a `connectionStatus` command against the `admin` database
+2. Assert that the command succeeds with `ok: 1`
+3. If authenticated, assert that `authInfo.authenticatedUsers` contains at least one user
+
+#### Assertion: CRUD Operations
+
+1. Insert a document into a test collection and assert the insert succeeds
+2. Query the collection using `find` and assert the inserted document is returned
+
+### Unauthenticated Tests
+
+Create a `MongoClient` configured with `SFP_ATLAS_URI` but without credentials. Run the following assertions:
+
+- Ping
+- Connection Status (assert `authenticatedUsers` is empty)
+
+### Authenticated Tests
+
+Each authenticated test MUST be run under each of the following variations:
+
+1. No additional configuration (baseline)
+2. With at least one compressor enabled (e.g., zlib, snappy, or zstd)
+3. With [Server API](../versioned-api/versioned-api.md#mongoclient-changes) version 1
+
+#### SCRAM-SHA-256
+
+Create a `MongoClient` with the connection string and SCRAM-SHA-256 credentials from environment variables. Run the
+following assertions:
+
+- Ping
+- Connection Status
+- CRUD Operations
+
+#### X.509
+
+Create a `MongoClient` with the connection string and X.509 authentication using the client certificate. Run the
+following assertions:
+
+- Ping
+- Connection Status
+- CRUD Operations
+
+## Changelog
+
+- 2025-02-27: Initial version
diff --git a/source/auth/auth.md b/source/auth/auth.md
new file mode 100644
index 0000000000..efaea8779c
--- /dev/null
+++ b/source/auth/auth.md
@@ -0,0 +1,2261 @@
+# Authentication
+
+- Status: Accepted
+- Minimum Server Version: 2.6
+
+______________________________________________________________________
+
+## Abstract
+
+MongoDB supports various authentication strategies across various versions. When authentication is turned on in the
+database, a driver must authenticate before it is allowed to communicate with the server. This spec defines when and how
+a driver performs authentication with a MongoDB server.
+
+### META
+
+The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
+"OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt).
+
+### References
+
+[Server Discovery and Monitoring](../server-discovery-and-monitoring/server-discovery-and-monitoring.md)
+
+## Specification
+
+### Definitions
+
+- Credential
+
+ The pieces of information used to establish the authenticity of a user. This is composed of an identity and some form
+ of evidence such as a password or a certificate.
+
+- FQDN
+
+ Fully Qualified Domain Name
+
+- Mechanism
+
+ A SASL implementation of a particular type of credential negotiation.
+
+- Source
+
+ The authority used to establish credentials and/or privileges in reference to a mongodb server. In practice, it is the
+ database to which sasl authentication commands are sent.
+
+- Realm
+
+ The authority used to establish credentials and/or privileges in reference to GSSAPI.
+
+- SASL
+
+ Simple Authentication and Security Layer - [RFC 4422](http://www.ietf.org/rfc/rfc4422.txt)
+
+### Client Implementation
+
+#### MongoCredential
+
+Drivers SHOULD contain a type called `MongoCredential`. It SHOULD contain some or all of the following information.
+
+- username (string)
+
+ - Applies to all mechanisms.
+ - Optional for MONGODB-X509, MONGODB-AWS, and MONGODB-OIDC.
+
+- source (string)
+
+ - Applies to all mechanisms.
+ - Always '$external' for GSSAPI and MONGODB-X509.
+ - This is the database to which the authenticate command will be sent.
+ - This is the database to which sasl authentication commands will be sent.
+
+- password (string)
+
+ - Does not apply to all mechanisms.
+
+- mechanism (string)
+
+ - Indicates which mechanism to use with the credential.
+
+- mechanism_properties
+
+ - Includes additional properties for the given mechanism.
+
+Each mechanism requires certain properties to be present in a MongoCredential for authentication to occur. See the
+individual mechanism definitions in the "MongoCredential Properties" section. All requirements listed for a mechanism
+must be met for authentication to occur.
+
+#### Credential delimiter in URI implies authentication
+
+The presence of a credential delimiter (i.e. @) in the URI connection string is evidence that the user has unambiguously
+specified user information and MUST be interpreted as a user configuring authentication credentials (even if the
+username and/or password are empty strings).
+
+##### Authentication source and URI database do not imply authentication
+
+The presence of a database name in the URI connection string MUST NOT be interpreted as a user configuring
+authentication credentials. The URI database name is only used as a default source for some mechanisms when
+authentication has been configured and a source is required but has not been specified. See individual mechanism
+definitions for details.
+
+Similarly, the presence of the `authSource` option in the URI connection string without other credential data such as
+Userinfo or authentication parameters in connection options MUST NOT be interpreted as a request for authentication.
+
+#### Errors
+
+Drivers SHOULD raise an error as early as possible when detecting invalid values in a credential. For instance, if a
+`mechanism_property` is specified for [MONGODB-CR](#mongodb-cr), the driver should raise an error indicating that the
+property does not apply.
+
+Drivers MUST raise an error if any required information for a mechanism is missing. For instance, if a `username` is not
+specified for SCRAM-SHA-256, the driver must raise an error indicating the the property is missing.
+
+#### Naming
+
+Naming of this information MUST be idiomatic to the driver's language/framework but still remain consistent. For
+instance, python would use "mechanism_properties" and .NET would use "MechanismProperties".
+
+Naming of mechanism properties MUST be case-insensitive. For instance, SERVICE_NAME and service_name refer to the same
+property.
+
+### Authentication
+
+A MongoClient instance MUST be considered a single logical connection to the server/deployment.
+
+Socket connections from a MongoClient to deployment members can be one of two types:
+
+- Monitoring-only socket: multi-threaded drivers maintain monitoring sockets separate from sockets in connection pools.
+- General-use socket: for multi-threaded drivers, these are sockets in connection pools used for (non-monitoring) user
+ operations; in single-threaded drivers, these are used for both monitoring and user operations.
+
+Authentication (including mechanism negotiation) MUST NOT happen on monitoring-only sockets.
+
+If one or more credentials are provided to a MongoClient, then whenever a general-use socket is opened, drivers MUST
+immediately conduct an authentication handshake over that socket.
+
+Drivers SHOULD require all credentials to be specified upon construction of the MongoClient. This is defined as eager
+authentication and drivers MUST support this mode.
+
+#### Authentication Handshake
+
+An authentication handshake consists of an initial `hello` or legacy hello command possibly followed by one or more
+authentication conversations.
+
+Drivers MUST follow the following steps for an authentication handshake:
+
+1. Upon opening a general-use socket to a server for a given MongoClient, drivers MUST issue a
+ [MongoDB Handshake](../mongodb-handshake/handshake.md) immediately. This allows a driver to determine the server
+ type. If the `hello` or legacy hello of the MongoDB Handshake fails with an error, drivers MUST treat this as an
+ authentication error.
+2. If the server is of type RSArbiter, no authentication is possible and the handshake is complete.
+3. Inspect the value of `maxWireVersion`. If the value is greater than or equal to `6`, then the driver MUST use
+ `OP_MSG` for authentication. Otherwise, it MUST use `OP_QUERY`.
+4. If credentials exist: 3.1. A driver MUST authenticate with all credentials provided to the MongoClient. 3.2. A single
+ invalid credential is the same as all credentials being invalid.
+
+If the authentication handshake fails for a socket, drivers MUST mark the server Unknown and clear the server's
+connection pool. (See [Q & A](#q-and-a) below and SDAM's
+[Why mark a server Unknown after an auth error](../server-discovery-and-monitoring/server-discovery-and-monitoring.md#why-mark-a-server-unknown-after-an-auth-error)
+for rationale.)
+
+All blocking operations executed as part of the authentication handshake MUST apply timeouts per the
+[Client Side Operations Timeout](../client-side-operations-timeout/client-side-operations-timeout.md) specification.
+
+#### Mechanism Negotiation via Handshake
+
+- Since: 4.0
+
+If an application provides a username but does not provide an authentication mechanism, drivers MUST negotiate a
+mechanism via a `hello` or legacy hello command requesting a user's supported SASL mechanisms:
+
+```javascript
+{hello: 1, saslSupportedMechs: "."}
+```
+
+In this example `` is the authentication database name that either SCRAM-SHA-1 or SCRAM-SHA-256 would use (they
+are the same; either from the connection string or else defaulting to 'admin') and `` is the username provided
+in the auth credential. The username MUST NOT be modified from the form provided by the user (i.e. do not normalize with
+SASLprep), as the server uses the raw form to look for conflicts with legacy credentials.
+
+If the handshake response includes a `saslSupportedMechs` field, then drivers MUST use the contents of that field to
+select a default mechanism as described later. If the command succeeds and the response does not include a
+`saslSupportedMechs` field, then drivers MUST use the legacy default mechanism rules for servers older than 4.0.
+
+Drivers MUST NOT validate the contents of the `saslSupportedMechs` attribute of the initial handshake reply. Drivers
+MUST NOT raise an error if the `saslSupportedMechs` attribute of the reply includes an unknown mechanism.
+
+### Single-credential drivers
+
+When the authentication mechanism is not specified, drivers that allow only a single credential per client MUST perform
+mechanism negotiation as part of the MongoDB Handshake portion of the authentication handshake. This lets authentication
+proceed without a separate negotiation round-trip exchange with the server.
+
+### Multi-credential drivers
+
+The use of multiple credentials within a driver is discouraged, but some legacy drivers still allow this. Such drivers
+may not have user credentials when connections are opened and thus will not be able to do negotiation.
+
+Drivers with a list of credentials at the time a connection is opened MAY do mechanism negotiation on the initial
+handshake, but only for the first credential in the list of credentials.
+
+When authenticating each credential, if the authentication mechanism is not specified and has not been negotiated for
+that credential:
+
+- If the connection handshake results indicate the server version is 4.0 or later, drivers MUST send a new `hello` or
+ legacy hello negotiation command for the credential to determine the default authentication mechanism.
+- Otherwise, when the server version is earlier than 4.0, the driver MUST select a default authentication mechanism for
+ the credential following the instructions for when the `saslSupportedMechs` field is not present in a legacy hello
+ response.
+
+### Caching credentials in SCRAM
+
+In the implementation of SCRAM authentication mechanisms (e.g. SCRAM-SHA-1 and SCRAM-SHA-256), drivers MUST maintain a
+cache of computed SCRAM credentials. The cache entries SHOULD be identified by the password, salt, iteration count, and
+a value that uniquely identifies the authentication mechanism (e.g. "SHA1" or "SCRAM-SHA-256").
+
+The cache entry value MUST be either the `saltedPassword` parameter or the combination of the `clientKey` and
+`serverKey` parameters.
+
+### Reauthentication
+
+On any operation that requires authentication, the server may raise the error `ReauthenticationRequired` (391),
+typically if the user's credential has expired. Drivers MUST immediately attempt a reauthentication on the connection
+using suitable credentials, as specified by the particular authentication mechanism when this error is raised, and then
+re-attempt the operation. This attempt MUST be irrespective of whether the operation is considered retryable. Drivers
+MUST NOT resend a `hello` message during reauthentication, instead using SASL messages directly. Any errors that could
+not be recovered from during reauthentication, or that were encountered during the subsequent re-attempt of the
+operation MUST be raised to the user.
+
+Currently the only authentication mechanism on the server that supports reauthentication is MONGODB-OIDC. See the
+[MONGODB-OIDC](#mongodb-oidc) section on reauthentication for more details. Note that in order to implement the unified
+spec tests for reauthentication, it may be necessary to add reauthentication support for whichever auth mechanism is
+used when running the authentication spec tests.
+
+### Default Authentication Methods
+
+- Since: 3.0
+- Revised: 4.0
+
+If the user did not provide a mechanism via the connection string or via code, the following logic describes how to
+select a default.
+
+If a `saslSupportedMechs` field was present in the handshake response for mechanism negotiation, then it MUST be
+inspected to select a default mechanism:
+
+```javascript
+{
+ "hello" : true,
+ "saslSupportedMechs": ["SCRAM-SHA-1", "SCRAM-SHA-256"],
+ ...
+ "ok" : 1
+}
+```
+
+If SCRAM-SHA-256 is present in the list of mechanism, then it MUST be used as the default; otherwise, SCRAM-SHA-1 MUST
+be used as the default, regardless of whether SCRAM-SHA-1 is in the list. Drivers MUST NOT attempt to use any other
+mechanism (e.g. PLAIN) as the default.
+
+If `saslSupportedMechs` is not present in the handshake response for mechanism negotiation, then SCRAM-SHA-1 MUST be
+used when talking to servers >= 3.0. Prior to server 3.0, MONGODB-CR MUST be used.
+
+When a user has specified a mechanism, regardless of the server version, the driver MUST honor this.
+
+#### Determining Server Version
+
+Drivers SHOULD use the server's wire version ranges to determine the server's version.
+
+### MONGODB-CR
+
+- Since: 1.4
+- Deprecated: 3.0
+- Removed: 4.0
+
+MongoDB Challenge Response is a nonce and MD5 based system. The driver sends a `getnonce` command, encodes and hashes
+the password using the returned nonce, and then sends an `authenticate` command.
+
+#### Conversation
+
+1. Send `getnonce` command
+
+ ```javascript
+ CMD = { getnonce: 1 }
+ RESP = { nonce: }
+ ```
+
+2. Compute key
+
+ ```javascript
+ passwordDigest = HEX( MD5( UTF8( username + ':mongo:' + password )))
+ key = HEX( MD5( UTF8( nonce + username + passwordDigest )))
+ ```
+
+3. Send `authenticate` command
+
+ ```javascript
+ CMD = { authenticate: 1, nonce: nonce, user: username, key: key }
+ ```
+
+As an example, given a username of "user" and a password of "pencil", the conversation would appear as follows:
+
+```javascript
+CMD = {getnonce : 1}
+RESP = {nonce: "2375531c32080ae8", ok: 1}
+CMD = {authenticate: 1, user: "user", nonce: "2375531c32080ae8", key: "21742f26431831d5cfca035a08c5bdf6"}
+RESP = {ok: 1}
+```
+
+#### [MongoCredential](#mongocredential) Properties
+
+- username
+
+ MUST be specified and non-zero length.
+
+- source
+
+ MUST be specified. Defaults to the database name if supplied on the connection string or `admin`.
+
+- password
+
+ MUST be specified.
+
+- mechanism
+
+ MUST be "MONGODB-CR"
+
+- mechanism_properties
+
+ MUST NOT be specified.
+
+### MONGODB-X509
+
+- Since: 2.6
+- Changed: 3.4
+
+MONGODB-X509 is the usage of X.509 certificates to validate a client where the distinguished subject name of the client
+certificate acts as the username.
+
+When connected to MongoDB 3.4:
+
+- You MUST NOT raise an error when the application only provides an X.509 certificate and no username.
+- If the application does not provide a username you MUST NOT send a username to the server.
+- If the application provides a username you MUST send that username to the server.
+
+When connected to MongoDB 3.2 or earlier:
+
+- You MUST send a username to the server.
+- If no username is provided by the application, you MAY extract the username from the X.509 certificate instead of
+ requiring the application to provide it.
+- If you choose not to automatically extract the username from the certificate you MUST error when no username is
+ provided by the application.
+
+#### Conversation
+
+1. Send `authenticate` command (MongoDB 3.4+)
+
+ ```javascript
+ CMD = {"authenticate": 1, "mechanism": "MONGODB-X509"}
+ RESP = {"dbname" : "$external", "user" : "C=IS,ST=Reykjavik,L=Reykjavik,O=MongoDB,OU=Drivers,CN=client", "ok" : 1}
+ ```
+
+2. Send `authenticate` command with username:
+
+ ```bash
+ username = $(openssl x509 -subject -nameopt RFC2253 -noout -inform PEM -in my-cert.pem)
+ ```
+
+ ```javascript
+ CMD = {authenticate: 1, mechanism: "MONGODB-X509", user: "C=IS,ST=Reykjavik,L=Reykjavik,O=MongoDB,OU=Drivers,CN=client"}
+ RESP = {"dbname" : "$external", "user" : "C=IS,ST=Reykjavik,L=Reykjavik,O=MongoDB,OU=Drivers,CN=client", "ok" : 1}
+ ```
+
+#### [MongoCredential](#mongocredential) Properties
+
+- username
+
+ SHOULD NOT be provided for MongoDB 3.4+ MUST be specified and non-zero length for MongoDB prior to 3.4
+
+- source
+
+ MUST be "$external". Defaults to `$external\`.
+
+- password
+
+ MUST NOT be specified.
+
+- mechanism
+
+ MUST be "MONGODB-X509"
+
+- mechanism_properties
+
+ MUST NOT be specified.
+
+TODO: Errors
+
+### SASL Mechanisms
+
+- Since: 2.4 Enterprise
+
+SASL mechanisms are all implemented using the same sasl commands and interpreted as defined by the
+[SASL specification RFC 4422](http://tools.ietf.org/html/rfc4422).
+
+1. Send the `saslStart` command.
+
+ ```javascript
+ CMD = { saslStart: 1, mechanism: , payload: BinData(...), autoAuthorize: 1 }
+ RESP = { conversationId: , code: , done: , payload: }
+ ```
+
+ - conversationId: the conversation identifier. This will need to be remembered and used for the duration of the
+ conversation.
+ - code: A response code that will indicate failure. This field is not included when the command was successful.
+ - done: a boolean value indicating whether or not the conversation has completed.
+ - payload: a sequence of bytes or a base64 encoded string (depending on input) to pass into the SASL library to
+ transition the state machine.
+
+2. Continue with the `saslContinue` command while `done` is `false`.
+
+ ```javascript
+ CMD = { saslContinue: 1, conversationId: conversationId, payload: BinData(...) }
+ RESP = { conversationId: , code: , done: , payload: }
+ ```
+
+ Many languages will have the ability to utilize 3rd party libraries. The server uses
+ [cyrus-sasl](https://www.cyrusimap.org/sasl/) and it would make sense for drivers with a choice to also choose
+ cyrus. However, it is important to ensure that when utilizing a 3rd party library it does implement the mechanism
+ on all supported OS versions and that it interoperates with the server. For instance, the cyrus sasl library
+ offered on RHEL 6 does not implement SCRAM-SHA-1. As such, if your driver supports RHEL 6, you'll need to implement
+ SCRAM-SHA-1 from scratch.
+
+### GSSAPI
+
+- Since:
+
+ 2.4 Enterprise
+
+ 2.6 Enterprise on Windows
+
+GSSAPI is kerberos authentication as defined in [RFC 4752](http://tools.ietf.org/html/rfc4752). Microsoft has a
+proprietary implementation called SSPI which is compatible with both Windows and Linux clients.
+
+[MongoCredential](#mongocredential) properties:
+
+- username
+
+ MUST be specified and non-zero length.
+
+- source
+
+ MUST be "$external". Defaults to `$external\`.
+
+- password
+
+ MAY be specified. If omitted, drivers MUST NOT pass the username without password to SSPI on Windows and instead use
+ the default credentials.
+
+- mechanism
+
+ MUST be "GSSAPI"
+
+- mechanism_properties
+
+ - SERVICE_NAME
+
+ Drivers MUST allow the user to specify a different service name. The default is "mongodb".
+
+ - CANONICALIZE_HOST_NAME
+
+ Drivers MAY allow the user to request canonicalization of the hostname. This might be required when the hosts report
+ different hostnames than what is used in the kerberos database. The value is a string of either "none", "forward",
+ or "forwardAndReverse". "none" is the default and performs no canonicalization. "forward" performs a forward DNS
+ lookup to canonicalize the hostname. "forwardAndReverse" performs a forward DNS lookup and then a reverse lookup
+ on that value to canonicalize the hostname. The driver MUST fallback to the provided host if any lookup errors or
+ returns no results. Drivers MAY decide to also keep the legacy boolean values where `true` equals the
+ "forwardAndReverse" behaviour and `false` equals "none".
+
+ - SERVICE_REALM
+
+ Drivers MAY allow the user to specify a different realm for the service. This might be necessary to support
+ cross-realm authentication where the user exists in one realm and the service in another.
+
+ - SERVICE_HOST
+
+ Drivers MAY allow the user to specify a different host for the service. This is stored in the service principal name
+ instead of the standard host name. This is generally used for cases where the initial role is being created from
+ localhost but the actual service host would differ.
+
+#### Hostname Canonicalization
+
+Valid values for CANONICALIZE_HOST_NAME are `true`, `false`, "none", "forward", "forwardAndReverse". If a value is
+provided that does not match one of these the driver MUST raise an error.
+
+If CANONICALIZE_HOST_NAME is `false`, "none", or not provided, the driver MUST NOT canonicalize the host name.
+
+If CANONICALIZE_HOST_NAME is `true`, "forward", or "forwardAndReverse", the client MUST canonicalize the name of each
+host it uses for authentication. There are two options. First, if the client's underlying GSSAPI library provides
+hostname canonicalization, the client MAY rely on it. For example, MIT Kerberos has
+[a configuration option for canonicalization](https://web.mit.edu/kerberos/krb5-1.13/doc/admin/princ_dns.html#service-principal-canonicalization).
+
+Second, the client MAY implement its own canonicalization. If so, the canonicalization algorithm MUST be:
+
+```python
+addresses = fetch addresses for host
+if no addresses:
+ throw error
+
+address = first result in addresses
+
+while true:
+ cnames = fetch CNAME records for host
+ if no cnames:
+ break
+
+ # Unspecified which CNAME is used if > 1.
+ host = one of the records in cnames
+
+if forwardAndReverse or true:
+ reversed = do a reverse DNS lookup for address
+ canonicalized = lowercase(reversed)
+else:
+ canonicalized = lowercase(host)
+```
+
+For example, here is a Python implementation of this algorithm using `getaddrinfo` (for address and CNAME resolution)
+and `getnameinfo` (for reverse DNS).
+
+```python
+from socket import *
+import sys
+
+
+def canonicalize(host, mode):
+ # Get a CNAME for host, if any.
+ af, socktype, proto, canonname, sockaddr = getaddrinfo(
+ host, None, 0, 0, IPPROTO_TCP, AI_CANONNAME)[0]
+
+ print('address from getaddrinfo: [%s]' % (sockaddr[0],))
+ print('canonical name from getaddrinfo: [%s]' % (canonname,))
+
+ if (mode == true or mode == 'forwardAndReverse'):
+ try:
+ # NI_NAMEREQD requests an error if getnameinfo fails.
+ name = getnameinfo(sockaddr, NI_NAMEREQD)
+ except gaierror as exc:
+ print('getname info failed: "%s"' % (exc,))
+ return canonname.lower()
+ return name[0].lower()
+ else:
+ return canonname.lower()
+
+
+canonicalized = canonicalize(sys.argv[1])
+print('canonicalized: [%s]' % (canonicalized,))
+```
+
+Beware of a bug in older glibc where `getaddrinfo` uses PTR records instead of CNAMEs if the address family hint is
+AF_INET6, and beware of a bug in older MIT Kerberos that causes it to always do reverse DNS lookup even if the `rdns`
+configuration option is set to `false`.
+
+### PLAIN
+
+- Since: 2.6 Enterprise
+
+The PLAIN mechanism, as defined in [RFC 4616](http://tools.ietf.org/html/rfc4616), is used in MongoDB to perform LDAP
+authentication. It cannot be used to perform any other type of authentication. Since the credentials are stored outside
+of MongoDB, the `$external` database must be used for authentication.
+
+#### Conversation
+
+As an example, given a username of "user" and a password of "pencil", the conversation would appear as follows:
+
+```javascript
+CMD = {saslStart: 1, mechanism: "PLAIN", payload: BinData(0, "AHVzZXIAcGVuY2ls")}
+RESP = {conversationId: 1, payload: BinData(0,""), done: true, ok: 1}
+```
+
+If your sasl client is also sending the authzid, it would be "user" and the conversation would appear as follows:
+
+```javascript
+CMD = {saslStart: 1, mechanism: "PLAIN", payload: BinData(0, "dXNlcgB1c2VyAHBlbmNpbA==")}
+RESP = {conversationId: 1, payload: BinData(0,""), done: true, ok: 1}
+```
+
+MongoDB supports either of these forms.
+
+#### [MongoCredential](#mongocredential) Properties
+
+- username
+
+ MUST be specified and non-zero length.
+
+- source
+
+ MUST be specified. Defaults to the database name if supplied on the connection string or `$external`.
+
+- password
+
+ MUST be specified.
+
+- mechanism
+
+ MUST be "PLAIN"
+
+- mechanism_properties
+
+ MUST NOT be specified.
+
+### SCRAM-SHA-1
+
+- Since: 3.0
+
+SCRAM-SHA-1 is defined in [RFC 5802](http://tools.ietf.org/html/rfc5802).
+
+[Page 11 of the RFC](http://tools.ietf.org/html/rfc5802#page-11) specifies that user names be prepared with SASLprep,
+but drivers MUST NOT do so.
+
+[Page 8 of the RFC](http://tools.ietf.org/html/rfc5802#page-8) identifies the "SaltedPassword" as
+`:= Hi(Normalize(password), salt, i)`. The `password` variable MUST be the mongodb hashed variant. The mongo hashed
+variant is computed as `hash = HEX( MD5( UTF8( username + ':mongo:' + plain_text_password )))`, where
+`plain_text_password` is actually plain text. The `username` and `password` MUST NOT be prepared with SASLprep before
+hashing.
+
+For example, to compute the ClientKey according to the RFC:
+
+```javascript
+// note that "salt" and "i" have been provided by the server
+function computeClientKey(username, plain_text_password) {
+ mongo_hashed_password = HEX( MD5( UTF8( username + ':mongo:' + plain_text_password )));
+ saltedPassword = Hi(Normalize(mongo_hashed_password), salt, i);
+ clientKey = HMAC(saltedPassword, "Client Key");
+}
+```
+
+In addition, SCRAM-SHA-1 requires that a client create a randomly generated nonce. It is imperative, for security sake,
+that this be as secure and truly random as possible. For instance, Java provides both a Random class as well as a
+SecureRandom class. SecureRandom is cryptographically generated while Random is just a pseudo-random generator with
+predictable outcomes.
+
+Additionally, drivers MUST enforce a minimum iteration count of 4096 and MUST error if the authentication conversation
+specifies a lower count. This mitigates downgrade attacks by a man-in-the-middle attacker.
+
+Drivers MUST NOT advertise support for channel binding, as the server does not support it and legacy servers may fail
+authentication if drivers advertise support. I.e. the client-first-message MUST start with `n,`.
+
+Drivers MUST add a top-level `options` field to the saslStart command, whose value is a document containing a field
+named `skipEmptyExchange` whose value is true. Older servers will ignore the `options` field and continue with the
+longer conversation as shown in the "Backwards Compatibility" section. Newer servers will set the `done` field to `true`
+when it responds to the client at the end of the second round trip, showing proof that it knows the password. This will
+shorten the conversation by one round trip.
+
+#### Conversation
+
+As an example, given a username of "user" and a password of "pencil" and an r value of "fyko+d2lbbFgONRv9qkxdawL", a
+SCRAM-SHA-1 conversation would appear as follows:
+
+```javascript
+CMD = "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL"
+RESP = "r=fyko+d2lbbFgONRv9qkxdawLHo+Vgk7qvUOKUwuWLIWg4l/9SraGMHEE,s=rQ9ZY3MntBeuP3E1TDVC4w==,i=10000"
+CMD = "c=biws,r=fyko+d2lbbFgONRv9qkxdawLHo+Vgk7qvUOKUwuWLIWg4l/9SraGMHEE,p=MC2T8BvbmWRckDw8oWl5IVghwCY="
+RESP = "v=UMWeI25JD1yNYZRMpZ4VHvhZ9e0="
+```
+
+This same conversation over MongoDB's SASL implementation would appear as follows:
+
+```javascript
+CMD = {saslStart: 1, mechanism: "SCRAM-SHA-1", payload: BinData(0, "biwsbj11c2VyLHI9ZnlrbytkMmxiYkZnT05Sdjlxa3hkYXdM"), options: { skipEmptyExchange: true }}
+RESP = {conversationId : 1, payload: BinData(0,"cj1meWtvK2QybGJiRmdPTlJ2OXFreGRhd0xIbytWZ2s3cXZVT0tVd3VXTElXZzRsLzlTcmFHTUhFRSxzPXJROVpZM01udEJldVAzRTFURFZDNHc9PSxpPTEwMDAw"), done: false, ok: 1}
+CMD = {saslContinue: 1, conversationId: 1, payload: BinData(0, "Yz1iaXdzLHI9ZnlrbytkMmxiYkZnT05Sdjlxa3hkYXdMSG8rVmdrN3F2VU9LVXd1V0xJV2c0bC85U3JhR01IRUUscD1NQzJUOEJ2Ym1XUmNrRHc4b1dsNUlWZ2h3Q1k9")}
+RESP = {conversationId: 1, payload: BinData(0,"dj1VTVdlSTI1SkQxeU5ZWlJNcFo0Vkh2aFo5ZTA9"), done: true, ok: 1}
+```
+
+#### [MongoCredential](#mongocredential) Properties
+
+- username
+
+ MUST be specified and non-zero length.
+
+- source
+
+ MUST be specified. Defaults to the database name if supplied on the connection string or `admin`.
+
+- password
+
+ MUST be specified.
+
+- mechanism
+
+ MUST be "SCRAM-SHA-1"
+
+- mechanism_properties
+
+ MUST NOT be specified.
+
+### SCRAM-SHA-256
+
+- Since: 4.0
+
+SCRAM-SHA-256 extends [RFC 5802](http://tools.ietf.org/html/rfc5802) and is formally defined in
+[RFC 7677](https://tools.ietf.org/html/rfc7677).
+
+The MongoDB SCRAM-SHA-256 mechanism works similarly to the SCRAM-SHA-1 mechanism, with the following changes:
+
+- The SCRAM algorithm MUST use SHA-256 as the hash function instead of SHA-1.
+- User names MUST NOT be prepared with SASLprep. This intentionally contravenes the "SHOULD" provision of RFC 5802.
+- Passwords MUST be prepared with SASLprep, per RFC 5802. Passwords are used directly for key derivation ; they MUST NOT
+ be digested as they are in SCRAM-SHA-1.
+
+Additionally, drivers MUST enforce a minimum iteration count of 4096 and MUST error if the authentication conversation
+specifies a lower count. This mitigates downgrade attacks by a man-in-the-middle attacker.
+
+Drivers MUST add a top-level `options` field to the saslStart command, whose value is a document containing a field
+named `skipEmptyExchange` whose value is true. Older servers will ignore the `options` field and continue with the
+longer conversation as shown in the "Backwards Compatibility" section. Newer servers will set the `done` field to `true`
+when it responds to the client at the end of the second round trip, showing proof that it knows the password. This will
+shorten the conversation by one round trip.
+
+#### Conversation
+
+As an example, given a username of "user" and a password of "pencil" and an r value of "rOprNGfwEbeRWgbNEkqO", a
+SCRAM-SHA-256 conversation would appear as follows:
+
+```javascript
+CMD = "n,,n=user,r=rOprNGfwEbeRWgbNEkqO"
+RESP = "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096"
+CMD = "c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ="
+RESP = "v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4="
+```
+
+This same conversation over MongoDB's SASL implementation would appear as follows:
+
+```javascript
+CMD = {saslStart: 1, mechanism:"SCRAM-SHA-256", options: {skipEmptyExchange: true}, payload: BinData(0, "biwsbj11c2VyLHI9ck9wck5HZndFYmVSV2diTkVrcU8=")}
+RESP = {conversationId: 1, payload: BinData(0, "cj1yT3ByTkdmd0ViZVJXZ2JORWtxTyVodllEcFdVYTJSYVRDQWZ1eEZJbGopaE5sRiRrMCxzPVcyMlphSjBTTlk3c29Fc1VFamI2Z1E9PSxpPTQwOTY="), done: false, ok: 1}
+CMD = {saslContinue: 1, conversationId: 1, payload: BinData(0, "Yz1iaXdzLHI9ck9wck5HZndFYmVSV2diTkVrcU8laHZZRHBXVWEyUmFUQ0FmdXhGSWxqKWhObEYkazAscD1kSHpiWmFwV0lrNGpVaE4rVXRlOXl0YWc5empmTUhnc3FtbWl6N0FuZFZRPQ==")}
+RESP = {conversationId: 1, payload: BinData(0, "dj02cnJpVFJCaTIzV3BSUi93dHVwK21NaFVaVW4vZEI1bkxUSlJzamw5NUc0PQ=="), done: true, ok: 1}
+```
+
+#### [MongoCredential](#mongocredential) Properties
+
+- username
+
+ MUST be specified and non-zero length.
+
+- source
+
+ MUST be specified. Defaults to the database name if supplied on the connection string or `admin`.
+
+- password
+
+ MUST be specified.
+
+- mechanism
+
+ MUST be "SCRAM-SHA-256"
+
+- mechanism_properties
+
+ MUST NOT be specified.
+
+### MONGODB-AWS
+
+- Since: 4.4
+
+MONGODB-AWS authenticates using AWS IAM credentials (an access key ID and a secret access key),
+[temporary AWS IAM credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html) obtained from
+an [AWS Security Token Service (STS)](https://docs.aws.amazon.com/STS/latest/APIReference/Welcome.html)
+[Assume Role](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html) request, an OpenID Connect ID
+token that supports
+[AssumeRoleWithWebIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html), or
+temporary AWS IAM credentials assigned to an
+[EC2 instance](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html) or ECS task.
+Temporary credentials, in addition to an access key ID and a secret access key, includes a security (or session) token.
+
+MONGODB-AWS requires that a client create a randomly generated nonce. It is imperative, for security sake, that this be
+as secure and truly random as possible. Additionally, the secret access key and only the secret access key is sensitive.
+Drivers MUST take proper precautions to ensure we do not leak this info.
+
+All messages between MongoDB clients and servers are sent as BSON V1.1 Objects in the payload field of saslStart and
+saslContinue. All fields in these messages have a "short name" which is used in the serialized BSON representation and a
+human-readable "friendly name" which is used in this specification. They are as follows:
+
+| Name | Friendly Name | Type | Description |
+| ---- | -------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
+| r | client nonce | BinData Subtype 0 | 32 byte cryptographically secure random number |
+| p | gs2-cb-flag | int32 | The integer representation of the ASCII character 'n' or 'y', i.e., `110` or `121` |
+| s | server nonce | BinData Subtype 0 | 64 bytes total, 32 bytes from the client first message and a 32 byte cryptographically secure random number generated by the server |
+| h | sts host | string | FQDN of the STS service |
+| a | authorization header | string | Authorization header for [AWS Signature Version 4](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html?shortFooter=true) |
+| d | X-AMZ-Date | string | Current date in UTC. See [AWS Signature Version 4](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html?shortFooter=true) |
+| t | X-AMZ-Security-Token | string | Optional AWS security token |
+
+Drivers MUST NOT advertise support for channel binding, as the server does not support it and legacy servers may fail
+authentication if drivers advertise support. The client-first-message MUST set the gs2-cb-flag to the integer
+representation of the ASCII character `n`, i.e., `110`.
+
+#### Conversation
+
+The first message sent by drivers MUST contain a `client nonce` and `gs2-cb-flag`. In response, the server will send a
+`server nonce` and `sts host`. Drivers MUST validate that the server nonce is exactly 64 bytes and the first 32 bytes
+are the same as the client nonce. Drivers MUST also validate that the length of the host is greater than 0 and less than
+or equal to 255 bytes per [RFC 1035](https://tools.ietf.org/html/rfc1035). Drivers MUST reject FQDN names with empty
+labels (e.g., "abc..def"), names that start with a period (e.g., ".abc.def") and names that end with a period (e.g.,
+"abc.def."). Drivers MUST respond to the server's message with an `authorization header` and a `date`.
+
+As an example, given a client nonce value of "dzw1U2IwSEtgaWI0IUxZMVJqc2xuQzNCcUxBc05wZjI=", a MONGODB-AWS conversation
+decoded from BSON to JSON would appear as follows:
+
+Client First
+
+```javascript
+{
+ "r" : new BinData(0, "dzw1U2IwSEtgaWI0IUxZMVJqc2xuQzNCcUxBc05wZjI="),
+ "p" : 110
+}
+```
+
+Server First
+
+```javascript
+{
+ "s" : new BinData(0, "dzw1U2IwSEtgaWI0IUxZMVJqc2xuQzNCcUxBc05wZjIGS0J9EgLwzEZ9dIzr/hnnK2mgd4D7F52t8g9yTC5cIA=="),
+ "h" : "sts.amazonaws.com"
+}
+```
+
+Client Second
+
+```javascript
+{
+ "a" : "AWS4-HMAC-SHA256 Credential=AKIAICGVLKOKZVY3X3DA/20191107/us-east-1/sts/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-mongodb-gs2-cb-flag;x-mongodb-server-nonce, Signature=ab62ce1c75f19c4c8b918b2ed63b46512765ed9b8bb5d79b374ae83eeac11f55",
+ "d" : "20191107T002607Z"
+ "t" : ""
+}
+```
+
+Note that `X-AMZ-Security-Token` is required when using temporary credentials. When using regular credentials, it MUST
+be omitted. Each message above will be encoded as BSON V1.1 objects and sent to the peer as the value of `payload`.
+Therefore, the SASL conversation would appear as:
+
+Client First
+
+```javascript
+{
+ "saslStart" : 1,
+ "mechanism" : "MONGODB-AWS"
+ "payload" : new BinData(0, "NAAAAAVyACAAAAAAWj0lSjp8M0BMKGU+QVAzRSpWfk0hJigqO1V+b0FaVz4QcABuAAAAAA==")
+}
+```
+
+Server First
+
+```javascript
+{
+ "conversationId" : 1,
+ "done" : false,
+ "payload" : new BinData(0, "ZgAAAAVzAEAAAAAAWj0lSjp8M0BMKGU+QVAzRSpWfk0hJigqO1V+b0FaVz5Rj7x9UOBHJLvPgvgPS9sSzZUWgAPTy8HBbI1cG1WJ9gJoABIAAABzdHMuYW1hem9uYXdzLmNvbQAA"),
+ "ok" : 1.0
+}
+```
+
+Client Second:
+
+```javascript
+{
+ "saslContinue" : 1,
+ "conversationId" : 1,
+ "payload" : new BinData(0, "LQEAAAJhAAkBAABBV1M0LUhNQUMtU0hBMjU2IENyZWRlbnRpYWw9QUtJQUlDR1ZMS09LWlZZM1gzREEvMjAxOTExMTIvdXMtZWFzdC0xL3N0cy9hd3M0X3JlcXVlc3QsIFNpZ25lZEhlYWRlcnM9Y29udGVudC1sZW5ndGg7Y29udGVudC10eXBlO2hvc3Q7eC1hbXotZGF0ZTt4LW1vbmdvZGItZ3MyLWNiLWZsYWc7eC1tb25nb2RiLXNlcnZlci1ub25jZSwgU2lnbmF0dXJlPThhMTI0NGZjODYyZTI5YjZiZjc0OTFmMmYwNDE5NDY2ZGNjOTFmZWU1MTJhYTViM2ZmZjQ1NDY3NDEwMjJiMmUAAmQAEQAAADIwMTkxMTEyVDIxMDEyMloAAA==")
+}
+```
+
+In response to the Server First message, drivers MUST send an `authorization header`. Drivers MUST follow the
+[Signature Version 4 Signing Process](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html) to
+calculate the signature for the `authorization header`. The required and optional headers and their associated values
+drivers MUST use for the canonical request (see
+[Summary of Signing Steps](https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html)) are
+specified in the table below. The following pseudocode shows the construction of the Authorization header.
+
+```text
+Authorization: algorithm Credential=access key ID/credential scope, SignedHeaders=SignedHeaders, Signature=signature
+```
+
+The following example shows a finished Authorization header.
+
+```text
+Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7
+```
+
+The following diagram is a summary of the steps drivers MUST follow to calculate the signature.
+
+
+
+| Name | Value |
+| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
+| HTTP Request Method | POST |
+| URI | / |
+| Content-Type\* | application/x-www-form-urlencoded |
+| Content-Length\* | 43 |
+| Host\* | Host field from Server First Message |
+| Region | Derived from Host - see [Region Calculation](#region-calculation) below |
+| X-Amz-Date\* | See [Amazon Documentation](https://docs.aws.amazon.com/general/latest/gr/sigv4_elements.html) |
+| X-Amz-Security-Token\* | Optional, see [Amazon Documentation](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html?shortFooter=true) |
+| X-MongoDB-Server-Nonce\* | Base64 string of server nonce |
+| X-MongoDB-GS2-CB-Flag\* | ASCII lower-case character 'n' or 'y' or 'p' |
+| X-MongoDB-Optional-Data\* | Optional data, base64 encoded representation of the optional object provided by the client |
+| Body | Action=GetCallerIdentity&Version=2011-06-15 |
+
+> [!NOTE]
+> `*`, Denotes a header that MUST be included in SignedHeaders, if present.
+
+#### Region Calculation
+
+To get the region from the host, the driver MUST follow the algorithm expressed in pseudocode below. :
+
+```text
+if the host is invalid according to the rules described earlier
+ the region is undefined and the driver must raise an error.
+else if the host is "aws.amazonaws.com"
+ the region is "us-east-1"
+else if the host contains the character '.' (a period)
+ split the host by its periods. The region is the second label.
+else // the valid host string contains no periods and is not "aws.amazonaws.com"
+ the region is "us-east-1"
+```
+
+Examples are provided below.
+
+| Host | Region | Notes |
+| ------------------------------ | --------- | ------------------------------------------------ |
+| sts.amazonaws.com | us-east-1 | the host is "sts.amazonaws.com"; use `us-east-1` |
+| sts.us-west-2.amazonaws.com | us-west-2 | use the second label |
+| sts.us-west-2.amazonaws.com.ch | us-west-2 | use the second label |
+| example.com | com | use the second label |
+| localhost | us-east-1 | no "`.`" character; use the default region |
+| sts..com | < Error > | second label is empty |
+| .amazonaws.com | < Error > | starts with a period |
+| sts.amazonaws. | < Error > | ends with a period |
+| "" | < Error > | empty string |
+| "string longer than 255" | < Error > | string longer than 255 bytes |
+
+#### [MongoCredential](#mongocredential) Properties
+
+- username
+
+ MAY be specified. The non-sensitive AWS access key.
+
+- source
+
+ MUST be "$external". Defaults to `$external\`.
+
+- password
+
+ MAY be specified. The sensitive AWS secret key.
+
+- mechanism
+
+ MUST be "MONGODB-AWS"
+
+- mechanism_properties
+
+ - AWS_CREDENTIAL_PROVIDER
+
+ An AWS [Custom Credential Provider](#custom-credential-providers) that returns AWS credentials. Drivers MAY allow
+ the user to specify an object or function, depending on what is idiomatic for the driver. This property MUST
+ follow the same API as the driver language's AWS SDK credential provider.
+
+#### Obtaining Credentials
+
+Drivers will need AWS IAM credentials (an access key, a secret access key and optionally a session token) to complete
+the steps in the
+[Signature Version 4 Signing Process](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html?shortFooter=true).
+Regardless of how Drivers obtain credentials the only valid combination of credentials is an access key ID and a secret
+access key or an access key ID, a secret access key and a session token. These values MUST be present in the environment
+or be retrieved via the optional AWS SDK. If credentials are provided in the URI or client options, the driver MUST
+raise an error.
+
+AWS recommends using an SDK to "take care of some of the heavy lifting necessary in successfully making API calls,
+including authentication, retry behavior, and more".
+
+A recommended pattern for drivers with existing custom implementation is to not further enhance existing
+implementations, and take an optional dependency on the AWS SDK. If the SDK is available, use it, otherwise fallback to
+the existing implementation.
+
+One thing to be mindful of when adopting an AWS SDK is that they typically will check for credentials in a shared AWS
+credentials file when one is present, which may be confusing for users relying on the previous authentication handling
+behavior. It would be helpful to include a note like the following:
+
+"Because we are now using the AWS SDK to handle credentials, if you have a shared AWS credentials or config file, then
+those credentials will be used by default if AWS auth environment variables are not set. To override this behavior, set
+`AWS_SHARED_CREDENTIALS_FILE=""` in your shell or set the equivalent environment variable value in your script or
+application. Alternatively, you can create an AWS profile specifically for your MongoDB credentials and set the
+`AWS_PROFILE` environment variable to that profile name."
+
+##### Custom Credential Providers
+
+Drivers that choose to use the AWS SDK to fetch credentials MAY also allow users to provide a custom credential provider
+as an option to the `MongoClient`. The interface for the option provided depends on the individual language SDK and
+drivers MUST consult AWS SDK documentation to determine that format when implementing. The name of the option MUST be
+`AWS_CREDENTIAL_PROVIDER` and be part of the authentication mechanism properties options that can be provided to the
+client.
+
+Drivers MAY expose API for default providers for the following scenarios when applicable in their language's SDK:
+
+1. The default SDK credential provider.
+2. A custom credential provider chain.
+3. A single credential provider of any available SDK options provided by the SDK.
+
+##### Credential Fetching Order
+
+The order in which Drivers MUST search for credentials is:
+
+1. A custom AWS credential provider if the driver supports it.
+2. Environment variables
+3. Using `AssumeRoleWithWebIdentity` if `AWS_WEB_IDENTITY_TOKEN_FILE` and `AWS_ROLE_ARN` are set.
+4. The ECS endpoint if `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` is set. Otherwise, the EC2 endpoint.
+
+> [!NOTE]
+> See *Should drivers support accessing Amazon EC2 instance metadata in Amazon ECS* in [Q & A](#q-and-a)
+>
+> Drivers are not expected to handle
+> [AssumeRole](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html) requests directly. See
+> description of `AssumeRole` below, which is distinct from `AssumeRoleWithWebIdentity` requests that are meant to be
+> handled directly by the driver.
+
+##### Environment variables
+
+AWS Lambda runtimes set several
+[environment variables](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime)
+during initialization. To support AWS Lambda runtimes Drivers MUST check a subset of these variables, i.e.,
+`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN`, for the access key ID, secret access key and
+session token, respectively. The `AWS_SESSION_TOKEN` may or may not be set. However, if `AWS_SESSION_TOKEN` is set
+Drivers MUST use its value as the session token. Drivers implemented in programming languages that support altering
+environment variables MUST always read environment variables dynamically during authorization, to handle the case where
+another part the application has refreshed the credentials.
+
+However, if environment variables are not present during initial authorization, credentials may be fetched from another
+source and cached. Even if the environment variables are present in subsequent authorization attempts, the driver MUST
+use the cached credentials, or refresh them if applicable. This behavior is consistent with how the AWS SDKs behave.
+
+##### AssumeRoleWithWebIdentity
+
+AWS EKS clusters can be configured to automatically provide a valid OpenID Connect ID token and associated role ARN.
+These can be exchanged for temporary credentials using an
+[AssumeRoleWithWebIdentity request](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html).
+
+If the `AWS_WEB_IDENTITY_TOKEN_FILE` and `AWS_ROLE_ARN` environment variables are set, drivers MUST make an
+`AssumeRoleWithWebIdentity` request to obtain temporary credentials. AWS recommends using an AWS Software Development
+Kit (SDK) to make STS requests.
+
+The `WebIdentityToken` value is obtained by reading the contents of the file given by `AWS_WEB_IDENTITY_TOKEN_FILE`. The
+`RoleArn` value is obtained from `AWS_ROLE_ARN`. If `AWS_ROLE_SESSION_NAME` is set, it MUST be used for the
+`RoleSessionName` parameter, otherwise a suitable random name can be chosen. No other request parameters need to be set
+if using an SDK.
+
+If not using an AWS SDK, the request must be made manually. If making a manual request, the `Version` should be
+specified as well. An example manual POST request looks like the following:
+
+```text
+https://sts.amazonaws.com/
+?Action=AssumeRoleWithWebIdentity
+&RoleSessionName=app1
+&RoleArn=
+&WebIdentityToken=
+&Version=2011-06-15
+```
+
+with the header:
+
+```text
+Accept: application/json
+```
+
+The JSON response from the STS endpoint will contain credentials in this format:
+
+```javascript
+{
+ "Credentials": {
+ "AccessKeyId": ,
+ "Expiration": ,
+ "RoleArn": ,
+ "SecretAccessKey": ,
+ "SessionToken":
+ }
+}
+```
+
+Note that the token is called `SessionToken` and not `Token` as it would be with other credential responses.
+
+##### ECS endpoint
+
+If a username and password are not provided and the aforementioned environment variables are not set, drivers MUST query
+a link-local AWS address for temporary credentials. If temporary credentials cannot be obtained then drivers MUST fail
+authentication and raise an error. Drivers SHOULD enforce a 10 second read timeout while waiting for incoming content
+from both the ECS and EC2 endpoints. If the environment variable `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` is set then
+drivers MUST assume that it was set by an AWS ECS agent and use the URI
+`http://169.254.170.2/$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` to obtain temporary credentials. Querying the URI will
+return the JSON response:
+
+```javascript
+{
+ "AccessKeyId": ,
+ "Expiration": ,
+ "RoleArn": ,
+ "SecretAccessKey": ,
+ "Token":
+}
+```
+
+##### EC2 endpoint
+
+If the environment variable `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` is unset, drivers MUST use the EC2 endpoint,
+
+```text
+http://169.254.169.254/latest/meta-data/iam/security-credentials/
+```
+
+with the required header,
+
+```text
+X-aws-ec2-metadata-token:
+```
+
+to access the EC2 instance's metadata. Drivers MUST obtain the role name from querying the URI
+
+```text
+http://169.254.169.254/latest/meta-data/iam/security-credentials/
+```
+
+The role name request also requires the header `X-aws-ec2-metadata-token`. Drivers MUST use v2 of the EC2 Instance
+Metadata Service
+([IMDSv2](https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service/))
+to access the secret token. In other words, Drivers MUST
+
+- Start a session with a simple HTTP PUT request to IMDSv2.
+
+ - The URL is `http://169.254.169.254/latest/api/token`.
+ - The required header is `X-aws-ec2-metadata-token-ttl-seconds`. Its value is the number of seconds the secret token
+ should remain valid with a max of six hours (`21600` seconds).
+
+- Capture the secret token IMDSv2 returned as a response to the PUT request. This token is the value for the header
+ `X-aws-ec2-metadata-token`.
+
+The curl recipe below demonstrates the above. It retrieves a secret token that's valid for 30 seconds. It then uses that
+token to access the EC2 instance's credentials:
+
+```bash
+$ TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 30"`
+$ ROLE_NAME=`curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ -H "X-aws-ec2-metadata-token: $TOKEN"`
+$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE_NAME -H "X-aws-ec2-metadata-token: $TOKEN"
+```
+
+Drivers can test this process using the mock EC2 server in
+[mongo-enterprise-modules](https://github.com/10gen/mongo-enterprise-modules/blob/master/jstests/external_auth/lib/ec2_metadata_http_server.py).
+The script must be run with `python3`:
+
+```bash
+python3 ec2_metadata_http_server.py
+```
+
+To re-direct queries from the EC2 endpoint to the mock server, replace the link-local address (`http://169.254.169.254`)
+with the IP and port of the mock server (by default, `http://localhost:8000`). For example, the curl script above
+becomes:
+
+```bash
+$ TOKEN=`curl -X PUT "http://localhost:8000/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 30"`
+$ ROLE_NAME=`curl http://localhost:8000/latest/meta-data/iam/security-credentials/ -H "X-aws-ec2-metadata-token: $TOKEN"`
+$ curl http://localhost:8000/latest/meta-data/iam/security-credentials/$ROLE_NAME -H "X-aws-ec2-metadata-token: $TOKEN"
+```
+
+The JSON response from both the actual and mock EC2 endpoint will be in this format:
+
+```javascript
+{
+ "Code": "Success",
+ "LastUpdated" : ,
+ "Type": "AWS-HMAC",
+ "AccessKeyId" : ,
+ "SecretAccessKey": ,
+ "Token" : ,
+ "Expiration":
+}
+```
+
+From the JSON response drivers MUST obtain the `access_key`, `secret_key` and `security_token` which will be used during
+the
+[Signature Version 4 Signing Process](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html?shortFooter=true).
+
+##### Caching Credentials
+
+Credentials fetched by the driver using AWS endpoints MUST be cached and reused to avoid hitting AWS rate limitations.
+AWS recommends using a suitable Software Development Kit (SDK) for your language. If that SDK supports credential fetch
+and automatic refresh/caching, then that mechanism can be used in lieu of manual caching.
+
+If using manual caching, the "Expiration" field MUST be stored and used to determine when to clear the cache.
+Credentials are considered valid if they are more than five minutes away from expiring; to the reduce the chance of
+expiration before they are validated by the server. Credentials that are retrieved from environment variables MUST NOT
+be cached.
+
+If there are no current valid cached credentials, the driver MUST initiate a credential request. To avoid adding a
+bottleneck that would override the `maxConnecting` setting, the driver MUST not place a lock on making a request. The
+cache MUST be written atomically.
+
+If AWS authentication fails for any reason, the cache MUST be cleared.
+
+> [!NOTE]
+> Five minutes was chosen based on the AWS documentation for
+> [IAM roles for EC2](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html) : "We make new
+> credentials available at least five minutes before the expiration of the old credentials". The intent is to have some
+> buffer between when the driver fetches the credentials and when the server verifies them.
+
+### MONGODB-OIDC
+
+- Since: 7.0 Enterprise
+
+MONGODB-OIDC authenticates using an [OpenID Connect (OIDC)](https://openid.net/specs/openid-connect-core-1_0.html)
+access token.
+
+There are two OIDC authentication flows that drivers can support: machine-to-machine ("machine") and human-in-the-loop
+("human"). Drivers MUST support the machine authentication flow. Drivers MAY support the human authentication flow.
+
+The MONGODB-OIDC specification refers to the following OIDC concepts:
+
+- **Identity Provider (IdP)**: A service that manages user accounts and authenticates users or applications, such as
+ Okta or OneLogin. In the [Human Authentication Flow](#human-authentication-flow), the
+ [OIDC Human Callback](#oidc-human-callback) interacts directly the IdP. In the
+ [Machine Authentication Flow](#machine-authentication-flow), only the MongoDB server interacts directly the IdP.
+- **Access token**: Used to authenticate requests to protected resources. OIDC access tokens are signed JWT strings.
+- **Refresh token**: Some OIDC providers may return a refresh token in addition to an access token. A refresh token can
+ be used to retrieve new access tokens without requiring a human to re-authorize the application. Refresh tokens are
+ typically only supported by the [Human Authentication Flow](#human-authentication-flow).
+
+#### Machine Authentication Flow
+
+The machine authentication flow is intended to be used in cases where human interaction is not necessary or practical,
+such as to authenticate database access for a web service. Some OIDC documentation refers to the machine authentication
+flow as "workload authentication".
+
+Drivers MUST implement all behaviors described in the MONGODB-OIDC specification, unless the section or block
+specifically says that it only applies to the [Human Authentication Flow](#human-authentication-flow).
+
+#### Human Authentication Flow
+
+The human authentication flow is intended to be used for applications that involve direct human interaction, such as
+database tools or CLIs. Some OIDC documentation refers to the human authentication flow as "workforce authentication".
+
+Drivers that support the [Human Authentication Flow](#human-authentication-flow) MUST implement all behaviors described
+in the MONGODB-OIDC specification, including sections or blocks that specifically say that it only applies the
+[Human Authentication Flow](#human-authentication-flow).
+
+#### [MongoCredential](#mongocredential) Properties
+
+- username
+
+ MAY be specified. Its meaning varies depending on the OIDC provider integration used.
+
+- source
+
+ MUST be "$external". Defaults to `$external\`.
+
+- password
+
+ MUST NOT be specified.
+
+- mechanism
+
+ MUST be "MONGODB-OIDC"
+
+- mechanism_properties
+
+ - ENVIRONMENT
+
+ Drivers MUST allow the user to specify the name of a built-in OIDC application environment integration to use to
+ obtain credentials. If provided, the value MUST be one of `["test", "azure", "gcp", "k8s"]`. If both `ENVIRONMENT`
+ and an [OIDC Callback](#oidc-callback) or [OIDC Human Callback](#oidc-human-callback) are provided for the same
+ `MongoClient`, the driver MUST raise an error.
+
+ - TOKEN_RESOURCE
+
+ The URI of the target resource. If `TOKEN_RESOURCE` is provided and `ENVIRONMENT` is not one of `["azure", "gcp"]`
+ or `TOKEN_RESOURCE` is not provided and `ENVIRONMENT` is one of `["azure", "gcp"]`, the driver MUST raise an
+ error. Note: because the `TOKEN_RESOURCE` is often itself a URL, drivers MUST document that a `TOKEN_RESOURCE`
+ with a comma `,` must be given as a `MongoClient` configuration and not as part of the connection string, and that
+ the `TOKEN_RESOURCE` value can contain a colon `:` character.
+
+ - OIDC_CALLBACK
+
+ An [OIDC Callback](#oidc-callback) that returns OIDC credentials. Drivers MAY allow the user to specify an
+ [OIDC Callback](#oidc-callback) using a `MongoClient` configuration instead of a mechanism property, depending on
+ what is idiomatic for the driver. Drivers MUST NOT support both the `OIDC_CALLBACK` mechanism property and a
+ `MongoClient` configuration.
+
+ - OIDC_HUMAN_CALLBACK
+
+ An [OIDC Human Callback](#oidc-human-callback) that returns OIDC credentials. Drivers MAY allow the user to specify
+ a [OIDC Human Callback](#oidc-human-callback) using a `MongoClient` configuration instead of a mechanism property,
+ depending on what is idiomatic for the driver. Drivers MUST NOT support both the `OIDC_HUMAN_CALLBACK` mechanism
+ property and a `MongoClient` configuration. Drivers MUST return an error if both an
+ [OIDC Callback](#oidc-callback) and `OIDC Human Callback` are provided for the same `MongoClient`. This property
+ is only required for drivers that support the [Human Authentication Flow](#human-authentication-flow).
+
+ - ALLOWED_HOSTS
+
+ The list of allowed hostnames or ip-addresses (ignoring ports) for MongoDB connections. The hostnames may include a
+ leading "\*." wildcard, which allows for matching (potentially nested) subdomains. `ALLOWED_HOSTS` is a security
+ feature and MUST default to
+ `["*.mongodb.net", "*.mongodb-qa.net", "*.mongodb-dev.net", "*.mongodbgov.net", "localhost", "127.0.0.1", "::1", "*.mongo.com"]`.
+ When MONGODB-OIDC authentication using a [OIDC Human Callback](#oidc-human-callback) is attempted against a
+ hostname that does not match any of list of allowed hosts, the driver MUST raise a client-side error without
+ invoking any user-provided callbacks. This value MUST NOT be allowed in the URI connection string. The hostname
+ check MUST be performed after SRV record resolution, if applicable. This property is only required for drivers
+ that support the [Human Authentication Flow](#human-authentication-flow).
+
+ - AWS_CREDENTIAL_PROVIDER
+
+ A function or object from the AWS SDK that can be used to return AWS credentials. Drivers MAY allow the user to
+ specify the callback using a `MongoClient` configuration instead of a mechanism property, depending on what is
+ idiomatic for the driver.
+
+
+
+#### Built-in OIDC Environment Integrations
+
+Drivers MUST support all of the following built-in OIDC application environment integrations.
+
+**Test**
+
+The test integration is enabled by setting auth mechanism property `ENVIRONMENT:test`. It is meant for driver testing
+purposes, and is not meant to be documented as a user-facing feature.
+
+If enabled, drivers MUST generate a token using a script in the `auth_oidc`
+[folder](https://github.com/mongodb-labs/drivers-evergreen-tools/tree/master/.evergreen/auth_oidc#readme) in Drivers
+Evergreen Tools. The driver MUST then set the `OIDC_TOKEN_FILE` environment variable to the path to that file. At
+runtime, the driver MUST use the `OIDC_TOKEN_FILE` environment variable and read the OIDC access token from that path.
+The driver MUST use the contents of that file as value in the `jwt` field of the `saslStart` payload.
+
+Drivers MAY implement the "test" integration so that it conforms to the function signature of the
+[OIDC Callback](#oidc-callback) to prevent having to re-implement the "test" integration logic in the OIDC prose tests.
+
+**Azure**
+
+The Azure provider integration is enabled by setting auth mechanism property `ENVIRONMENT:azure`.
+
+If enabled, drivers MUST use an internal machine callback that calls the
+[Azure Instance Metadata Service](https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service)
+and parse the JSON response body, as follows:
+
+Make an HTTP GET request to
+
+```text
+http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=&client_id=
+```
+
+with headers
+
+```text
+Accept: application/json
+Metadata: true
+```
+
+where `` is the url-encoded value of the `TOKEN_RESOURCE` mechanism property and `` is the
+`username` from the connection string. If a `username` is not provided, the `client_id` query parameter should be
+omitted. The timeout should equal the `callbackTimeoutMS` parameter given to the callback.
+
+```bash
+curl -X GET \
+ -H "Accept: application/json" \
+ -H "Metadata: true" \
+ --max-time $CALLBACK_TIMEOUT_MS \
+ "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=$ENCODED_TOKEN_RESOURCE"
+```
+
+The JSON response will be in this format:
+
+```json
+{
+ "access_token": "eyJ0eXAi...",
+ "refresh_token": "",
+ "expires_in": "3599",
+ "expires_on": "1506484173",
+ "not_before": "1506480273",
+ "resource": "https://management.azure.com/",
+ "token_type": "Bearer"
+}
+```
+
+The driver MUST use the returned `"access_token"` value as the access token in a `JwtStepRequest`. If the response does
+not return a status code of 200, the driver MUST raise an error including the HTTP response body.
+
+For more details, see
+[How to use managed identities for Azure resources on an Azure VM to acquire an access token](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token).
+
+The callback itself MUST not perform any caching, and the driver MUST cache its tokens in the same way as if a custom
+callback had been provided by the user.
+
+For details on test environment setup, see the README in
+[Drivers-Evergreen-Tools](https://github.com/mongodb-labs/drivers-evergreen-tools/blob/master/.evergreen/auth_oidc/azure/README.md).
+
+**GCP**
+
+The GCP provider integration is enabled by setting auth mechanism property `ENVIRONMENT:gcp`.
+
+If enabled, drivers MUST use an internal machine callback that calls the
+[Google Cloud VM metadata](https://cloud.google.com/compute/docs/metadata/overview) endpoint and parse the JSON response
+body, as follows:
+
+Make an HTTP GET request to
+
+```text
+http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=
+```
+
+with headers
+
+```text
+Metadata-Flavor: Google
+```
+
+where `` is the url-encoded value of the `TOKEN_RESOURCE` mechanism property. The timeout should equal the
+`callbackTimeoutMS` parameter given to the callback.
+
+```bash
+curl -X GET \
+ -H "Metadata-Flavor: Google" \
+ --max-time $CALLBACK_TIMEOUT_MS \
+ "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=$ENCODED_TOKEN_RESOURCE"
+```
+
+The response body will be the access token itself.
+
+The driver MUST use the returned value as the access token in a `JwtStepRequest`. If the response does not return a
+status code of 200, the driver MUST raise an error including the HTTP response body.
+
+For more details, see [View and query VM metadata](https://cloud.google.com/compute/docs/metadata/querying-metadata).
+
+The callback itself MUST not perform any caching, and the driver MUST cache its tokens in the same way as if a custom
+callback had been provided by the user.
+
+For details on test environment setup, see the README in
+[Drivers-Evergreen-Tools](https://github.com/mongodb-labs/drivers-evergreen-tools/blob/master/.evergreen/auth_oidc/gcp/README.md).
+
+***Kubernetes***
+
+The Kubernetes integration is enabled by setting auth mechanism property `ENVIRONMENT:k8s`. In this configuration, the
+driver is expected to be running inside a Kubernetes environment with a configured
+[ServiceAccount](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/#bound-service-account-token-volume).
+
+If enabled, drivers MUST read the contents of the token from the local file path found using the following algorithm:
+
+```python
+if 'AZURE_FEDERATED_TOKEN_FILE' in os.environ:
+ fname = os.environ['AZURE_FEDERATED_TOKEN_FILE']
+elif 'AWS_WEB_IDENTITY_TOKEN_FILE' in os.environ:
+ fname = os.environ['AWS_WEB_IDENTITY_TOKEN_FILE']
+else:
+ fname = '/var/run/secrets/kubernetes.io/serviceaccount/token'
+```
+
+Where `AZURE_FEDERATED_TOKEN_FILE` contains the file path on Azure Kubernetes Service (AKS),
+`AWS_WEB_IDENTITY_TOKEN_FILE` contains the file path on Elastic Kubernetes Service (EKS), and
+`/var/run/secrets/kubernetes.io/serviceaccount/token` is the default path for a Kubernetes
+[ServiceAccount token](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/#serviceaccount-admission-controller),
+which is used by Google Kubernetes Engine (GKE).
+
+The callback itself MUST not perform any caching, and the driver MUST cache its tokens in the same way as if a custom
+callback had been provided by the user.
+
+For details on test environment setup, see the README in
+[Drivers-Evergreen-Tools](https://github.com/mongodb-labs/drivers-evergreen-tools/blob/master/.evergreen/auth_oidc/k8s/README.md).
+
+#### OIDC Callback
+
+Drivers MUST allow users to provide a callback that returns an OIDC access token. The purpose of the callback is to
+allow users to integrate with OIDC providers not supported by the
+[Built-in Provider Integrations](#built-in-provider-integrations). Callbacks can be synchronous or asynchronous,
+depending on the driver and/or language. Asynchronous callbacks should be preferred when other operations in the driver
+use asynchronous functions.
+
+Drivers MUST provide a way for the callback to be either automatically canceled, or to cancel itself. This can be as a
+timeout argument to the callback, a cancellation context passed to the callback, or some other language-appropriate
+mechanism. The timeout value MUST be `min(remaining connectTimeoutMS, remaining timeoutMS)` as described in the Server
+Selection section of the CSOT spec. If CSOT is not applied, then the driver MUST use 1 minute as the timeout.
+
+The driver MUST pass the following information to the callback:
+
+- `timeout`: A timeout, in milliseconds, a deadline, or a `timeoutContext`.
+
+- `username`: The username given as part of the connection string or `MongoClient` parameter.
+
+- `version`: The callback API version number. The version number is used to communicate callback API changes that are
+ not breaking but that users may want to know about and review their implementation. Drivers MUST pass `1` for the
+ initial callback API version number and increment the version number anytime the API changes. Note that this may
+ eventually lead to some drivers having different callback version numbers.
+
+ For example, users may add the following check in their callback:
+
+ ```typescript
+ if(params.version > 1) {
+ throw new Error("OIDC callback API has changed!");
+ }
+ ```
+
+The callback MUST be able to return the following information:
+
+- `accessToken`: An OIDC access token string. The driver MUST NOT attempt to validate `accessToken` directly.
+- `expiresIn`: An optional expiry duration for the access token. Drivers with optional parameters MAY interpret a
+ missing value as infinite. Drivers MUST error if a negative value is returned. Drivers SHOULD use the most idiomatic
+ type for representing a duration in the driver's language. Note that the access token expiry value is currently not
+ used in [Credential Caching](#credential-caching), but is intended to support future caching optimizations.
+
+The signature and naming of the callback API is up to the driver's discretion. Drivers MUST ensure that additional
+optional input parameters and return values can be added to the callback signature in the future without breaking
+backward compatibility.
+
+An example callback API might look like:
+
+```typescript
+interface OIDCCallbackParams {
+ callbackTimeoutMS: int;
+ username: str;
+ version: int;
+}
+
+interface OIDCCredential {
+ accessToken: string;
+ expiresInSeconds: Optional;
+}
+
+function oidcCallback(params: OIDCCallbackParams): OIDCCredential
+```
+
+##### OIDC Human Callback
+
+The human callback is an OIDC callback that includes additional information that is required when using the
+[Human Authentication Flow](#human-authentication-flow). Drivers that support the
+[Human Authentication Flow](#human-authentication-flow) MUST implement the human callback.
+
+In addition to the information described in the [OIDC Callback](#oidc-callback) section, drivers MUST be able to pass
+the following information to the callback:
+
+- `idpInfo`: Information used to authenticate with the IdP.
+ - `issuer`: A URL which describes the Authentication Server. This identifier should be the iss of provided access
+ tokens, and be viable for RFC8414 metadata discovery and RFC9207 identification.
+ - `clientId`: A unique client ID for this OIDC client.
+ - `requestScopes`: A list of additional scopes to request from IdP.
+- `refreshToken`: The refresh token, if applicable, to be used by the callback to request a new token from the issuer.
+
+In addition to the information described in the [OIDC Callback](#oidc-callback) section, the callback MUST be able to
+return the following information:
+
+- `refreshToken`: An optional refresh token that can be used to fetch new access tokens.
+
+The signature and naming of the callback API is up to the driver's discretion. Drivers MAY use a single callback API for
+both callback types or separate callback APIs for each callback type. Drivers MUST ensure that additional optional input
+parameters and return values can be added to the callback signature in the future without breaking backward
+compatibility.
+
+An example human callback API might look like:
+
+```typescript
+interface IdpInfo {
+ issuer: string;
+ clientId: Optional;
+ requestScopes: Optional>;
+}
+
+interface OIDCCallbackParams {
+ username: str;
+ callbackTimeoutMS: int;
+ version: int;
+ idpInfo: Optional;
+ refreshToken: Optional;
+}
+
+interface OIDCCredential {
+ accessToken: string;
+ expiresInSeconds: Optional;
+ refreshToken: Optional;
+}
+
+function oidcCallback(params: OIDCCallbackParams): OIDCCredential
+```
+
+When a human callback is provided, drivers MUST use the following behaviors when calling the callback:
+
+- The driver MUST pass the `IdpInfo` and the refresh token (if available) to the callback.
+ - If there is no cached `IdpInfo`, drivers MUST start a [Two-Step](#two-step) conversation before calling the human
+ callback. See the Conversation and [Credential Caching](#credential-caching) sections for more details.
+- The timeout duration MUST be 5 minutes. This is to account for the human interaction required to complete the
+ callback. In this case, the callback is not subject to CSOT.
+
+#### Conversation
+
+OIDC supports two conversation styles: one-step and two-step. The server detects whether the driver is using a one-step
+or two-step conversation based on the structure of the `saslStart` payload.
+
+##### One-Step
+
+A one-step conversation is used for OIDC providers that allow direct access to an access token. For example, an OIDC
+provider configured for machine-to-machine authentication may provide an access token via a local file pre-loaded on an
+application host.
+
+Drivers MUST use a one-step conversation when using a cached access token, one of the
+[Built-in Provider Integrations](#built-in-provider-integrations), or an [OIDC Callback](#oidc-callback) (not an
+[OIDC Human Callback](#oidc-human-callback)).
+
+The one-step conversation starts with a `saslStart` containing a `JwtStepRequest` payload. The value of `jwt` is the
+OIDC access token string.
+
+```typescript
+interface JwtStepRequest:
+ // Compact serialized JWT with signature.
+ jwt: string;
+}
+```
+
+An example OIDC one-step SASL conversation with access token string "abcd1234" looks like:
+
+```javascript
+// Client:
+{
+ saslStart: 1,
+ mechanism: "MONGODB-OIDC",
+ db: "$external"
+ // payload is a BSON generic binary field containing a JwtStepRequest BSON
+ // document: {"jwt": "abcd1234"}
+ payload: BinData(0, "FwAAAAJqd3QACQAAAGFiY2QxMjM0AAA=")
+}
+
+// Server:
+{
+ conversationId : 1,
+ payload: BinData(0, ""),
+ done: true,
+ ok: 1
+}
+```
+
+##### Two-Step
+
+A two-step conversation is used for OIDC providers that require an extra authorization step before issuing a credential.
+For example, an OIDC provider configured for end-user authentication may require redirecting the user to a webpage so
+they can authorize the request.
+
+Drivers that support the [Human Authentication Flow](#human-authentication-flow) MUST implement the two-step
+conversation. Drivers MUST use a two-step conversation when using a [OIDC Human Callback](#oidc-human-callback) and when
+there is no cached access token.
+
+The two-step conversation starts with a `saslStart` containing a `PrincipalStepRequest` payload. The value of `n` is the
+`username` from the connection string. If a `username` is not provided, field `n` should be omitted.
+
+```typescript
+interface PrincipalStepRequest {
+ // Name of the OIDC user principal.
+ n: Optional;
+}
+```
+
+The server uses `n` (if provided) to select an appropriate IdP. Note that the principal name is optional as it may be
+provided by the IdP in environments where only one IdP is used.
+
+The server responds to the `PrincipalStepRequest` with `IdpInfo` for the selected IdP:
+
+```typescript
+interface IdpInfo {
+ // A URL which describes the Authentication Server. This identifier should
+ // be the iss of provided access tokens, and be viable for RFC8414 metadata
+ // discovery and RFC9207 identification.
+ issuer: string;
+
+ // A unique client ID for this OIDC client.
+ clientId: string;
+
+ // A list of additional scopes to request from IdP.
+ requestScopes: Optional>;
+}
+```
+
+The driver passes the IdP information to the [OIDC Human Callback](#oidc-human-callback), which should return an OIDC
+credential containing an access token and, optionally, a refresh token.
+
+The driver then sends a `saslContinue` with a `JwtStepRequest` payload to complete authentication. The value of `jwt` is
+the OIDC access token string.
+
+```typescript
+interface JwtStepRequest:
+ // Compact serialized JWT with signature.
+ jwt: string;
+}
+```
+
+An example OIDC two-step SASL conversation with username "myidp" and access token string "abcd1234" looks like:
+
+```javascript
+// Client:
+{
+ saslStart: 1,
+ mechanism: "MONGODB-OIDC",
+ db: "$external",
+ // payload is a BSON generic binary field containing a PrincipalStepRequest
+ // BSON document: {"n": "myidp"}
+ payload: BinData(0, "EgAAAAJuAAYAAABteWlkcAAA")
+}
+
+// Server:
+{
+ conversationId : 1,
+ // payload is a BSON generic binary field containing an IdpInfo BSON document:
+ // {"issuer": "https://issuer", "clientId": "abcd", "requestScopes": ["a","b"]}
+ payload: BinData(0, "WQAAAAJpc3N1ZXIADwAAAGh0dHBzOi8vaXNzdWVyAAJjbGllbnRJZAAFAAAAYWJjZAAEcmVxdWVzdFNjb3BlcwAXAAAAAjAAAgAAAGEAAjEAAgAAAGIAAAA="),
+ done: false,
+ ok: 1
+}
+
+// Client:
+{
+ saslContinue: 1,
+ conversationId: 1,
+ // payload is a BSON generic binary field containing a JwtStepRequest BSON
+ // document: {"jwt": "abcd1234"}
+ payload: BinData(0, "FwAAAAJqd3QACQAAAGFiY2QxMjM0AAA=")
+}
+
+// Server:
+{
+ conversationId: 1,
+ payload: BinData(0, ""),
+ done: true,
+ ok: 1
+}
+```
+
+#### Credential Caching
+
+Some OIDC providers may impose rate limits, incur per-request costs, or be slow to return. To minimize those issues,
+drivers MUST cache and reuse access tokens returned by OIDC providers.
+
+Drivers MUST cache the most recent access token per `MongoClient` (henceforth referred to as the *Client Cache*).
+Drivers MAY store the *Client Cache* on the `MongoClient` object or any object that guarantees exactly 1 cached access
+token per `MongoClient`. Additionally, drivers MUST cache the access token used to authenticate a connection on the
+connection object (henceforth referred to as the *Connection Cache*).
+
+Drivers MUST ensure that only one call to the configured provider or OIDC callback can happen at a time. To avoid adding
+a bottleneck that would override the `maxConnecting` setting, the driver MUST NOT hold an exclusive lock while running
+`saslStart` or `saslContinue`.
+
+Example code for credential caching using the read-through cache pattern:
+
+```python
+def get_access_token():
+ # Lock the OIDC authenticator so that only one caller can modify the cache
+ # and call the configured OIDC provider at a time.
+ client.oidc_cache.lock()
+
+ # Check if we can use the access token from the Client Cache or if we need
+ # to fetch and cache a new access token from the OIDC provider.
+ access_token = client.oidc_cache.access_token
+ is_cache = True
+ if access_token is None
+ credential = oidc_provider()
+ is_cache = False
+ client.oidc_cache.access_token = credential.access_token
+
+ client.oidc_cache.unlock()
+
+ return access_token, is_cache
+```
+
+Drivers MUST have a way to invalidate a specific access token from the *Client Cache*. Invalidation MUST only clear the
+cached access token if it is the same as the invalid access token and MUST be an atomic operation (e.g. using a mutex or
+a compare-and-swap operation).
+
+Example code for invalidation:
+
+```python
+def invalidate(access_token):
+ client.oidc_cache.lock()
+
+ if client.oidc_cache.access_token == access_token:
+ client.oidc_cache.access_token = None
+
+ client.oidc_cache.unlock()
+```
+
+Drivers that support the [Human Authentication Flow](#human-authentication-flow) MUST also cache the `IdPInfo` and
+refresh token in the *Client Cache* when a [OIDC Human Callback](#oidc-human-callback) is configured.
+
+**Authentication**
+
+Use the following algorithm to authenticate a new connection:
+
+- Check if the the *Client Cache* has an access token.
+ - If it does, cache the access token in the *Connection Cache* and perform a `One-Step` SASL conversation using the
+ access token in the *Client Cache*. If the server returns a Authentication error (18), invalidate that access
+ token. Raise any other errors to the user. On success, exit the algorithm.
+- Call the configured built-in provider integration or the OIDC callback to retrieve a new access token. Wait until it
+ has been at least 100ms since the last callback invocation, to avoid overloading the callback.
+- Cache the new access token in the *Client Cache* and *Connection Cache*.
+- Perform a `One-Step` SASL conversation using the new access token. Raise any errors to the user.
+
+Example code to authenticate a connection using the `get_access_token` and `invalidate` functions described above:
+
+```python
+def auth(connection):
+ access_token, is_cache = get_access_token()
+
+ # If there is a cached access token, try to authenticate with it. If
+ # authentication fails with an Authentication error (18),
+ # invalidate the access token, fetch a new access token, and try
+ # to authenticate again.
+ # If the server fails for any other reason, do not clear the cache.
+ if is_cache:
+ try:
+ connection.oidc_cache.access_token = access_token
+ sasl_start(connection, payload={"jwt": access_token})
+ return
+ except ServerError as e:
+ if e.code == 18:
+ invalidate(access_token)
+ access_token, _ = get_access_token()
+ else:
+ raise e # Raise other errors.
+
+ connection.oidc_cache.access_token = access_token
+ sasl_start(connection, payload={"jwt": access_token})
+```
+
+For drivers that support the [Human Authentication Flow](#human-authentication-flow), use the following algorithm to
+authenticate a new connection when a [OIDC Human Callback](#oidc-human-callback) is configured:
+
+- Check if the *Client Cache* has an access token.
+ - If it does, cache the access token in the *Connection Cache* and perform a [One-Step](#one-step) SASL conversation
+ using the access token. If the server returns an Authentication error (18), invalidate the access token token from
+ the *Client Cache*, clear the *Connection Cache*, and restart the authentication flow. Raise any other errors to
+ the user. On success, exit the algorithm.
+- Check if the *Client Cache* has a refresh token.
+ - If it does, call the [OIDC Human Callback](#oidc-human-callback) with the cached refresh token and `IdpInfo` to get
+ a new access token. Cache the new access token in the *Client Cache* and *Connection Cache*. Perform a
+ [One-Step](#one-step) SASL conversation using the new access token. If the the server returns an Authentication
+ error (18), clear the refresh token, invalidate the access token from the *Client Cache*, clear the *Connection
+ Cache*, and restart the authentication flow. Raise any other errors to the user. On success, exit the algorithm.
+- Start a new [Two-Step](#two-step) SASL conversation.
+- Run a `PrincipalStepRequest` to get the `IdpInfo`.
+- Call the [OIDC Human Callback](#oidc-human-callback) with the new `IdpInfo` to get a new access token and optional
+ refresh token. Drivers MUST NOT pass a cached refresh token to the callback when performing a new
+ [Two-Step](#two-step) conversation.
+- Cache the new `IdpInfo` and refresh token in the *Client Cache* and the new access token in the *Client Cache* and
+ *Connection Cache*.
+- Attempt to authenticate using a `JwtStepRequest` with the new access token. Raise any errors to the user.
+
+#### Speculative Authentication
+
+Drivers MUST implement speculative authentication for MONGODB-OIDC during the `hello` handshake. Drivers MUST NOT
+attempt speculative authentication if the *Client Cache* does not have a cached access token. Drivers MUST NOT
+invalidate tokens from the *Client Cache* if speculative authentication does not succeed.
+
+Use the following algorithm to perform speculative authentication:
+
+- Check if the *Client Cache* has an access token.
+ - If it does, cache the access token in the *Connection Cache* and send a `JwtStepRequest` with the cached access
+ token in the speculative authentication SASL payload. If the response is missing a speculative authentication
+ document or the speculative authentication document indicates authentication was not successful, clear the the
+ *Connection Cache* and proceed to the next step.
+- Authenticate with the standard authentication handshake.
+
+Example code for speculative authentication using the `auth` function described above:
+
+```python
+def speculative_auth(connection):
+ access_token = client.oidc_cache.access_token
+ if access_token != None:
+ connection.oidc_cache.access_token = access_token
+ res = hello(connection, payload={"jwt": access_token})
+ if res.speculative_authenticate.done:
+ return
+
+ connection.oidc_cache.access_token = None
+ auth(connection)
+```
+
+#### Reauthentication
+
+If any operation fails with `ReauthenticationRequired` (error code 391) and MONGODB-OIDC is in use, the driver MUST
+reauthenticate the connection. Drivers MUST NOT resend a `hello` message during reauthentication, instead using SASL
+messages directly. Drivers MUST NOT try to use Speculative Authentication during reauthentication. See the main
+[reauthentication](#reauthentication) section for more information.
+
+To reauthenticate a connection, invalidate the access token stored on the connection (i.e. the *Connection Cache*) from
+the *Client Cache*, fetch a new access token, and re-run the SASL conversation.
+
+Example code for reauthentication using the `auth` function described above:
+
+```python
+def reauth(connection):
+ invalidate(connection.oidc_cache.access_token)
+ connection.oidc_cache.access_token = None
+ auth(connection)
+```
+
+### Connection String Options
+
+`mongodb://[username[:password]@]host1[:port1][,[host2:[port2]],...[hostN:[portN]]][/database][?options]`
+
+
+
+#### Auth Related Options
+
+- authMechanism
+
+ MONGODB-CR, MONGODB-X509, GSSAPI, PLAIN, SCRAM-SHA-1, SCRAM-SHA-256, MONGODB-AWS
+
+ Sets the Mechanism property on the MongoCredential. When not set, the default will be one of SCRAM-SHA-256,
+ SCRAM-SHA-1 or MONGODB-CR, following the auth spec default mechanism rules.
+
+- authSource
+
+ Sets the Source property on the MongoCredential.
+
+For GSSAPI, MONGODB-X509 and MONGODB-AWS authMechanisms the authSource defaults to `$external`. For PLAIN the authSource
+defaults to the database name if supplied on the connection string or `$external`. For MONGODB-CR, SCRAM-SHA-1 and
+SCRAM-SHA-256 authMechanisms, the authSource defaults to the database name if supplied on the connection string or
+`admin`.
+
+- authMechanismProperties=PROPERTY_NAME:PROPERTY_VALUE,PROPERTY_NAME2:PROPERTY_VALUE2
+
+ A generic method to set mechanism properties in the connection string.
+
+For example, to set REALM and CANONICALIZE_HOST_NAME, the option would be
+`authMechanismProperties=CANONICALIZE_HOST_NAME:forward,SERVICE_REALM:AWESOME`.
+
+- gssapiServiceName (deprecated)
+
+ An alias for `authMechanismProperties=SERVICE_NAME:mongodb`.
+
+#### Errors
+
+Drivers MUST raise an error if the `authSource` option is specified in the connection string with an empty value, e.g.
+`mongodb://localhost/admin?authSource=`.
+
+#### Implementation
+
+1. Credentials MAY be specified in the connection string immediately after the scheme separator "//".
+
+2. A realm MAY be passed as a part of the username in the url. It would be something like , where dev
+ is the username and MONGODB.COM is the realm. Per the RFC, the @ symbol should be url encoded using %40.
+
+ - When GSSAPI is specified, this should be interpreted as the realm.
+ - When non-GSSAPI is specified, this should be interpreted as part of the username.
+
+3. It is permissible for only the username to appear in the connection string. This would be identified by having no
+ colon follow the username before the '@' hostname separator.
+
+4. The source is determined by the following:
+
+ - if authSource is specified, it is used.
+ - otherwise, if database is specified, it is used.
+ - otherwise, the admin database is used.
+
+## Test Plan
+
+Connection string tests have been defined in the associated files:
+
+- [Connection String](./tests/legacy/connection-string.json).
+
+### SCRAM-SHA-256 and mechanism negotiation
+
+Testing SCRAM-SHA-256 requires server version 3.7.3 or later with `featureCompatibilityVersion` of "4.0" or later.
+
+Drivers that allow specifying auth parameters in code as well as via connection string should test both for the test
+cases described below.
+
+#### Step 1
+
+Create three test users, one with only SHA-1, one with only SHA-256 and one with both. For example:
+
+```javascript
+db.runCommand({createUser: 'sha1', pwd: 'sha1', roles: ['root'], mechanisms: ['SCRAM-SHA-1']})
+db.runCommand({createUser: 'sha256', pwd: 'sha256', roles: ['root'], mechanisms: ['SCRAM-SHA-256']})
+db.runCommand({createUser: 'both', pwd: 'both', roles: ['root'], mechanisms: ['SCRAM-SHA-1', 'SCRAM-SHA-256']})
+```
+
+#### Step 2
+
+For each test user, verify that you can connect and run a command requiring authentication for the following cases:
+
+- Explicitly specifying each mechanism the user supports.
+- Specifying no mechanism and relying on mechanism negotiation.
+
+For the example users above, the `dbstats` command could be used as a test command.
+
+For a test user supporting both SCRAM-SHA-1 and SCRAM-SHA-256, drivers should verify that negotiation selects
+SCRAM-SHA-256. This may require monkey patching, manual log analysis, etc.
+
+#### Step 3
+
+For test users that support only one mechanism, verify that explicitly specifying the other mechanism fails.
+
+For a non-existent username, verify that not specifying a mechanism when connecting fails with the same error type that
+would occur with a correct username but incorrect password or mechanism. (Because negotiation with a non-existent user
+name at one point during server development caused a handshake error, we want to verify this is seen by users as similar
+to other authentication errors, not as a network or database command error on the `hello` or legacy hello commands
+themselves.)
+
+#### Step 4
+
+To test SASLprep behavior, create two users:
+
+1. username: "IX", password "IX"
+2. username: "\\u2168" (ROMAN NUMERAL NINE), password "\\u2163" (ROMAN NUMERAL FOUR)
+
+To create the users, use the exact bytes for username and password without SASLprep or other normalization and specify
+SCRAM-SHA-256 credentials:
+
+```javascript
+db.runCommand({createUser: 'IX', pwd: 'IX', roles: ['root'], mechanisms: ['SCRAM-SHA-256']})
+db.runCommand({createUser: '\\u2168', pwd: '\\u2163', roles: ['root'], mechanisms: ['SCRAM-SHA-256']})
+```
+
+For each user, verify that the driver can authenticate with the password in both SASLprep normalized and non-normalized
+forms:
+
+- User "IX": use password forms "IX" and "I\\u00ADX"
+- User "\\u2168": use password forms "IV" and "I\\u00ADV"
+
+As a URI, those have to be UTF-8 encoded and URL-escaped, e.g.:
+
+- `mongodb://IX:IX@mongodb.example.com/admin`
+- `mongodb://IX:I%C2%ADX@mongodb.example.com/admin`
+- `mongodb://%E2%85%A8:IV@mongodb.example.com/admin`
+- `mongodb://%E2%85%A8:I%C2%ADV@mongodb.example.com/admin`
+
+### Speculative Authentication
+
+See the speculative authentication section in the [MongoDB Handshake spec](../mongodb-handshake/handshake.md).
+
+### Minimum iteration count
+
+For SCRAM-SHA-1 and SCRAM-SHA-256, test that the minimum iteration count is respected. This may be done via unit testing
+of an underlying SCRAM library.
+
+## Backwards Compatibility
+
+Drivers may need to remove support for association of more than one credential with a MongoClient, including
+
+- Deprecation and removal of MongoClient constructors that take as an argument more than a single credential
+- Deprecation and removal of methods that allow lazy authentication (i.e post-MongoClient construction)
+
+Drivers need to support both the shorter and longer SCRAM-SHA-1 and SCRAM-SHA-256 conversations over MongoDB's SASL
+implementation. Earlier versions of the server required an extra round trip due to an implementation decision. This was
+accomplished by sending no bytes back to the server, as seen in the following conversation (extra round trip
+emphasized):
+
+```javascript
+CMD = {saslStart: 1, mechanism: "SCRAM-SHA-1", payload: BinData(0, "biwsbj11c2VyLHI9ZnlrbytkMmxiYkZnT05Sdjlxa3hkYXdM"), options: {skipEmptyExchange: true}}
+RESP = {conversationId : 1, payload: BinData(0,"cj1meWtvK2QybGJiRmdPTlJ2OXFreGRhd0xIbytWZ2s3cXZVT0tVd3VXTElXZzRsLzlTcmFHTUhFRSxzPXJROVpZM01udEJldVAzRTFURFZDNHc9PSxpPTEwMDAw"), done: false, ok: 1}
+CMD = {saslContinue: 1, conversationId: 1, payload: BinData(0, "Yz1iaXdzLHI9ZnlrbytkMmxiYkZnT05Sdjlxa3hkYXdMSG8rVmdrN3F2VU9LVXd1V0xJV2c0bC85U3JhR01IRUUscD1NQzJUOEJ2Ym1XUmNrRHc4b1dsNUlWZ2h3Q1k9")}
+RESP = {conversationId: 1, payload: BinData(0,"dj1VTVdlSTI1SkQxeU5ZWlJNcFo0Vkh2aFo5ZTA9"), done: false, ok: 1}
+# Extra round trip
+CMD = {saslContinue: 1, conversationId: 1, payload: BinData(0, "")}
+RESP = {conversationId: 1, payload: BinData(0,""), done: true, ok: 1}
+```
+
+The extra round trip will be removed in server version 4.4 when `options: { skipEmptyExchange: true }` is specified
+during `saslStart`.
+
+## Reference Implementation
+
+The Java and .NET drivers currently uses eager authentication and abide by this specification.
+
+
+
+## Q & A
+
+Q: According to [Authentication Handshake](#authentication-handshake), we are calling `hello` or legacy hello for every
+socket. Isn't this a lot?
+
+Drivers should be pooling connections and, as such, new sockets getting opened should be relatively infrequent. It's
+simply part of the protocol for setting up a socket to be used.
+
+Q: Where is information related to user management?
+
+Not here currently. Should it be? This is about authentication, not user management. Perhaps a new spec is necessary.
+
+Q: It's possible to continue using authenticated sockets even if new sockets fail authentication. Why can't we do that
+so that applications continue to work.
+
+Yes, that's technically true. The issue with doing that is for drivers using connection pooling. An application would
+function normally until an operation needed an additional connection(s) during a spike. Each new connection would fail
+to authenticate causing intermittent failures that would be very difficult to understand for a user.
+
+Q: Should a driver support multiple credentials?
+
+No.
+
+Historically, the MongoDB server and drivers have supported multiple credentials, one per authSource, on a single
+connection. It was necessary because early versions of MongoDB allowed a user to be granted privileges to access the
+database in which the user was defined (or all databases in the special case of the "admin" database). But with the
+introduction of role-based access control in MongoDB 2.6, that restriction was removed and it became possible to create
+applications that access multiple databases with a single authenticated user.
+
+Role-based access control also introduces the potential for accidental privilege escalation. An application may, for
+example, authenticate user A from authSource X, and user B from authSource Y, thinking that user A has privileges only
+on collections in X and user B has privileges only on collections in Y. But with role-based access control that
+restriction no longer exists, and it's possible that user B has, for example, more privileges on collections in X than
+user A does. Due to this risk it's generally safer to create a single user with only the privileges required for a given
+application, and authenticate only that one user in the application.
+
+In addition, since only a single credential is supported per authSource, certain mechanisms are restricted to a single
+credential and some credentials cannot be used in conjunction (GSSAPI and X509 both use the "$external" database).
+
+Finally, MongoDB 3.6 introduces sessions, and allows at most a single authenticated user on any connection which makes
+use of one. Therefore any application that requires multiple authenticated users will not be able to make use of any
+feature that builds on sessions (e.g. retryable writes).
+
+Drivers should therefore guide application creators in the right direction by supporting the association of at most one
+credential with a MongoClient instance.
+
+Q: Should a driver support lazy authentication?
+
+No, for the same reasons as given in the previous section, as lazy authentication is another mechanism for allowing
+multiple credentials to be associated with a single MongoClient instance.
+
+Q: Why does SCRAM sometimes SASLprep and sometimes not?
+
+When MongoDB implemented SCRAM-SHA-1, it required drivers to *NOT* SASLprep usernames and passwords. The primary reason
+for this was to allow a smooth upgrade path from MongoDB-CR using existing usernames and passwords. Also, because
+MongoDB's SCRAM-SHA-1 passwords are hex characters of a digest, SASLprep of passwords was irrelevant.
+
+With the introduction of SCRAM-SHA-256, MongoDB requires users to explicitly create new SCRAM-SHA-256 credentials
+distinct from those used for MONGODB-CR and SCRAM-SHA-1. This means SCRAM-SHA-256 passwords are not digested and any
+Unicode character could now appear in a password. Therefore, the SCRAM-SHA-256 mechanism requires passwords to be
+normalized with SASLprep, in accordance with the SCRAM RFC.
+
+However, usernames must be unique, which creates a similar upgrade path problem. SASLprep maps multiple byte
+representations to a single normalized one. An existing database could have multiple existing users that map to the same
+SASLprep form, which makes it impossible to find the correct user document for SCRAM authentication given only a
+SASLprep username. After considering various options to address or workaround this problem, MongoDB decided that the
+best user experience on upgrade and lowest technical risk of implementation is to require drivers to continue to not
+SASLprep usernames in SCRAM-SHA-256.
+
+Q: Should drivers support accessing Amazon EC2 instance metadata in Amazon ECS?
+
+No. While it's possible to allow access to EC2 instance metadata in ECS, for security reasons, Amazon states it's best
+practice to avoid this. (See
+[accessing EC2 metadata in ECS](https://aws.amazon.com/premiumsupport/knowledge-center/ecs-container-ec2-metadata/) and
+[IAM Roles for Tasks](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html))
+
+## Changelog
+
+- 2025-11-25: Remove redundant `*.mongodbgov.net` on `ALLOWED_HOSTS`
+
+- 2025-11-19: Extend `ALLOWED_HOSTS` with `*.mongo.com` and `*.mongodbgov.net`
+
+- 2025-09-30: Remove support for explicitly specifying MONGODDB-AWS authentication properties.
+
+- 2025-09-10: Update precedence of MONGODB-AWS credential fetching behaviour.
+
+- 2025-01-29: Add support for custom AWS credential providers.
+
+- 2024-10-02: Add Kubernetes built-in OIDC provider integration.
+
+- 2024-08-19: Clarify Reauthentication and Speculative Authentication combination behavior.
+
+- 2024-05-29: Disallow comma character when `TOKEN_RESOURCE` is given in a connection string.
+
+- 2024-05-03: Clarify timeout behavior for OIDC machine callback. Add `serverless:forbid` to OIDC unified tests. Add an
+ additional prose test for the behavior of `ALLOWED_HOSTS`.
+
+- 2024-04-24: Clarify that TOKEN_RESOURCE for MONGODB-OIDC must be url-encoded.
+
+- 2024-04-22: Fix API description for GCP built-in OIDC provider.
+
+- 2024-04-22: Updated OIDC authentication flow and prose tests.
+
+- 2024-04-22: Clarify that driver should not validate `saslSupportedMechs` content.
+
+- 2024-04-03: Added GCP built-in OIDC provider integration.
+
+- 2024-03-29: Updated OIDC test setup and descriptions.
+
+- 2024-03-21: Added Azure built-in OIDC provider integration.
+
+- 2024-03-09: Rename OIDC integration name and values.
+
+- 2024-01-31: Migrated from reStructuredText to Markdown.
+
+- 2024-01-17: Added MONGODB-OIDC machine auth flow spec and combine with human auth flow specs.
+
+- 2023-04-28: Added MONGODB-OIDC auth mechanism
+
+- 2022-11-02: Require environment variables to be read dynamically.
+
+- 2022-10-28: Recommend the use of AWS SDKs where available.
+
+- 2022-10-07: Require caching of AWS credentials fetched by the driver.
+
+- 2022-10-05: Remove spec front matter and convert version history to changelog.
+
+- 2022-09-07: Add support for AWS AssumeRoleWithWebIdentity.
+
+- 2022-01-20: Require that timeouts be applied per the client-side operations timeout spec.
+
+- 2022-01-14: Clarify that `OP_MSG` must be used for authentication when it is supported.
+
+- 2021-04-23: Updated to use hello and legacy hello.
+
+- 2021-03-04: Note that errors encountered during auth are handled by SDAM.
+
+- 2020-03-06: Add reference to the speculative authentication section of the handshake spec.
+
+- 2020-02-15: Rename MONGODB-IAM to MONGODB-AWS
+
+- 2020-02-04: Support shorter SCRAM conversation starting in version 4.4 of the server.
+
+- 2020-01-31: Clarify that drivers must raise an error when a connection string has an empty value for authSource.
+
+- 2020-01-23: Clarify when authentication will occur.
+
+- 2020-01-22: Clarify that authSource in URI is not treated as a user configuring auth credentials.
+
+- 2019-12-05: Added MONGODB-IAM auth mechanism
+
+- 2019-07-13: Clarify database to use for auth mechanism negotiation.
+
+- 2019-04-26: Test format changed to improve specificity of behavior assertions.
+
+ - Clarify that database name in URI is not treated as a user configuring auth credentials.
+
+- 2018-08-08: Unknown users don't cause handshake errors. This was changed before server 4.0 GA in SERVER-34421, so the
+ auth spec no longer refers to such a possibility.
+
+- 2018-04-17: Clarify authSource defaults
+
+ - Fix PLAIN authSource rule to allow user provided values
+ - Change SCRAM-SHA-256 rules such that usernames are *NOT* normalized; this follows a change in the server design and
+ should be available in server 4.0-rc0.
+
+- 2018-03-29: Clarify auth handshake and that it only applies to non-monitoring sockets.
+
+- 2018-03-15: Describe CANONICALIZE_HOST_NAME algorithm.
+
+- 2018-03-02: Added SCRAM-SHA-256 and mechanism negotiation as provided by server 4.0
+
+ - Updated default mechanism determination
+ - Clarified SCRAM-SHA-1 rules around SASLprep
+ - Require SCRAM-SHA-1 and SCRAM-SHA-256 to enforce a minimum iteration count
+
+- 2017-11-10: Updated minimum server version to 2.6
+
+ - Updated the Q & A to recommend support for at most a single credential per MongoClient
+ - Removed lazy authentication section
+ - Changed the list of server types requiring authentication
+ - Made providing username for X509 authentication optional
+
+- 2015-02-04: Added SCRAM-SHA-1 sasl mechanism
+
+ - Added connection handshake
+ - Changed connection string to support mechanism properties in generic form
+ - Added example conversations for all mechanisms except GSSAPI
+ - Miscellaneous wording changes for clarification
+ - Added MONGODB-X509
+ - Added PLAIN sasl mechanism
+ - Added support for GSSAPI mechanism property gssapiServiceName
+
+______________________________________________________________________
diff --git a/source/auth/auth.rst b/source/auth/auth.rst
index e605dabdee..cffd20f3b1 100644
--- a/source/auth/auth.rst
+++ b/source/auth/auth.rst
@@ -1,925 +1,6 @@
.. role:: javascript(code)
:language: javascript
-=====================
-Driver Authentication
-=====================
-
-:Spec: 100
-:Spec Version: 1.7.1
-:Title: Driver Authentication
-:Author: Craig Wilson, David Golden
-:Advisors: Andy Schwerin, Bernie Hacket, Jeff Yemin, David Golden
-:Status: Accepted
-:Type: Standards
-:Minimum Server Version: 2.6
-:Last Modified: 2018-04-17
-
-.. contents::
-
---------
-
-Abstract
-========
-
-MongoDB supports various authentication strategies across various versions. When authentication is turned on in the database, a driver must authenticate before it is allowed to communicate with the server. This spec defines when and how a driver performs authentication with a MongoDB server.
-
-----
-META
-----
-
-The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in `RFC 2119 `_.
-
-----------
-References
-----------
-
-`Server Discovery and Monitoring `_
-
-Specification
-=============
-
------------
-Definitions
------------
-
-Credential
- The pieces of information used to establish the authenticity of a user. This is composed of an identity and some form of evidence such as a password or a certificate.
-
-FQDN
- Fully Qualified Domain Name
-
-Mechanism
- A SASL implementation of a particular type of credential negotiation.
-
-Source
- The authority used to establish credentials and/or privileges in reference to a mongodb server. In practice, it is the database to which sasl authentication commands are sent.
-
-Realm
- The authority used to establish credentials and/or privileges in reference to GSSAPI.
-
-SASL
- Simple Authentication and Security Layer - `RFC 4422 `_
-
-
----------------------
-Client Implementation
----------------------
-
-
-MongoCredential
----------------
-
-Drivers SHOULD contain a type called `MongoCredential`. It SHOULD contain some or all of the following information.
-
-username (string)
- * Applies to all mechanisms.
- * Optional for MONGODB-X509.
-source (string)
- * Applies to all mechanisms.
- * Always '$external' for GSSAPI and MONGODB-X509.
- * This is the database to which the authenticate command will be sent.
- * This is the database to which sasl authentication commands will be sent.
-password (string)
- * Does not apply to all mechanisms.
-mechanism (string)
- * Indicates which mechanism to use with the credential.
-mechanism_properties
- * Includes additional properties for the given mechanism.
-
-
-Errors
-~~~~~~
-
-Drivers SHOULD raise an error as early as possible when detecting invalid values in a credential. For instance, if a ``mechanism_property`` is specified for `MONGODB-CR`_, the driver should raise an error indicating that the property does not apply.
-
-
-Naming
-~~~~~~
-
-Naming of this information MUST be idiomatic to the driver's language/framework but still remain consistent. For instance, python would use "mechanism_properties" and .NET would use "MechanismProperties".
-
-Naming of mechanism properties MUST be case-insensitive. For instance, SERVICE_NAME and service_name refer to the same property.
-
-
-Authentication
---------------
-
-A MongoClient instance MUST be considered a single logical connection to
-the server/deployment.
-
-Socket connections from a MongoClient to deployment members can be one
-of two types:
-
-* Monitoring-only socket: multi-threaded drivers maintain monitoring
- sockets separate from sockets in connection pools.
-
-* General-use socket: for multi-threaded drivers, these are sockets in
- connection pools used for (non-monitoring) user operations; in
- single-threaded drivers, these are used for both monitoring and user
- operations.
-
-Authentication (including mechanism negotiation) MUST NOT happen on
-monitoring-only sockets.
-
-If one or more credentials are provided to a MongoClient, then whenever
-a general-use socket is opened, drivers MUST immediately conduct an
-authentication handshake over that socket.
-
-Drivers SHOULD require all credentials to be specified upon construction
-of the MongoClient. This is defined as eager authentication and drivers
-MUST support this mode.
-
-Authentication Handshake
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-An authentication handshake consists of an initial ``isMaster`` command
-possibly followed by one or more authentication conversations.
-
-Drivers MUST follow the following steps for an authentication
-handshake:
-
-#. Upon opening a general-use socket to a server for a given
- MongoClient, drivers MUST issue a `MongoDB Handshake
- <../mongodb-handshake/handshake.rst>`_ immediately. This allows a
- driver to determine the server type. If the ``isMaster`` of the
- MongoDB Handshake fails with an error, drivers MUST treat this an
- an authentication error.
-
-# If the server is not of type Standalone, RSPrimary, RSSecondary or
- Mongos, no authentication is possible and the handshake is complete.
-
-#. If credentials exist:
-
- #. A driver MUST authenticate with all credentials provided to the
- MongoClient.
-
- #. A single invalid credential is the same as all credentials being
- invalid.
-
-If the authentication handshake fails for a socket, drivers MUST close all
-other general-use sockets connected to the same server.
-
-Mechanism Negotiation via Handshake
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-:since: 4.0
-
-If an application provides a username but does not provide an
-authentication mechanism, drivers MUST negotiate a mechanism via an
-``isMaster`` command requesting a user's supported SASL mechanisms::
-
- {isMaster: 1, saslSupportedMechs: "."}
-
-In this example ```` is the authentication database name (default
-'admin') and ```` is the username provided in the auth credential.
-The username MUST NOT be modified from the form provided by the user (i.e. do
-not normalize with SASLprep), as the server uses the raw form to look for
-conflicts with legacy credentials.
-
-If the ``isMaster`` response includes a
-``saslSupportedMechs`` field, then drivers MUST use the contents of that field
-to select a default mechanism as described later. If the command succeeds and
-the response does not include a ``saslSupportedMechs`` field, then drivers MUST
-use the legacy default mechanism rules for servers older than 4.0.
-
-Single-credential drivers
-`````````````````````````
-
-When the authentication mechanism is not specied, drivers that allow
-only a single credential per client MUST perform mechanism negotiation
-as part of the MongoDB Handshake portion of the authentication
-handshake. This lets authentication proceed without a separate
-negotiation round-trip exchange with the server.
-
-Multi-credential drivers
-````````````````````````
-
-The use of multiple credentials within a driver is discouraged, but some
-legacy drivers still allow this. Such drivers may not have user credentials
-when connections are opened and thus will not be able to do negotiation.
-
-Drivers with a list of credentials at the time a connection is opened MAY do
-mechanism negotiation on the initial handshake, but only for the first
-credential in the list of credentials.
-
-When authenticating each credential, if the authentication mechanism is not
-specified and has not been negotiated for that credential:
-
-- If the connection handshake results indicate the server version is 4.0 or
- later, drivers MUST send a new ``isMaster`` negotiation command for the
- credential to determine the default authentication mechanism.
-
-- Otherwise, when the server version is earlier than 4.0, the driver MUST
- select a default authentication mechanism for the credential following the
- instructions for when the ``saslSupportedMechs`` field is not present in
- an ``isMaster`` response.
-
-Caching credentials in SCRAM
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-In the implementation of SCRAM authentication mechanisms (e.g. SCRAM-SHA-1
-and SCRAM-SHA-256), drivers MUST maintain a cache of computed SCRAM credentials.
-The cache entries SHOULD be identified by the password, salt, iteration count,
-and a value that uniquely identifies the authentication mechanism (e.g. "SHA1"
-or "SCRAM-SHA-256").
-
-The cache entry value MUST be either the ``saltedPassword`` parameter or the
-combination of the ``clientKey`` and ``serverKey`` parameters.
-
---------------------------------
-Supported Authentication Methods
---------------------------------
-
-Defaults
---------
-
-:since: 3.0
-:revised: 4.0
-
-If the user did not provide a mechanism via the connection string or via code,
-the following logic describes how to select a default.
-
-If a ``saslSupportedMechs`` field was present in the ``isMaster`` results for
-mechanism negotiation, then it MUST be inspected to select a default
-mechanism::
-
- {
- "ismaster" : true,
- "saslSupportedMechs": ["SCRAM-SHA-1", "SCRAM-SHA-256"],
- ...
- "ok" : 1
- }
-
-If SCRAM-SHA-256 is present in the list of mechanism, then it MUST be
-used as the default; otherwise, SCRAM-SHA-1 MUST be used as the default,
-regardless of whether SCRAM-SHA-1 is in the list. Drivers MUST NOT
-attempt to use any other mechanism (e.g. PLAIN) as the default.
-
-If ``saslSupportedMechs`` is not present in the ``isMaster`` results for
-mechanism negotiation, then SCRAM-SHA-1 MUST be used when talking to servers >=
-3.0. Prior to server 3.0, MONGODB-CR MUST be used.
-
-When a user has specified a mechanism, regardless of the server version, the
-driver MUST honor this and attempt to authenticate.
-
-Determining Server Version
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Drivers SHOULD use the server's wire version ranges to determine the server's
-version.
-
-MongoDB Custom Mechanisms
--------------------------
-
-MONGODB-CR
-~~~~~~~~~~
-
-:since: 1.4
-:deprecated: 3.0
-:removed: 4.0
-
-MongoDB Challenge Response is a nonce and MD5 based system. The driver sends a `getNonce` command, encodes and hashes the password using the returned nonce, and then sends an `authenticate` command.
-
-Conversation
-````````````
-
-#. Send ``getNonce`` command
- * :javascript:`{ getNonce: 1 }`
- * Response: :javascript:`{ nonce: }`
-#. Compute key
- * :javascript:`passwordDigest = HEX( MD5( UTF8( username + ':mongo:' + password )))`
- * :javascript:`key = HEX( MD5( UTF8( nonce + username + passwordDigest )))`
-#. Send ``authenticate`` command
- * :javascript:`{ authenticate: 1, nonce: nonce, user: username, key: key }`
-
-As an example, given a username of "user" and a password of "pencil", the conversation would appear as follows:
-
-| C: :javascript:`{getnonce : 1}`
-| S: :javascript:`{nonce: "2375531c32080ae8", ok: 1}`
-| C: :javascript:`{authenticate: 1, user: "user", nonce: "2375531c32080ae8", key: "21742f26431831d5cfca035a08c5bdf6"}`
-| S: :javascript:`{ok: 1}`
-
-`MongoCredential`_ Properties
-`````````````````````````````
-
-username
- MUST be specified.
-
-source
- MUST be specified. Defaults to the database name if supplied on the connection string or ``admin``.
-
-password
- MUST be specified.
-
-mechanism
- MUST be "MONGODB-CR"
-
-mechanism_properties
- MUST NOT be specified.
-
-
-MONGODB-X509
-~~~~~~~~~~~~
-
-:since: 2.6
-:changed: 3.4
-
-
-MONGODB-X509 is the usage of X.509 certificates to validate a client where the
-distinguished subject name of the client certificate acts as the username.
-
-When connected to MongoDB 3.4:
- * You MUST NOT raise an error when the application only provides an X.509 certificate and no username.
- * If the application does not provide a username you MUST NOT send a username to the server.
- * If the application provides a username you MUST send that username to the server.
-When connected to MongoDB 3.2 or earlier:
- * You MUST send a username to the server.
- * If no username is provided by the application, you MAY extract the username from the X.509 certificate instead of requiring the application to provide it.
- * If you choose not to automatically extract the username from the certificate you MUST error when no username is provided by the application.
-
-
-Conversation
-````````````
-
-#. Send ``authenticate`` command (MongoDB 3.4+)
- * C: :javascript:`{"authenticate": 1, "mechanism": "MONGODB-X509"}`
- * S: :javascript:`{"dbname" : "$external", "user" : "C=IS,ST=Reykjavik,L=Reykjavik,O=MongoDB,OU=Drivers,CN=client", "ok" : 1}`
-
-#. Send ``authenticate`` command with username:
- * ``username = openssl x509 -subject -nameopt RFC2253 -noout -inform PEM -in my-cert.pem``
- * C: :javascript:`{authenticate: 1, mechanism: "MONGODB-X509", user: "C=IS,ST=Reykjavik,L=Reykjavik,O=MongoDB,OU=Drivers,CN=client"}`
- * S: :javascript:`{"dbname" : "$external", "user" : "C=IS,ST=Reykjavik,L=Reykjavik,O=MongoDB,OU=Drivers,CN=client", "ok" : 1}`
-
-
-`MongoCredential`_ Properties
-`````````````````````````````
-
-username
- SHOULD NOT be provided for MongoDB 3.4+
- MUST be specified for MongoDB prior to 3.4
-
-source
- MUST be "$external". Defaults to ``$external``.
-
-password
- MUST NOT be specified.
-
-mechanism
- MUST be "MONGODB-X509"
-
-mechanism_properties
- MUST NOT be specified.
-
-
-TODO: Errors
-
-
-SASL Mechanisms
----------------
-
-:since: 2.4 enterprise
-
-SASL mechanisms are all implemented using the same sasl commands and interpreted as defined by the `SASL specification RFC 4422 `_.
-
-#. Send the `saslStart` command.
- * :javascript:`{ saslStart: 1, mechanism: , payload: BinData(...), autoAuthorize: 1 }`
- * Response: :javascript:`{ conversationId: , code: , done: , payload: }`
- - conversationId: the conversation identifier. This will need to be remembered and used for the duration of the conversation.
- - code: A response code that will indicate failure. This field is not included when the command was successful.
- - done: a boolean value indicating whether or not the conversation has completed.
- - payload: a sequence of bytes or a base64 encoded string (depending on input) to pass into the SASL library to transition the state machine.
-#. Continue with the `saslContinue` command while `done` is `false`.
- * :javascript:`{ saslContinue: 1, conversationId: conversationId, payload: BinData(...) }`
- * Response is the same as that of `saslStart`
-
-
-Many languages will have the ability to utilize 3rd party libraries. The server uses `cyrus-sasl `_ and it would make sense for drivers with a choice to also choose cyrus. However, it is important to ensure that when utilizing a 3rd party library it does implement the mechanism on all supported OS versions and that it interoperates with the server. For instance, the cyrus sasl library offered on RHEL 6 does not implement SCRAM-SHA-1. As such, if your driver supports RHEL 6, you'll need to implement SCRAM-SHA-1 from scratch.
-
-
-GSSAPI
-~~~~~~
-
-:since:
- 2.4 enterprise
-
- 2.6 enterprise on windows
-
-GSSAPI is kerberos authentication as defined in `RFC 4752 `_. Microsoft has a proprietary implementation called SSPI which is compatible with both windows and linux clients.
-
-`MongoCredential`_ properties:
-
-username
- MUST be specified.
-
-source
- MUST be "$external". Defaults to ``$external``.
-
-password
- MAY be specified.
-
-mechanism
- MUST be "GSSAPI"
-
-mechanism_properties
- SERVICE_NAME
- Drivers MUST allow the user to specify a different service name. The default is "mongodb".
-
- CANONICALIZE_HOST_NAME
- Drivers MAY allow the user to request canonicalization of the hostname. This might be required when the hosts report different hostnames than what is used in the kerberos database. The default is "false".
-
- SERVICE_REALM
- Drivers MAY allow the user to specify a different realm for the service. This might be necessary to support cross-realm authentication where the user exists in one realm and the service in another.
-
-Hostname Canonicalization
-`````````````````````````
-
-If CANONICALIZE_HOST_NAME is true, the client MUST canonicalize the name of each host it uses for authentication. There are two options. First, if the client's underlying GSSAPI library provides hostname canonicalization, the client MAY rely on it. For example, MIT Kerberos has `a configuration option for canonicalization `_.
-
-Second, the client MAY implement its own canonicalization. If so, the canonicalization algorithm MUST be::
-
- addresses = fetch addresses for host
- if no addresses:
- throw error
-
- address = first result in addresses
-
- while true:
- cnames = fetch CNAME records for host
- if no cnames:
- break
-
- # Unspecified which CNAME is used if > 1.
- host = one of the records in cnames
-
- reversed = do a reverse DNS lookup for address
- if reversed:
- canonicalized = lowercase(reversed)
- else:
- canonicalized = lowercase(host)
-
-For example, here is a Python implementation of this algorithm using ``getaddrinfo`` (for address and CNAME resolution) and ``getnameinfo`` (for reverse DNS).
-
-.. code-block:: python
-
- from socket import *
- import sys
-
-
- def canonicalize(host):
- # Get a CNAME for host, if any.
- af, socktype, proto, canonname, sockaddr = getaddrinfo(
- host, None, 0, 0, IPPROTO_TCP, AI_CANONNAME)[0]
-
- print('address from getaddrinfo: [%s]' % (sockaddr[0],))
- print('canonical name from getaddrinfo: [%s]' % (canonname,))
-
- try:
- # NI_NAMEREQD requests an error if getnameinfo fails.
- name = getnameinfo(sockaddr, NI_NAMEREQD)
- except gaierror as exc:
- print('getname info failed: "%s"' % (exc,))
- return canonname.lower()
-
- return name[0].lower()
-
-
- canonicalized = canonicalize(sys.argv[1])
- print('canonicalized: [%s]' % (canonicalized,))
-
-Beware of a bug in older glibc where ``getaddrinfo`` uses PTR records instead of CNAMEs if the address family hint is AF_INET6, and beware of a bug in older MIT Kerberos that causes it to always do reverse DNS lookup even if the ``rdns`` configuration option is set to ``false``.
-
-PLAIN
-~~~~~
-
-:since: 2.6 enterprise
-
-The PLAIN mechanism, as defined in `RFC 4616 `_, is used in MongoDB to perform LDAP authentication. It cannot be used to perform any other type of authentication. Since the credentials are stored outside of MongoDB, the `$external` database must be used for authentication.
-
-Conversation
-````````````
-
-As an example, given a username of "user" and a password of "pencil", the conversation would appear as follows:
-
-| C: :javascript:`{saslStart: 1, mechanism: "PLAIN", payload: BinData(0, "AHVzZXIAcGVuY2ls")}`
-| S: :javascript:`{conversationId: 1, payload: BinData(0,""), done: true, ok: 1}`
-
-If your sasl client is also sending the authzid, it would be "user" and the conversation would appear as follows:
-
-| C: :javascript:`{saslStart: 1, mechanism: "PLAIN", payload: BinData(0, "dXNlcgB1c2VyAHBlbmNpbA==")}`
-| S: :javascript:`{conversationId: 1, payload: BinData(0,""), done: true, ok: 1}`
-
-MongoDB supports either of these forms.
-
-`MongoCredential`_ Properties
-`````````````````````````````
-
-username
- MUST be specified.
-
-source
- MUST be specified. Defaults to the database name if supplied on the connection string or ``$external``.
-
-password
- MUST be specified.
-
-mechanism
- MUST be "PLAIN"
-
-mechanism_properties
- MUST NOT be specified.
-
-
-SCRAM-SHA-1
-~~~~~~~~~~~
-
-:since: 3.0
-
-SCRAM-SHA-1 is defined in `RFC 5802 `_.
-
-`Page 11 of the RFC `_ specifies
-that user names be prepared with SASLprep, but drivers MUST NOT do so.
-
-`Page 8 of the RFC `_ identifies the
-"SaltedPassword" as ``:= Hi(Normalize(password), salt, i)``. The ``password``
-variable MUST be the mongodb hashed variant. The mongo hashed variant is
-computed as :javascript:`hash = HEX( MD5( UTF8( username + ':mongo:' +
-plain_text_password )))`, where ``plain_text_password`` is actually plain text.
-The ``username`` and ``password`` MUST NOT be prepared with SASLprep before
-hashing.
-
-For example, to compute the ClientKey according to the RFC:
-
-.. code:: javascript
-
- // note that "salt" and "i" have been provided by the server
- function computeClientKey(username, plain_text_password) {
- mongo_hashed_password = HEX( MD5( UTF8( username + ':mongo:' + plain_text_password )));
- saltedPassword = Hi(Normalize(mongo_hashed_password), salt, i);
- clientKey = HMAC(saltedPassword, "Client Key");
- }
-
-In addition, SCRAM-SHA-1 requires that a client create a randomly generated
-nonce. It is imperative, for security sake, that this be as secure and truly
-random as possible. For instance, Java provides both a Random class as well as
-a SecureRandom class. SecureRandom is cryptographically generated while Random
-is just a pseudo-random generator with predictable outcomes.
-
-Additionally, drivers MUST enforce a minimum iteration count of 4096 and MUST
-error if the authentication conversation specifies a lower count. This
-mitigates downgrade attacks by a man-in-the-middle attacker.
-
-Drivers MUST NOT advertise support for channel binding, as the server does
-not support it and legacy servers may fail authentication if drivers advertise
-support. I.e. the client-first-message MUST start with ``n,``.
-
-Conversation
-````````````
-
-As an example, given a username of "user" and a password of "pencil" and an r
-value of "fyko+d2lbbFgONRv9qkxdawL", a SCRAM-SHA-1 conversation would appear as
-follows:
-
-| C: ``n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL``
-| S: ``r=fyko+d2lbbFgONRv9qkxdawLHo+Vgk7qvUOKUwuWLIWg4l/9SraGMHEE,s=rQ9ZY3MntBeuP3E1TDVC4w==,i=10000``
-| C: ``c=biws,r=fyko+d2lbbFgONRv9qkxdawLHo+Vgk7qvUOKUwuWLIWg4l/9SraGMHEE,p=MC2T8BvbmWRckDw8oWl5IVghwCY=``
-| S: ``v=UMWeI25JD1yNYZRMpZ4VHvhZ9e0=``
-
-This same conversation over mongodb's sasl implementation would appear as follows:
-
-| C: :javascript:`{saslStart: 1, mechanism: "SCRAM-SHA-1", payload: BinData(0, "biwsbj11c2VyLHI9ZnlrbytkMmxiYkZnT05Sdjlxa3hkYXdM")}`
-| S: :javascript:`{conversationId : 1, payload: BinData(0,"cj1meWtvK2QybGJiRmdPTlJ2OXFreGRhd0xIbytWZ2s3cXZVT0tVd3VXTElXZzRsLzlTcmFHTUhFRSxzPXJROVpZM01udEJldVAzRTFURFZDNHc9PSxpPTEwMDAw"), done: false, ok: 1}`
-| C: :javascript:`{saslContinue: 1, conversationId: 1, payload: BinData(0, "Yz1iaXdzLHI9ZnlrbytkMmxiYkZnT05Sdjlxa3hkYXdMSG8rVmdrN3F2VU9LVXd1V0xJV2c0bC85U3JhR01IRUUscD1NQzJUOEJ2Ym1XUmNrRHc4b1dsNUlWZ2h3Q1k9")}`
-| S: :javascript:`{conversationId: 1, payload: BinData(0,"dj1VTVdlSTI1SkQxeU5ZWlJNcFo0Vkh2aFo5ZTA9"), done: false, ok: 1}`
-| C: :javascript:`{saslContinue: 1, conversationId: 1, payload: BinData(0, "")}`
-| S: :javascript:`{conversationId: 1, payload: BinData(0,""), done: true, ok: 1}`
-
.. note::
-
- There is an extra round trip due to an implementation decision on the server. This is accomplished by sending no bytes back to the server for what is effectively a no-op.
-
-`MongoCredential`_ Properties
-`````````````````````````````
-
-username
- MUST be specified.
-
-source
- MUST be specified. Defaults to the database name if supplied on the connection string or ``admin``.
-
-password
- MUST be specified.
-
-mechanism
- MUST be "SCRAM-SHA-1"
-
-mechanism_properties
- MUST NOT be specified.
-
-SCRAM-SHA-256
-~~~~~~~~~~~~~
-
-:since: 4.0
-
-SCRAM-SHA-256 extends `RFC 5802 `_ and
-is formally defined in `RFC 7677 `_.
-
-The MongoDB SCRAM-SHA-256 mechanism works similarly to the SCRAM-SHA-1
-mechanism, with the following changes:
-
-- The SCRAM algorithm MUST use SHA-256 as the hash function instead of SHA-1.
-- User names MUST NOT be prepared with SASLprep. This intentionally
- contravenes the "SHOULD" provision of RFC 5802.
-- Passwords MUST be prepared with SASLprep, per RFC 5802. Passwords are
- used directly for key derivation ; they MUST NOT be digested as they are in
- SCRAM-SHA-1.
-
-Additionally, drivers MUST enforce a minimum iteration count of 4096 and MUST
-error if the authentication conversation specifies a lower count. This
-mitigates downgrade attacks by a man-in-the-middle attacker.
-
-Conversation
-````````````
-
-As an example, given a username of "user" and a password of "pencil" and an r
-value of "rOprNGfwEbeRWgbNEkqO", a SCRAM-SHA-256 conversation would appear as
-follows:
-
-| C: ``n,,n=user,r=rOprNGfwEbeRWgbNEkqO``
-| S: ``r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096``
-| C: ``c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=``
-| S: ``v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=``
-
-`MongoCredential`_ Properties
-`````````````````````````````
-
-username
- MUST be specified.
-
-source
- MUST be specified. Defaults to the database name if supplied on the connection string or ``admin``.
-
-password
- MUST be specified.
-
-mechanism
- MUST be "SCRAM-SHA-256"
-
-mechanism_properties
- MUST NOT be specified.
-
--------------------------
-Connection String Options
--------------------------
-
-``mongodb://[username[:password]@]host1[:port1][,[host2:[port2]],...[hostN:[portN]]][/database][?options]``
-
-
-Auth Related Options
---------------------
-
-authMechanism
- MONGODB-CR, MONGODB-X509, GSSAPI, PLAIN, SCRAM-SHA-1, SCRAM-SHA-256
-
- Sets the Mechanism property on the MongoCredential. When not set, the default will be one of SCRAM-SHA-256, SCRAM-SHA-1 or MONGODB-CR, following the auth spec default mechanism rules.
-
-authSource
- Sets the Source property on the MongoCredential.
-
- For GSSAPI and MONGODB-X509 authMechanisms the authSource defaults to ``$external``.
- For PLAIN the authSource defaults to the database name if supplied on the connection string or ``$external``.
- For MONGODB-CR, SCRAM-SHA-1 and SCRAM-SHA-256 authMechanisms, the authSource defaults to the database name if supplied on the connection string or ``admin``.
-
-authMechanismProperties=PROPERTY_NAME:PROPERTY_VALUE,PROPERTY_NAME2:PROPERTY_VALUE2
- A generic method to set mechanism properties in the connection string.
-
- For example, to set REALM and CANONICALIZE_HOST_NAME, the option would be ``authMechanismProperties=CANONICALIZE_HOST_NAME:true,SERVICE_REALM:AWESOME``.
-
-gssapiServiceName (deprecated)
- An alias for ``authMechanismProperties=SERVICE_NAME:mongodb``.
-
-
-Implementation
---------------
-
-#. Credentials MAY be specified in the connection string immediately after the scheme separator "//".
-#. A realm MAY be passed as a part of the username in the url. It would be something like dev@MONGODB.COM, where dev is the username and MONGODB.COM is the realm. Per the RFC, the @ symbol should be url encoded using %40.
- * When GSSAPI is specified, this should be interpretted as the realm.
- * When non-GSSAPI is specified, this should be interpetted as part of the username.
-#. It is permissible for only the username to appear in the connection string. This would be identified by having no colon follow the username before the '@' hostname separator.
-#. The source is determined by the following:
- * if authSource is specified, it is used.
- * otherwise, if database is specified, it is used.
- * otherwise, the admin database is used.
-
-
-Test Plan
-=========
-
-Connection string tests have been defined in the associated files:
-
-* `Connection String `_.
-
----------------------------------------
-SCRAM-SHA-256 and mechanism negotiation
----------------------------------------
-
-Testing SCRAM-SHA-256 requires server version 3.7.3 or later with
-``featureCompatibilityVersion`` of "4.0" or later.
-
-Drivers that allow specifying auth parameters in code as well as via
-connection string should test both for the test cases described below.
-
-Step 1
-------
-
-Create three test users, one with only SHA-1, one with only SHA-256 and one
-with both. For example::
-
- db.runCommand({createUser: 'sha1', pwd: 'sha1', roles: ['root'], mechanisms: ['SCRAM-SHA-1']})
- db.runCommand({createUser: 'sha256', pwd: 'sha256', roles: ['root'], mechanisms: ['SCRAM-SHA-256']})
- db.runCommand({createUser: 'both', pwd: 'both', roles: ['root'], mechanisms: ['SCRAM-SHA-1', 'SCRAM-SHA-256']})
-
-Step 2
-------
-
-For each test user, verify that you can connect and run a command requiring
-authentication for the following cases:
-
-- Explicitly specifying each mechanism the user supports.
-- Specifying no mechanism and relying on mechanism negotiation.
-
-For the example users above, the ``dbstats`` command could be used as a test
-command.
-
-For a test user supporting both SCRAM-SHA-1 and SCRAM-SHA-256, drivers should
-verify that negotation selects SCRAM-SHA-256. This may require monkey
-patching, manual log analysis, etc.
-
-Step 3
-------
-
-For test users that support only one mechanism, verify that explictly specifying
-the other mechanism fails.
-
-For a non-existent username, verify that not specifying a mechanism when
-connecting fails with the same error type that would occur with a correct
-username but incorrect password or mechanism. (Because negotiation with a
-non-existent user name at one point during server development caused an
-isMaster error, we want to verify this is seen by users as similar to other
-authentication errors, not as a network or database command error on the 'ismaster'
-command itself.)
-
-Step 4
-------
-
-To test SASLprep behavior, create two users:
-
-#. username: "IX", password "IX"
-#. username: "\\u2168" (ROMAN NUMERAL NINE), password "\\u2163" (ROMAN NUMERAL FOUR)
-
-To create the users, use the exact bytes for username and password without
-SASLprep or other normalization and specify SCRAM-SHA-256 credentials:
-
- db.runCommand({createUser: 'IX', pwd: 'IX', roles: ['root'], mechanisms: ['SCRAM-SHA-256']})
- db.runCommand({createUser: '\\u2168', pwd: '\\u2163', roles: ['root'], mechanisms: ['SCRAM-SHA-256']})
-
-For each user, verify that the driver can authenticate with the password in
-both SASLprep normalized and non-normalized forms:
-
-- User "IX": use password forms "IX" and "I\\u00ADX"
-- User "\\u2168": use password forms "IV" and "I\\u00ADV"
-
-As a URI, those have to be UTF-8 encoded and URL-escaped, e.g.:
-
-- mongodb://IX:IX@mongodb.example.com/admin
-- mongodb://IX:I%C2%ADX@mongodb.example.com/admin
-- mongodb://%E2%85%A8:IV@mongodb.example.com/admin
-- mongodb://%E2%85%A8:I%C2%ADV@mongodb.example.com/admin
-
------------------------
-Minimum iteration count
------------------------
-
-For SCRAM-SHA-1 and SCRAM-SHA-256, test that the minimum iteration count
-is respected. This may be done via unit testing of an underlying SCRAM
-library.
-
-Backwards Compatibility
-=======================
-
-Drivers may need to remove support for association of more than one credential with a MongoClient, including
-
- * Deprecation and removal of MongoClient constructors that take as an argument more than a single credential
- * Deprecation and removal of methods that allow lazy authentication (i.e post-MongoClient construction)
-
-Reference Implementation
-========================
-
-The Java and .NET drivers currently uses eager authentication and abide by this specification.
-
-Q & A
-=====
-
-Q: According to `Authentication Handshake`_, we are calling isMaster for every socket. Isn't this a lot?
- Drivers should be pooling connections and, as such, new sockets getting opened should be relatively infrequent. It's simply part of the protocol for setting up a socket to be used.
-
-Q: Where is information related to user management?
- Not here currently. Should it be? This is about authentication, not user management. Perhaps a new spec is necessary.
-
-Q: It's possible to continue using authenticated sockets even if new sockets fail authentication. Why can't we do that so that applications continue to work.
- Yes, that's technically true. The issue with doing that is for drivers using connection pooling. An application would function normally until an operation needed an additional connection(s) during a spike. Each new connection would fail to authenticate causing intermittent failures that would be very difficult to understand for a user.
-
-Q: Should a driver support multiple credentials?
- No.
-
- Historically, the MongoDB server and drivers have supported multiple credentials, one per authSource, on a single connection. It was necessary because early versions of MongoDB allowed a user to be granted privileges
- to access the database in which the user was defined (or all databases in the special case of the "admin" database). But with the introduction of role-based access control in MongoDB 2.6, that restriction was
- removed and it became possible to create applications that access multiple databases with a single authenticated user.
-
- Role-based access control also introduces the potential for accidental privilege escalation. An application may, for example, authenticate user A from authSource X, and user B from authSource Y, thinking that
- user A has privileges only on collections in X and user B has privileges only on collections in Y. But with role-based access control that restriction no longer exists, and it's possible that user B has, for example,
- more privileges on collections in X than user A does. Due to this risk it's generally safer to create a single user with only the privileges required for a given application, and authenticate only that one user
- in the application.
-
- In addition, since only a single credential is supported per authSource, certain mechanisms are restricted to a single credential and some credentials cannot be used in conjunction (GSSAPI and X509 both use the "$external" database).
-
- Finally, MongoDB 3.6 introduces sessions, and allows at most a single authenticated user on any connection which makes use of one. Therefore any application that requires multiple authenticated users will not be able to make use of any feature that builds on sessions (e.g. retryable writes).
-
- Drivers should therefore guide application creators in the right direction by supporting the association of at most one credential with a MongoClient instance.
-
-Q: Should a driver support lazy authentication?
- No, for the same reasons as given in the previous section, as lazy authentication is another mechanism for allowing multiple credentials to be associated with a single MongoClient instance.
-
-Q: Why does SCRAM sometimes SASLprep and sometimes not?
- When MongoDB implemented SCRAM-SHA-1, it required drivers to *NOT* SASLprep
- usernames and passwords. The primary reason for this was to allow a smooth
- upgrade path from MongoDB-CR using existing usernames and passwords.
- Also, because MongoDB's SCRAM-SHA-1 passwords are hex characters of a digest,
- SASLprep of passwords was irrelevant.
-
- With the introduction of SCRAM-SHA-256, MongoDB requires users to
- explicitly create new SCRAM-SHA-256 credentials distinct from those used
- for MONGODB-CR and SCRAM-SHA-1. This means SCRAM-SHA-256 passwords are not
- digested and any Unicode character could now appear in a password.
- Therefore, the SCRAM-SHA-256 mechanism requires passwords to be normalized
- with SASLprep, in accordance with the SCRAM RFC.
-
- However, usernames must be unique, which creates a similar upgrade path
- problem. SASLprep maps multiple byte representations to a single
- normalized one. An existing database could have multiple existing users
- that map to the same SASLprep form, which makes it impossible to find the
- correct user document for SCRAM authentication given only a SASLprep
- username. After considering various options to address or workaround this
- problem, MongoDB decided that the best user experience on upgrade and
- lowest technical risk of implementation is to require drivers to continue
- to not SASLprep usernames in SCRAM-SHA-256.
-
-Version History
-===============
-
-Version 1.7.1 Changes
- * Unknown users don't cause ismaster errors. This was changed before
- server 4.0 GA in SERVER-34421, so the auth spec no longer refers to
- such a possibility.
-
-Version 1.7 Changes
- * Clarify authSource defaults
- * Fix PLAIN authSource rule to allow user provided values
-
-Version 1.6 Changes
- * Change SCRAM-SHA-256 rules such that usernames are *NOT* normalized;
- this follows a change in the server design and should be available in
- server 4.0-rc0.
-
-Version 1.5 Changes
- * Clarify auth handshake and that it only applies to non-monitoring
- sockets.
-
-Version 1.4.1 Changes
- * Describe CANONICALIZE_HOST_NAME algorithm.
-
-Version 1.4 Changes
- * Added SCRAM-SHA-256 and mechanism negotiation as provided by server 4.0
- * Updated default mechanism determination
- * Clarified SCRAM-SHA-1 rules around SASLprep
- * Require SCRAM-SHA-1 and SCRAM-SHA-256 to enforce a minimum iteration count
-
-Version 1.3 Changes
- * Updated minimum server version to 2.6
- * Updated the Q & A to recommend support for at most a single credential per MongoClient
- * Removed lazy authentication section
- * Changed the list of server types requiring authentication
- * Made providing username for X509 authentication optional
-
-Version 1.2 Changes
- * Added SCRAM-SHA-1 sasl mechanism
- * Added `Connection Handshake`_
- * Changed connection string to support mechanism properties in generic form
- * Added example conversations for all mechanisms except GSSAPI
- * Miscellaneous wording changes for clarification
-
-Version 1.1 Changes
- * Added MONGODB-X509
- * Added PLAIN sasl mechanism
- * Added support for GSSAPI mechanism property gssapiServiceName
+ This specification has been converted to Markdown and renamed to
+ `auth.md `_.
diff --git a/source/auth/includes/calculating_a_signature.png b/source/auth/includes/calculating_a_signature.png
new file mode 100644
index 0000000000..8bfbbd6516
Binary files /dev/null and b/source/auth/includes/calculating_a_signature.png differ
diff --git a/source/auth/tests/README.md b/source/auth/tests/README.md
new file mode 100644
index 0000000000..c4b3eec74f
--- /dev/null
+++ b/source/auth/tests/README.md
@@ -0,0 +1,47 @@
+# Auth Tests
+
+## Introduction
+
+This document describes the format of the driver spec tests included in the JSON and YAML files included in the `legacy`
+sub-directory. Tests in the `unified` directory are written using the
+[Unified Test Format](../../unified-test-format/unified-test-format.md).
+
+The YAML and JSON files in the `legacy` directory tree are platform-independent tests that drivers can use to prove
+their conformance to the Auth Spec at least with respect to connection string URI input.
+
+Drivers should do additional unit testing if there are alternate ways of configuring credentials on a client.
+
+Driver must also conduct the prose tests in the Auth Spec test plan section.
+
+## Format
+
+Each YAML file contains an object with a single `tests` key. This key is an array of test case objects, each of which
+have the following keys:
+
+- `description`: A string describing the test.
+- `uri`: A string containing the URI to be parsed.
+- `valid:` A boolean indicating if the URI should be considered valid.
+- `credential`: If null, the credential must not be considered configured for the the purpose of deciding if the driver
+ should authenticate to the topology. If non-null, it is an object containing one or more of the following properties
+ of a credential:
+ - `username`: A string containing the username. For auth mechanisms that do not utilize a password, this may be the
+ entire `userinfo` token from the connection string.
+ - `password`: A string containing the password.
+ - `source`: A string containing the authentication database.
+ - `mechanism`: A string containing the authentication mechanism. A null value for this key is used to indicate that a
+ mechanism wasn't specified and that mechanism negotiation is required. Test harnesses should modify the mechanism
+ test as needed to assert this condition.
+ - `mechanism_properties`: A document containing mechanism-specific properties. It specifies a subset of properties
+ that must match. If a key exists in the test data, it must exist with the corresponding value in the credential.
+ Other values may exist in the credential without failing the test.
+
+If any key is missing, no assertion about that key is necessary. Except as specified explicitly above, if a key is
+present, but the test value is null, the observed value for that key must be uninitialized (whatever that means for a
+given driver and data type).
+
+## Implementation notes
+
+Testing whether a URI is valid or not should simply be a matter of checking whether URI parsing (or MongoClient
+construction) raises an error or exception.
+
+If a credential is configured, its properties must be compared to the `credential` field.
diff --git a/source/auth/tests/connection-string.json b/source/auth/tests/connection-string.json
deleted file mode 100644
index 6ae9491d61..0000000000
--- a/source/auth/tests/connection-string.json
+++ /dev/null
@@ -1,452 +0,0 @@
-{
- "tests": [
- {
- "description": "should use the default source and mechanism",
- "uri": "mongodb://user:password@localhost",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user",
- "password": "password",
- "db": "admin"
- },
- "options": null
- },
- {
- "description": "should use the database when no authSource is specified",
- "uri": "mongodb://user:password@localhost/foo",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user",
- "password": "password",
- "db": "foo"
- },
- "options": null
- },
- {
- "description": "should use the authSource when specified",
- "uri": "mongodb://user:password@localhost/foo?authSource=bar",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user",
- "password": "password",
- "db": "bar"
- },
- "options": null
- },
- {
- "description": "should recognise the mechanism (GSSAPI)",
- "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user@DOMAIN.COM",
- "password": null,
- "db": "$external"
- },
- "options": {
- "authmechanism": "GSSAPI"
- }
- },
- {
- "description": "should ignore the database (GSSAPI)",
- "uri": "mongodb://user%40DOMAIN.COM@localhost/foo?authMechanism=GSSAPI",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user@DOMAIN.COM",
- "password": null,
- "db": "$external"
- },
- "options": {
- "authmechanism": "GSSAPI"
- }
- },
- {
- "description": "should accept valid authSource (GSSAPI)",
- "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authSource=$external",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user@DOMAIN.COM",
- "password": null,
- "db": "$external"
- },
- "options": {
- "authmechanism": "GSSAPI"
- }
- },
- {
- "description": "should accept generic mechanism property (GSSAPI)",
- "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:true",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user@DOMAIN.COM",
- "password": null,
- "db": "$external"
- },
- "options": {
- "authmechanism": "GSSAPI",
- "authmechanismproperties": {
- "SERVICE_NAME": "other",
- "CANONICALIZE_HOST_NAME": true
- }
- }
- },
- {
- "description": "should accept the password (GSSAPI)",
- "uri": "mongodb://user%40DOMAIN.COM:password@localhost/?authMechanism=GSSAPI&authSource=$external",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user@DOMAIN.COM",
- "password": "password",
- "db": "$external"
- },
- "options": {
- "authmechanism": "GSSAPI"
- }
- },
- {
- "description": "may support deprecated gssapiServiceName option (GSSAPI)",
- "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&gssapiServiceName=other",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user@DOMAIN.COM",
- "password": null,
- "db": "$external"
- },
- "options": {
- "authmechanism": "GSSAPI",
- "authmechanismproperties": {
- "SERVICE_NAME": "other"
- }
- }
- },
- {
- "description": "should throw an exception if authSource is invalid (GSSAPI)",
- "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authSource=foo",
- "hosts": null,
- "valid": false,
- "warning": false,
- "auth": null,
- "options": null
- },
- {
- "description": "should throw an exception if no username (GSSAPI)",
- "uri": "mongodb://localhost/?authMechanism=GSSAPI",
- "hosts": null,
- "valid": false,
- "warning": false,
- "auth": null,
- "options": null
- },
- {
- "description": "should recognize the mechanism (MONGODB-CR)",
- "uri": "mongodb://user:password@localhost/?authMechanism=MONGODB-CR",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user",
- "password": "password",
- "db": "admin"
- },
- "options": {
- "authmechanism": "MONGODB-CR"
- }
- },
- {
- "description": "should use the database when no authSource is specified (MONGODB-CR)",
- "uri": "mongodb://user:password@localhost/foo?authMechanism=MONGODB-CR",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user",
- "password": "password",
- "db": "foo"
- },
- "options": {
- "authmechanism": "MONGODB-CR"
- }
- },
- {
- "description": "should use the authSource when specified (MONGODB-CR)",
- "uri": "mongodb://user:password@localhost/foo?authMechanism=MONGODB-CR&authSource=bar",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user",
- "password": "password",
- "db": "bar"
- },
- "options": {
- "authmechanism": "MONGODB-CR"
- }
- },
- {
- "description": "should throw an exception if no username is supplied (MONGODB-CR)",
- "uri": "mongodb://localhost/?authMechanism=MONGODB-CR",
- "hosts": null,
- "valid": false,
- "warning": false,
- "auth": null,
- "options": null
- },
- {
- "description": "should recognize the mechanism (MONGODB-X509)",
- "uri": "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/?authMechanism=MONGODB-X509",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry",
- "password": null,
- "db": "$external"
- },
- "options": {
- "authmechanism": "MONGODB-X509"
- }
- },
- {
- "description": "should ignore the database (MONGODB-X509)",
- "uri": "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/foo?authMechanism=MONGODB-X509",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry",
- "password": null,
- "db": "$external"
- },
- "options": {
- "authmechanism": "MONGODB-X509"
- }
- },
- {
- "description": "should accept valid authSource (MONGODB-X509)",
- "uri": "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/?authMechanism=MONGODB-X509&authSource=$external",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry",
- "password": null,
- "db": "$external"
- },
- "options": {
- "authmechanism": "MONGODB-X509"
- }
- },
- {
- "description": "should recognize the mechanism with no username (MONGODB-X509)",
- "uri": "mongodb://localhost/?authMechanism=MONGODB-X509",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": null,
- "password": null,
- "db": "$external"
- },
- "options": {
- "authmechanism": "MONGODB-X509"
- }
- },
- {
- "description": "should throw an exception if supplied a password (MONGODB-X509)",
- "uri": "mongodb://user:password@localhost/?authMechanism=MONGODB-X509",
- "hosts": null,
- "valid": false,
- "warning": false,
- "auth": null,
- "options": null
- },
- {
- "description": "should throw an exception if authSource is invalid (MONGODB-X509)",
- "uri": "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/foo?authMechanism=MONGODB-X509&authSource=bar",
- "hosts": null,
- "valid": false,
- "warning": false,
- "auth": null,
- "options": null
- },
- {
- "description": "should recognize the mechanism (PLAIN)",
- "uri": "mongodb://user:password@localhost/?authMechanism=PLAIN",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user",
- "password": "password",
- "db": "$external"
- },
- "options": {
- "authmechanism": "PLAIN"
- }
- },
- {
- "description": "should use the database when no authSource is specified (PLAIN)",
- "uri": "mongodb://user:password@localhost/foo?authMechanism=PLAIN",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user",
- "password": "password",
- "db": "foo"
- },
- "options": {
- "authmechanism": "PLAIN"
- }
- },
- {
- "description": "should use the authSource when specified (PLAIN)",
- "uri": "mongodb://user:password@localhost/foo?authMechanism=PLAIN&authSource=bar",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user",
- "password": "password",
- "db": "bar"
- },
- "options": {
- "authmechanism": "PLAIN"
- }
- },
- {
- "description": "should throw an exception if no username (PLAIN)",
- "uri": "mongodb://localhost/?authMechanism=PLAIN",
- "hosts": null,
- "valid": false,
- "warning": false,
- "auth": null,
- "options": null
- },
- {
- "description": "should recognize the mechanism (SCRAM-SHA-1)",
- "uri": "mongodb://user:password@localhost/?authMechanism=SCRAM-SHA-1",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user",
- "password": "password",
- "db": "admin"
- },
- "options": {
- "authmechanism": "SCRAM-SHA-1"
- }
- },
- {
- "description": "should use the database when no authSource is specified (SCRAM-SHA-1)",
- "uri": "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-1",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user",
- "password": "password",
- "db": "foo"
- },
- "options": {
- "authmechanism": "SCRAM-SHA-1"
- }
- },
- {
- "description": "should accept valid authSource (SCRAM-SHA-1)",
- "uri": "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-1&authSource=bar",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user",
- "password": "password",
- "db": "bar"
- },
- "options": {
- "authmechanism": "SCRAM-SHA-1"
- }
- },
- {
- "description": "should throw an exception if no username (SCRAM-SHA-1)",
- "uri": "mongodb://localhost/?authMechanism=SCRAM-SHA-1",
- "hosts": null,
- "valid": false,
- "warning": false,
- "auth": null,
- "options": null
- },
- {
- "description": "should recognize the mechanism (SCRAM-SHA-256)",
- "uri": "mongodb://user:password@localhost/?authMechanism=SCRAM-SHA-256",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user",
- "password": "password",
- "db": "admin"
- },
- "options": {
- "authmechanism": "SCRAM-SHA-256"
- }
- },
- {
- "description": "should use the database when no authSource is specified (SCRAM-SHA-256)",
- "uri": "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-256",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user",
- "password": "password",
- "db": "foo"
- },
- "options": {
- "authmechanism": "SCRAM-SHA-256"
- }
- },
- {
- "description": "should accept valid authSource (SCRAM-SHA-256)",
- "uri": "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-256&authSource=bar",
- "hosts": null,
- "valid": true,
- "warning": false,
- "auth": {
- "username": "user",
- "password": "password",
- "db": "bar"
- },
- "options": {
- "authmechanism": "SCRAM-SHA-256"
- }
- },
- {
- "description": "should throw an exception if no username (SCRAM-SHA-256)",
- "uri": "mongodb://localhost/?authMechanism=SCRAM-SHA-256",
- "hosts": null,
- "valid": false,
- "warning": false,
- "auth": null,
- "options": null
- }
- ]
-}
diff --git a/source/auth/tests/connection-string.yml b/source/auth/tests/connection-string.yml
deleted file mode 100644
index be1dcccd13..0000000000
--- a/source/auth/tests/connection-string.yml
+++ /dev/null
@@ -1,367 +0,0 @@
-tests:
- -
- description: "should use the default source and mechanism"
- uri: "mongodb://user:password@localhost"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user"
- password: "password"
- db: "admin"
- options: ~
- -
- description: "should use the database when no authSource is specified"
- uri: "mongodb://user:password@localhost/foo"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user"
- password: "password"
- db: "foo"
- options: ~
- -
- description: "should use the authSource when specified"
- uri: "mongodb://user:password@localhost/foo?authSource=bar"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user"
- password: "password"
- db: "bar"
- options: ~
- -
- description: "should recognise the mechanism (GSSAPI)"
- uri: "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user@DOMAIN.COM"
- password: ~
- db: "$external"
- options:
- authmechanism: "GSSAPI"
- -
- description: "should ignore the database (GSSAPI)"
- uri: "mongodb://user%40DOMAIN.COM@localhost/foo?authMechanism=GSSAPI"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user@DOMAIN.COM"
- password: ~
- db: "$external"
- options:
- authmechanism: "GSSAPI"
- -
- description: "should accept valid authSource (GSSAPI)"
- uri: "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authSource=$external"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user@DOMAIN.COM"
- password: ~
- db: "$external"
- options:
- authmechanism: "GSSAPI"
- -
- description: "should accept generic mechanism property (GSSAPI)"
- uri: "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:true"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user@DOMAIN.COM"
- password: ~
- db: "$external"
- options:
- authmechanism: "GSSAPI"
- authmechanismproperties:
- SERVICE_NAME: "other"
- CANONICALIZE_HOST_NAME: true
- -
- description: "should accept the password (GSSAPI)"
- uri: "mongodb://user%40DOMAIN.COM:password@localhost/?authMechanism=GSSAPI&authSource=$external"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user@DOMAIN.COM"
- password: "password"
- db: "$external"
- options:
- authmechanism: "GSSAPI"
- -
- description: "may support deprecated gssapiServiceName option (GSSAPI)"
- uri: "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&gssapiServiceName=other"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user@DOMAIN.COM"
- password: ~
- db: "$external"
- options:
- authmechanism: "GSSAPI"
- authmechanismproperties:
- SERVICE_NAME: "other"
- -
- description: "should throw an exception if authSource is invalid (GSSAPI)"
- uri: "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authSource=foo"
- hosts: ~
- valid: false
- warning: false
- auth: ~
- options: ~
- -
- description: "should throw an exception if no username (GSSAPI)"
- uri: "mongodb://localhost/?authMechanism=GSSAPI"
- hosts: ~
- valid: false
- warning: false
- auth: ~
- options: ~
- -
- description: "should recognize the mechanism (MONGODB-CR)"
- uri: "mongodb://user:password@localhost/?authMechanism=MONGODB-CR"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user"
- password: "password"
- db: "admin"
- options:
- authmechanism: "MONGODB-CR"
- -
- description: "should use the database when no authSource is specified (MONGODB-CR)"
- uri: "mongodb://user:password@localhost/foo?authMechanism=MONGODB-CR"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user"
- password: "password"
- db: "foo"
- options:
- authmechanism: "MONGODB-CR"
- -
- description: "should use the authSource when specified (MONGODB-CR)"
- uri: "mongodb://user:password@localhost/foo?authMechanism=MONGODB-CR&authSource=bar"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user"
- password: "password"
- db: "bar"
- options:
- authmechanism: "MONGODB-CR"
- -
- description: "should throw an exception if no username is supplied (MONGODB-CR)"
- uri: "mongodb://localhost/?authMechanism=MONGODB-CR"
- hosts: ~
- valid: false
- warning: false
- auth: ~
- options: ~
- -
- description: "should recognize the mechanism (MONGODB-X509)"
- uri: "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/?authMechanism=MONGODB-X509"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry"
- password: ~
- db: "$external"
- options:
- authmechanism: MONGODB-X509
- -
- description: "should ignore the database (MONGODB-X509)"
- uri: "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/foo?authMechanism=MONGODB-X509"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry"
- password: ~
- db: "$external"
- options:
- authmechanism: MONGODB-X509
- -
- description: "should accept valid authSource (MONGODB-X509)"
- uri: "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/?authMechanism=MONGODB-X509&authSource=$external"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry"
- password: ~
- db: "$external"
- options:
- authmechanism: MONGODB-X509
- -
- description: "should recognize the mechanism with no username (MONGODB-X509)"
- uri: "mongodb://localhost/?authMechanism=MONGODB-X509"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: ~
- password: ~
- db: "$external"
- options:
- authmechanism: "MONGODB-X509"
- -
- description: "should throw an exception if supplied a password (MONGODB-X509)"
- uri: "mongodb://user:password@localhost/?authMechanism=MONGODB-X509"
- hosts: ~
- valid: false
- warning: false
- auth: ~
- options: ~
- -
- description: "should throw an exception if authSource is invalid (MONGODB-X509)"
- uri: "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/foo?authMechanism=MONGODB-X509&authSource=bar"
- hosts: ~
- valid: false
- warning: false
- auth: ~
- options: ~
- -
- description: "should recognize the mechanism (PLAIN)"
- uri: "mongodb://user:password@localhost/?authMechanism=PLAIN"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user"
- password: "password"
- db: "$external"
- options:
- authmechanism: "PLAIN"
- -
- description: "should use the database when no authSource is specified (PLAIN)"
- uri: "mongodb://user:password@localhost/foo?authMechanism=PLAIN"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user"
- password: "password"
- db: "foo"
- options:
- authmechanism: "PLAIN"
- -
- description: "should use the authSource when specified (PLAIN)"
- uri: "mongodb://user:password@localhost/foo?authMechanism=PLAIN&authSource=bar"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user"
- password: "password"
- db: "bar"
- options:
- authmechanism: "PLAIN"
- -
- description: "should throw an exception if no username (PLAIN)"
- uri: "mongodb://localhost/?authMechanism=PLAIN"
- hosts: ~
- valid: false
- warning: false
- auth: ~
- options: ~
- -
- description: "should recognize the mechanism (SCRAM-SHA-1)"
- uri: "mongodb://user:password@localhost/?authMechanism=SCRAM-SHA-1"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user"
- password: "password"
- db: "admin"
- options:
- authmechanism: "SCRAM-SHA-1"
- -
- description: "should use the database when no authSource is specified (SCRAM-SHA-1)"
- uri: "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-1"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user"
- password: "password"
- db: "foo"
- options:
- authmechanism: "SCRAM-SHA-1"
- -
- description: "should accept valid authSource (SCRAM-SHA-1)"
- uri: "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-1&authSource=bar"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user"
- password: "password"
- db: "bar"
- options:
- authmechanism: "SCRAM-SHA-1"
- -
- description: "should throw an exception if no username (SCRAM-SHA-1)"
- uri: "mongodb://localhost/?authMechanism=SCRAM-SHA-1"
- hosts: ~
- valid: false
- warning: false
- auth: ~
- options: ~
- -
- description: "should recognize the mechanism (SCRAM-SHA-256)"
- uri: "mongodb://user:password@localhost/?authMechanism=SCRAM-SHA-256"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user"
- password: "password"
- db: "admin"
- options:
- authmechanism: "SCRAM-SHA-256"
- -
- description: "should use the database when no authSource is specified (SCRAM-SHA-256)"
- uri: "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-256"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user"
- password: "password"
- db: "foo"
- options:
- authmechanism: "SCRAM-SHA-256"
- -
- description: "should accept valid authSource (SCRAM-SHA-256)"
- uri: "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-256&authSource=bar"
- hosts: ~
- valid: true
- warning: false
- auth:
- username: "user"
- password: "password"
- db: "bar"
- options:
- authmechanism: "SCRAM-SHA-256"
- -
- description: "should throw an exception if no username (SCRAM-SHA-256)"
- uri: "mongodb://localhost/?authMechanism=SCRAM-SHA-256"
- hosts: ~
- valid: false
- warning: false
- auth: ~
- options: ~
\ No newline at end of file
diff --git a/source/auth/tests/legacy/connection-string.json b/source/auth/tests/legacy/connection-string.json
new file mode 100644
index 0000000000..8982b61d5a
--- /dev/null
+++ b/source/auth/tests/legacy/connection-string.json
@@ -0,0 +1,666 @@
+{
+ "tests": [
+ {
+ "description": "should use the default source and mechanism",
+ "uri": "mongodb://user:password@localhost",
+ "valid": true,
+ "credential": {
+ "username": "user",
+ "password": "password",
+ "source": "admin",
+ "mechanism": null,
+ "mechanism_properties": null
+ }
+ },
+ {
+ "description": "should use the database when no authSource is specified",
+ "uri": "mongodb://user:password@localhost/foo",
+ "valid": true,
+ "credential": {
+ "username": "user",
+ "password": "password",
+ "source": "foo",
+ "mechanism": null,
+ "mechanism_properties": null
+ }
+ },
+ {
+ "description": "should use the authSource when specified",
+ "uri": "mongodb://user:password@localhost/foo?authSource=bar",
+ "valid": true,
+ "credential": {
+ "username": "user",
+ "password": "password",
+ "source": "bar",
+ "mechanism": null,
+ "mechanism_properties": null
+ }
+ },
+ {
+ "description": "should recognise the mechanism (GSSAPI)",
+ "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI",
+ "valid": true,
+ "credential": {
+ "username": "user@DOMAIN.COM",
+ "password": null,
+ "source": "$external",
+ "mechanism": "GSSAPI",
+ "mechanism_properties": {
+ "SERVICE_NAME": "mongodb"
+ }
+ }
+ },
+ {
+ "description": "should ignore the database (GSSAPI)",
+ "uri": "mongodb://user%40DOMAIN.COM@localhost/foo?authMechanism=GSSAPI",
+ "valid": true,
+ "credential": {
+ "username": "user@DOMAIN.COM",
+ "password": null,
+ "source": "$external",
+ "mechanism": "GSSAPI",
+ "mechanism_properties": {
+ "SERVICE_NAME": "mongodb"
+ }
+ }
+ },
+ {
+ "description": "should accept valid authSource (GSSAPI)",
+ "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authSource=$external",
+ "valid": true,
+ "credential": {
+ "username": "user@DOMAIN.COM",
+ "password": null,
+ "source": "$external",
+ "mechanism": "GSSAPI",
+ "mechanism_properties": {
+ "SERVICE_NAME": "mongodb"
+ }
+ }
+ },
+ {
+ "description": "should accept generic mechanism property (GSSAPI)",
+ "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:forward,SERVICE_HOST:example.com",
+ "valid": true,
+ "credential": {
+ "username": "user@DOMAIN.COM",
+ "password": null,
+ "source": "$external",
+ "mechanism": "GSSAPI",
+ "mechanism_properties": {
+ "SERVICE_NAME": "other",
+ "SERVICE_HOST": "example.com",
+ "CANONICALIZE_HOST_NAME": "forward"
+ }
+ }
+ },
+ {
+ "description": "should accept forwardAndReverse hostname canonicalization (GSSAPI)",
+ "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:forwardAndReverse",
+ "valid": true,
+ "credential": {
+ "username": "user@DOMAIN.COM",
+ "password": null,
+ "source": "$external",
+ "mechanism": "GSSAPI",
+ "mechanism_properties": {
+ "SERVICE_NAME": "other",
+ "CANONICALIZE_HOST_NAME": "forwardAndReverse"
+ }
+ }
+ },
+ {
+ "description": "should accept no hostname canonicalization (GSSAPI)",
+ "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:none",
+ "valid": true,
+ "credential": {
+ "username": "user@DOMAIN.COM",
+ "password": null,
+ "source": "$external",
+ "mechanism": "GSSAPI",
+ "mechanism_properties": {
+ "SERVICE_NAME": "other",
+ "CANONICALIZE_HOST_NAME": "none"
+ }
+ }
+ },
+ {
+ "description": "must raise an error when the hostname canonicalization is invalid",
+ "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:invalid",
+ "valid": false
+ },
+ {
+ "description": "should accept the password (GSSAPI)",
+ "uri": "mongodb://user%40DOMAIN.COM:password@localhost/?authMechanism=GSSAPI&authSource=$external",
+ "valid": true,
+ "credential": {
+ "username": "user@DOMAIN.COM",
+ "password": "password",
+ "source": "$external",
+ "mechanism": "GSSAPI",
+ "mechanism_properties": {
+ "SERVICE_NAME": "mongodb"
+ }
+ }
+ },
+ {
+ "description": "must raise an error when the authSource is empty",
+ "uri": "mongodb://user:password@localhost/foo?authSource=",
+ "valid": false
+ },
+ {
+ "description": "must raise an error when the authSource is empty without credentials",
+ "uri": "mongodb://localhost/admin?authSource=",
+ "valid": false
+ },
+ {
+ "description": "should throw an exception if authSource is invalid (GSSAPI)",
+ "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authSource=foo",
+ "valid": false
+ },
+ {
+ "description": "should throw an exception if no username (GSSAPI)",
+ "uri": "mongodb://localhost/?authMechanism=GSSAPI",
+ "valid": false
+ },
+ {
+ "description": "should recognize the mechanism (MONGODB-X509)",
+ "uri": "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/?authMechanism=MONGODB-X509",
+ "valid": true,
+ "credential": {
+ "username": "CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry",
+ "password": null,
+ "source": "$external",
+ "mechanism": "MONGODB-X509",
+ "mechanism_properties": null
+ }
+ },
+ {
+ "description": "should ignore the database (MONGODB-X509)",
+ "uri": "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/foo?authMechanism=MONGODB-X509",
+ "valid": true,
+ "credential": {
+ "username": "CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry",
+ "password": null,
+ "source": "$external",
+ "mechanism": "MONGODB-X509",
+ "mechanism_properties": null
+ }
+ },
+ {
+ "description": "should accept valid authSource (MONGODB-X509)",
+ "uri": "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/?authMechanism=MONGODB-X509&authSource=$external",
+ "valid": true,
+ "credential": {
+ "username": "CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry",
+ "password": null,
+ "source": "$external",
+ "mechanism": "MONGODB-X509",
+ "mechanism_properties": null
+ }
+ },
+ {
+ "description": "should recognize the mechanism with no username (MONGODB-X509)",
+ "uri": "mongodb://localhost/?authMechanism=MONGODB-X509",
+ "valid": true,
+ "credential": {
+ "username": null,
+ "password": null,
+ "source": "$external",
+ "mechanism": "MONGODB-X509",
+ "mechanism_properties": null
+ }
+ },
+ {
+ "description": "should recognize the mechanism with no username when auth source is explicitly specified (MONGODB-X509)",
+ "uri": "mongodb://localhost/?authMechanism=MONGODB-X509&authSource=$external",
+ "valid": true,
+ "credential": {
+ "username": null,
+ "password": null,
+ "source": "$external",
+ "mechanism": "MONGODB-X509",
+ "mechanism_properties": null
+ }
+ },
+ {
+ "description": "should throw an exception if supplied a password (MONGODB-X509)",
+ "uri": "mongodb://user:password@localhost/?authMechanism=MONGODB-X509",
+ "valid": false
+ },
+ {
+ "description": "should throw an exception if authSource is invalid (MONGODB-X509)",
+ "uri": "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/foo?authMechanism=MONGODB-X509&authSource=bar",
+ "valid": false
+ },
+ {
+ "description": "should recognize the mechanism (PLAIN)",
+ "uri": "mongodb://user:password@localhost/?authMechanism=PLAIN",
+ "valid": true,
+ "credential": {
+ "username": "user",
+ "password": "password",
+ "source": "$external",
+ "mechanism": "PLAIN",
+ "mechanism_properties": null
+ }
+ },
+ {
+ "description": "should use the database when no authSource is specified (PLAIN)",
+ "uri": "mongodb://user:password@localhost/foo?authMechanism=PLAIN",
+ "valid": true,
+ "credential": {
+ "username": "user",
+ "password": "password",
+ "source": "foo",
+ "mechanism": "PLAIN",
+ "mechanism_properties": null
+ }
+ },
+ {
+ "description": "should use the authSource when specified (PLAIN)",
+ "uri": "mongodb://user:password@localhost/foo?authMechanism=PLAIN&authSource=bar",
+ "valid": true,
+ "credential": {
+ "username": "user",
+ "password": "password",
+ "source": "bar",
+ "mechanism": "PLAIN",
+ "mechanism_properties": null
+ }
+ },
+ {
+ "description": "should throw an exception if no username (PLAIN)",
+ "uri": "mongodb://localhost/?authMechanism=PLAIN",
+ "valid": false
+ },
+ {
+ "description": "should recognize the mechanism (SCRAM-SHA-1)",
+ "uri": "mongodb://user:password@localhost/?authMechanism=SCRAM-SHA-1",
+ "valid": true,
+ "credential": {
+ "username": "user",
+ "password": "password",
+ "source": "admin",
+ "mechanism": "SCRAM-SHA-1",
+ "mechanism_properties": null
+ }
+ },
+ {
+ "description": "should use the database when no authSource is specified (SCRAM-SHA-1)",
+ "uri": "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-1",
+ "valid": true,
+ "credential": {
+ "username": "user",
+ "password": "password",
+ "source": "foo",
+ "mechanism": "SCRAM-SHA-1",
+ "mechanism_properties": null
+ }
+ },
+ {
+ "description": "should accept valid authSource (SCRAM-SHA-1)",
+ "uri": "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-1&authSource=bar",
+ "valid": true,
+ "credential": {
+ "username": "user",
+ "password": "password",
+ "source": "bar",
+ "mechanism": "SCRAM-SHA-1",
+ "mechanism_properties": null
+ }
+ },
+ {
+ "description": "should throw an exception if no username (SCRAM-SHA-1)",
+ "uri": "mongodb://localhost/?authMechanism=SCRAM-SHA-1",
+ "valid": false
+ },
+ {
+ "description": "should recognize the mechanism (SCRAM-SHA-256)",
+ "uri": "mongodb://user:password@localhost/?authMechanism=SCRAM-SHA-256",
+ "valid": true,
+ "credential": {
+ "username": "user",
+ "password": "password",
+ "source": "admin",
+ "mechanism": "SCRAM-SHA-256",
+ "mechanism_properties": null
+ }
+ },
+ {
+ "description": "should use the database when no authSource is specified (SCRAM-SHA-256)",
+ "uri": "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-256",
+ "valid": true,
+ "credential": {
+ "username": "user",
+ "password": "password",
+ "source": "foo",
+ "mechanism": "SCRAM-SHA-256",
+ "mechanism_properties": null
+ }
+ },
+ {
+ "description": "should accept valid authSource (SCRAM-SHA-256)",
+ "uri": "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-256&authSource=bar",
+ "valid": true,
+ "credential": {
+ "username": "user",
+ "password": "password",
+ "source": "bar",
+ "mechanism": "SCRAM-SHA-256",
+ "mechanism_properties": null
+ }
+ },
+ {
+ "description": "should throw an exception if no username (SCRAM-SHA-256)",
+ "uri": "mongodb://localhost/?authMechanism=SCRAM-SHA-256",
+ "valid": false
+ },
+ {
+ "description": "URI with no auth-related info doesn't create credential",
+ "uri": "mongodb://localhost/",
+ "valid": true,
+ "credential": null
+ },
+ {
+ "description": "database in URI path doesn't create credentials",
+ "uri": "mongodb://localhost/foo",
+ "valid": true,
+ "credential": null
+ },
+ {
+ "description": "authSource without username doesn't create credential (default mechanism)",
+ "uri": "mongodb://localhost/?authSource=foo",
+ "valid": true,
+ "credential": null
+ },
+ {
+ "description": "should throw an exception if no username provided (userinfo implies default mechanism)",
+ "uri": "mongodb://@localhost.com/",
+ "valid": false
+ },
+ {
+ "description": "should throw an exception if no username/password provided (userinfo implies default mechanism)",
+ "uri": "mongodb://:@localhost.com/",
+ "valid": false
+ },
+ {
+ "description": "should recognise the mechanism (MONGODB-AWS)",
+ "uri": "mongodb://localhost/?authMechanism=MONGODB-AWS",
+ "valid": true,
+ "credential": {
+ "username": null,
+ "password": null,
+ "source": "$external",
+ "mechanism": "MONGODB-AWS",
+ "mechanism_properties": null
+ }
+ },
+ {
+ "description": "should recognise the mechanism when auth source is explicitly specified (MONGODB-AWS)",
+ "uri": "mongodb://localhost/?authMechanism=MONGODB-AWS&authSource=$external",
+ "valid": true,
+ "credential": {
+ "username": null,
+ "password": null,
+ "source": "$external",
+ "mechanism": "MONGODB-AWS",
+ "mechanism_properties": null
+ }
+ },
+ {
+ "description": "should throw an exception if username and no password (MONGODB-AWS)",
+ "uri": "mongodb://user@localhost/?authMechanism=MONGODB-AWS",
+ "valid": false,
+ "credential": null
+ },
+ {
+ "description": "should use username and password if specified (MONGODB-AWS)",
+ "uri": "mongodb://user%21%40%23%24%25%5E%26%2A%28%29_%2B:pass%21%40%23%24%25%5E%26%2A%28%29_%2B@localhost/?authMechanism=MONGODB-AWS",
+ "valid": true,
+ "credential": {
+ "username": "user!@#$%^&*()_+",
+ "password": "pass!@#$%^&*()_+",
+ "source": "$external",
+ "mechanism": "MONGODB-AWS",
+ "mechanism_properties": null
+ }
+ },
+ {
+ "description": "should use username, password and session token if specified (MONGODB-AWS)",
+ "uri": "mongodb://user:password@localhost/?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN:token%21%40%23%24%25%5E%26%2A%28%29_%2B",
+ "valid": true,
+ "credential": {
+ "username": "user",
+ "password": "password",
+ "source": "$external",
+ "mechanism": "MONGODB-AWS",
+ "mechanism_properties": {
+ "AWS_SESSION_TOKEN": "token!@#$%^&*()_+"
+ }
+ }
+ },
+ {
+ "description": "should throw an exception if username provided (MONGODB-AWS)",
+ "uri": "mongodb://user@localhost.com/?authMechanism=MONGODB-AWS",
+ "valid": false
+ },
+ {
+ "description": "should throw an exception if username and password provided (MONGODB-AWS)",
+ "uri": "mongodb://user:pass@localhost.com/?authMechanism=MONGODB-AWS",
+ "valid": false
+ },
+ {
+ "description": "should throw an exception if AWS_SESSION_TOKEN provided (MONGODB-AWS)",
+ "uri": "mongodb://localhost/?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN:token",
+ "valid": false
+ },
+ {
+ "description": "should recognise the mechanism with test environment (MONGODB-OIDC)",
+ "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test",
+ "valid": true,
+ "credential": {
+ "username": null,
+ "password": null,
+ "source": "$external",
+ "mechanism": "MONGODB-OIDC",
+ "mechanism_properties": {
+ "ENVIRONMENT": "test"
+ }
+ }
+ },
+ {
+ "description": "should recognise the mechanism when auth source is explicitly specified and with environment (MONGODB-OIDC)",
+ "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external&authMechanismProperties=ENVIRONMENT:test",
+ "valid": true,
+ "credential": {
+ "username": null,
+ "password": null,
+ "source": "$external",
+ "mechanism": "MONGODB-OIDC",
+ "mechanism_properties": {
+ "ENVIRONMENT": "test"
+ }
+ }
+ },
+ {
+ "description": "should throw an exception if supplied a password (MONGODB-OIDC)",
+ "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test",
+ "valid": false,
+ "credential": null
+ },
+ {
+ "description": "should throw an exception if username is specified for test (MONGODB-OIDC)",
+ "uri": "mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test",
+ "valid": false,
+ "credential": null
+ },
+ {
+ "description": "should throw an exception if specified environment is not supported (MONGODB-OIDC)",
+ "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:invalid",
+ "valid": false,
+ "credential": null
+ },
+ {
+ "description": "should throw an exception if neither environment nor callbacks specified (MONGODB-OIDC)",
+ "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC",
+ "valid": false,
+ "credential": null
+ },
+ {
+ "description": "should throw an exception when unsupported auth property is specified (MONGODB-OIDC)",
+ "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=UnsupportedProperty:unexisted",
+ "valid": false,
+ "credential": null
+ },
+ {
+ "description": "should recognise the mechanism with azure provider (MONGODB-OIDC)",
+ "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo",
+ "valid": true,
+ "credential": {
+ "username": null,
+ "password": null,
+ "source": "$external",
+ "mechanism": "MONGODB-OIDC",
+ "mechanism_properties": {
+ "ENVIRONMENT": "azure",
+ "TOKEN_RESOURCE": "foo"
+ }
+ }
+ },
+ {
+ "description": "should accept a username with azure provider (MONGODB-OIDC)",
+ "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo",
+ "valid": true,
+ "credential": {
+ "username": "user",
+ "password": null,
+ "source": "$external",
+ "mechanism": "MONGODB-OIDC",
+ "mechanism_properties": {
+ "ENVIRONMENT": "azure",
+ "TOKEN_RESOURCE": "foo"
+ }
+ }
+ },
+ {
+ "description": "should accept a url-encoded TOKEN_RESOURCE (MONGODB-OIDC)",
+ "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:mongodb%3A%2F%2Ftest-cluster",
+ "valid": true,
+ "credential": {
+ "username": "user",
+ "password": null,
+ "source": "$external",
+ "mechanism": "MONGODB-OIDC",
+ "mechanism_properties": {
+ "ENVIRONMENT": "azure",
+ "TOKEN_RESOURCE": "mongodb://test-cluster"
+ }
+ }
+ },
+ {
+ "description": "should accept an un-encoded TOKEN_RESOURCE (MONGODB-OIDC)",
+ "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:mongodb://test-cluster",
+ "valid": true,
+ "credential": {
+ "username": "user",
+ "password": null,
+ "source": "$external",
+ "mechanism": "MONGODB-OIDC",
+ "mechanism_properties": {
+ "ENVIRONMENT": "azure",
+ "TOKEN_RESOURCE": "mongodb://test-cluster"
+ }
+ }
+ },
+ {
+ "description": "should handle a complicated url-encoded TOKEN_RESOURCE (MONGODB-OIDC)",
+ "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:abcd%25ef%3Ag%26hi",
+ "valid": true,
+ "credential": {
+ "username": "user",
+ "password": null,
+ "source": "$external",
+ "mechanism": "MONGODB-OIDC",
+ "mechanism_properties": {
+ "ENVIRONMENT": "azure",
+ "TOKEN_RESOURCE": "abcd%ef:g&hi"
+ }
+ }
+ },
+ {
+ "description": "should url-encode a TOKEN_RESOURCE (MONGODB-OIDC)",
+ "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:a$b",
+ "valid": true,
+ "credential": {
+ "username": "user",
+ "password": null,
+ "source": "$external",
+ "mechanism": "MONGODB-OIDC",
+ "mechanism_properties": {
+ "ENVIRONMENT": "azure",
+ "TOKEN_RESOURCE": "a$b"
+ }
+ }
+ },
+ {
+ "description": "should accept a username and throw an error for a password with azure provider (MONGODB-OIDC)",
+ "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo",
+ "valid": false,
+ "credential": null
+ },
+ {
+ "description": "should throw an exception if no token audience is given for azure provider (MONGODB-OIDC)",
+ "uri": "mongodb://username@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure",
+ "valid": false,
+ "credential": null
+ },
+ {
+ "description": "should recognise the mechanism with gcp provider (MONGODB-OIDC)",
+ "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:foo",
+ "valid": true,
+ "credential": {
+ "username": null,
+ "password": null,
+ "source": "$external",
+ "mechanism": "MONGODB-OIDC",
+ "mechanism_properties": {
+ "ENVIRONMENT": "gcp",
+ "TOKEN_RESOURCE": "foo"
+ }
+ }
+ },
+ {
+ "description": "should throw an error for a username and password with gcp provider (MONGODB-OIDC)",
+ "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:foo",
+ "valid": false,
+ "credential": null
+ },
+ {
+ "description": "should throw an error if not TOKEN_RESOURCE with gcp provider (MONGODB-OIDC)",
+ "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp",
+ "valid": false,
+ "credential": null
+ },
+ {
+ "description": "should recognise the mechanism with k8s provider (MONGODB-OIDC)",
+ "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:k8s",
+ "valid": true,
+ "credential": {
+ "username": null,
+ "password": null,
+ "source": "$external",
+ "mechanism": "MONGODB-OIDC",
+ "mechanism_properties": {
+ "ENVIRONMENT": "k8s"
+ }
+ }
+ },
+ {
+ "description": "should throw an error for a username and password with k8s provider (MONGODB-OIDC)",
+ "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:k8s",
+ "valid": false,
+ "credential": null
+ }
+ ]
+}
diff --git a/source/auth/tests/legacy/connection-string.yml b/source/auth/tests/legacy/connection-string.yml
new file mode 100644
index 0000000000..2b98f0f8f2
--- /dev/null
+++ b/source/auth/tests/legacy/connection-string.yml
@@ -0,0 +1,480 @@
+---
+tests:
+- description: should use the default source and mechanism
+ uri: mongodb://user:password@localhost
+ valid: true
+ credential:
+ username: user
+ password: password
+ source: admin
+ mechanism:
+ mechanism_properties:
+- description: should use the database when no authSource is specified
+ uri: mongodb://user:password@localhost/foo
+ valid: true
+ credential:
+ username: user
+ password: password
+ source: foo
+ mechanism:
+ mechanism_properties:
+- description: should use the authSource when specified
+ uri: mongodb://user:password@localhost/foo?authSource=bar
+ valid: true
+ credential:
+ username: user
+ password: password
+ source: bar
+ mechanism:
+ mechanism_properties:
+- description: should recognise the mechanism (GSSAPI)
+ uri: mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI
+ valid: true
+ credential:
+ username: user@DOMAIN.COM
+ password:
+ source: "$external"
+ mechanism: GSSAPI
+ mechanism_properties:
+ SERVICE_NAME: mongodb
+- description: should ignore the database (GSSAPI)
+ uri: mongodb://user%40DOMAIN.COM@localhost/foo?authMechanism=GSSAPI
+ valid: true
+ credential:
+ username: user@DOMAIN.COM
+ password:
+ source: "$external"
+ mechanism: GSSAPI
+ mechanism_properties:
+ SERVICE_NAME: mongodb
+- description: should accept valid authSource (GSSAPI)
+ uri: mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authSource=$external
+ valid: true
+ credential:
+ username: user@DOMAIN.COM
+ password:
+ source: "$external"
+ mechanism: GSSAPI
+ mechanism_properties:
+ SERVICE_NAME: mongodb
+- description: should accept generic mechanism property (GSSAPI)
+ uri: mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:forward,SERVICE_HOST:example.com
+ valid: true
+ credential:
+ username: user@DOMAIN.COM
+ password:
+ source: "$external"
+ mechanism: GSSAPI
+ mechanism_properties:
+ SERVICE_NAME: other
+ SERVICE_HOST: example.com
+ CANONICALIZE_HOST_NAME: forward
+- description: should accept forwardAndReverse hostname canonicalization (GSSAPI)
+ uri: mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:forwardAndReverse
+ valid: true
+ credential:
+ username: user@DOMAIN.COM
+ password:
+ source: "$external"
+ mechanism: GSSAPI
+ mechanism_properties:
+ SERVICE_NAME: other
+ CANONICALIZE_HOST_NAME: forwardAndReverse
+- description: should accept no hostname canonicalization (GSSAPI)
+ uri: mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:none
+ valid: true
+ credential:
+ username: user@DOMAIN.COM
+ password:
+ source: "$external"
+ mechanism: GSSAPI
+ mechanism_properties:
+ SERVICE_NAME: other
+ CANONICALIZE_HOST_NAME: none
+- description: must raise an error when the hostname canonicalization is invalid
+ uri: mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:invalid
+ valid: false
+- description: should accept the password (GSSAPI)
+ uri: mongodb://user%40DOMAIN.COM:password@localhost/?authMechanism=GSSAPI&authSource=$external
+ valid: true
+ credential:
+ username: user@DOMAIN.COM
+ password: password
+ source: "$external"
+ mechanism: GSSAPI
+ mechanism_properties:
+ SERVICE_NAME: mongodb
+- description: must raise an error when the authSource is empty
+ uri: mongodb://user:password@localhost/foo?authSource=
+ valid: false
+- description: must raise an error when the authSource is empty without credentials
+ uri: mongodb://localhost/admin?authSource=
+ valid: false
+- description: should throw an exception if authSource is invalid (GSSAPI)
+ uri: mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authSource=foo
+ valid: false
+- description: should throw an exception if no username (GSSAPI)
+ uri: mongodb://localhost/?authMechanism=GSSAPI
+ valid: false
+- description: should recognize the mechanism (MONGODB-X509)
+ uri: mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/?authMechanism=MONGODB-X509
+ valid: true
+ credential:
+ username: CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry
+ password:
+ source: "$external"
+ mechanism: MONGODB-X509
+ mechanism_properties:
+- description: should ignore the database (MONGODB-X509)
+ uri: mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/foo?authMechanism=MONGODB-X509
+ valid: true
+ credential:
+ username: CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry
+ password:
+ source: "$external"
+ mechanism: MONGODB-X509
+ mechanism_properties:
+- description: should accept valid authSource (MONGODB-X509)
+ uri: mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/?authMechanism=MONGODB-X509&authSource=$external
+ valid: true
+ credential:
+ username: CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry
+ password:
+ source: "$external"
+ mechanism: MONGODB-X509
+ mechanism_properties:
+- description: should recognize the mechanism with no username (MONGODB-X509)
+ uri: mongodb://localhost/?authMechanism=MONGODB-X509
+ valid: true
+ credential:
+ username:
+ password:
+ source: "$external"
+ mechanism: MONGODB-X509
+ mechanism_properties:
+- description: should recognize the mechanism with no username when auth source is
+ explicitly specified (MONGODB-X509)
+ uri: mongodb://localhost/?authMechanism=MONGODB-X509&authSource=$external
+ valid: true
+ credential:
+ username:
+ password:
+ source: "$external"
+ mechanism: MONGODB-X509
+ mechanism_properties:
+- description: should throw an exception if supplied a password (MONGODB-X509)
+ uri: mongodb://user:password@localhost/?authMechanism=MONGODB-X509
+ valid: false
+- description: should throw an exception if authSource is invalid (MONGODB-X509)
+ uri: mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/foo?authMechanism=MONGODB-X509&authSource=bar
+ valid: false
+- description: should recognize the mechanism (PLAIN)
+ uri: mongodb://user:password@localhost/?authMechanism=PLAIN
+ valid: true
+ credential:
+ username: user
+ password: password
+ source: "$external"
+ mechanism: PLAIN
+ mechanism_properties:
+- description: should use the database when no authSource is specified (PLAIN)
+ uri: mongodb://user:password@localhost/foo?authMechanism=PLAIN
+ valid: true
+ credential:
+ username: user
+ password: password
+ source: foo
+ mechanism: PLAIN
+ mechanism_properties:
+- description: should use the authSource when specified (PLAIN)
+ uri: mongodb://user:password@localhost/foo?authMechanism=PLAIN&authSource=bar
+ valid: true
+ credential:
+ username: user
+ password: password
+ source: bar
+ mechanism: PLAIN
+ mechanism_properties:
+- description: should throw an exception if no username (PLAIN)
+ uri: mongodb://localhost/?authMechanism=PLAIN
+ valid: false
+- description: should recognize the mechanism (SCRAM-SHA-1)
+ uri: mongodb://user:password@localhost/?authMechanism=SCRAM-SHA-1
+ valid: true
+ credential:
+ username: user
+ password: password
+ source: admin
+ mechanism: SCRAM-SHA-1
+ mechanism_properties:
+- description: should use the database when no authSource is specified (SCRAM-SHA-1)
+ uri: mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-1
+ valid: true
+ credential:
+ username: user
+ password: password
+ source: foo
+ mechanism: SCRAM-SHA-1
+ mechanism_properties:
+- description: should accept valid authSource (SCRAM-SHA-1)
+ uri: mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-1&authSource=bar
+ valid: true
+ credential:
+ username: user
+ password: password
+ source: bar
+ mechanism: SCRAM-SHA-1
+ mechanism_properties:
+- description: should throw an exception if no username (SCRAM-SHA-1)
+ uri: mongodb://localhost/?authMechanism=SCRAM-SHA-1
+ valid: false
+- description: should recognize the mechanism (SCRAM-SHA-256)
+ uri: mongodb://user:password@localhost/?authMechanism=SCRAM-SHA-256
+ valid: true
+ credential:
+ username: user
+ password: password
+ source: admin
+ mechanism: SCRAM-SHA-256
+ mechanism_properties:
+- description: should use the database when no authSource is specified (SCRAM-SHA-256)
+ uri: mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-256
+ valid: true
+ credential:
+ username: user
+ password: password
+ source: foo
+ mechanism: SCRAM-SHA-256
+ mechanism_properties:
+- description: should accept valid authSource (SCRAM-SHA-256)
+ uri: mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-256&authSource=bar
+ valid: true
+ credential:
+ username: user
+ password: password
+ source: bar
+ mechanism: SCRAM-SHA-256
+ mechanism_properties:
+- description: should throw an exception if no username (SCRAM-SHA-256)
+ uri: mongodb://localhost/?authMechanism=SCRAM-SHA-256
+ valid: false
+- description: URI with no auth-related info doesn't create credential
+ uri: mongodb://localhost/
+ valid: true
+ credential:
+- description: database in URI path doesn't create credentials
+ uri: mongodb://localhost/foo
+ valid: true
+ credential:
+- description: authSource without username doesn't create credential (default mechanism)
+ uri: mongodb://localhost/?authSource=foo
+ valid: true
+ credential:
+- description: should throw an exception if no username provided (userinfo implies
+ default mechanism)
+ uri: mongodb://@localhost.com/
+ valid: false
+- description: should throw an exception if no username/password provided (userinfo
+ implies default mechanism)
+ uri: mongodb://:@localhost.com/
+ valid: false
+- description: should recognise the mechanism (MONGODB-AWS)
+ uri: mongodb://localhost/?authMechanism=MONGODB-AWS
+ valid: true
+ credential:
+ username:
+ password:
+ source: "$external"
+ mechanism: MONGODB-AWS
+ mechanism_properties:
+- description: should recognise the mechanism when auth source is explicitly specified
+ (MONGODB-AWS)
+ uri: mongodb://localhost/?authMechanism=MONGODB-AWS&authSource=$external
+ valid: true
+ credential:
+ username:
+ password:
+ source: "$external"
+ mechanism: MONGODB-AWS
+ mechanism_properties:
+- description: should throw an exception if username and no password (MONGODB-AWS)
+ uri: mongodb://user@localhost/?authMechanism=MONGODB-AWS
+ valid: false
+ credential:
+- description: should use username and password if specified (MONGODB-AWS)
+ uri: mongodb://user%21%40%23%24%25%5E%26%2A%28%29_%2B:pass%21%40%23%24%25%5E%26%2A%28%29_%2B@localhost/?authMechanism=MONGODB-AWS
+ valid: true
+ credential:
+ username: user!@#$%^&*()_+
+ password: pass!@#$%^&*()_+
+ source: "$external"
+ mechanism: MONGODB-AWS
+ mechanism_properties:
+- description: should use username, password and session token if specified (MONGODB-AWS)
+ uri: mongodb://user:password@localhost/?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN:token%21%40%23%24%25%5E%26%2A%28%29_%2B
+ valid: true
+ credential:
+ username: user
+ password: password
+ source: "$external"
+ mechanism: MONGODB-AWS
+ mechanism_properties:
+ AWS_SESSION_TOKEN: token!@#$%^&*()_+
+- description: should throw an exception if username provided (MONGODB-AWS)
+ uri: mongodb://user@localhost.com/?authMechanism=MONGODB-AWS
+ valid: false
+- description: should throw an exception if username and password provided (MONGODB-AWS)
+ uri: mongodb://user:pass@localhost.com/?authMechanism=MONGODB-AWS
+ valid: false
+- description: should throw an exception if AWS_SESSION_TOKEN provided (MONGODB-AWS)
+ uri: mongodb://localhost/?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN:token
+ valid: false
+- description: should recognise the mechanism with test environment (MONGODB-OIDC)
+ uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test
+ valid: true
+ credential:
+ username:
+ password:
+ source: "$external"
+ mechanism: MONGODB-OIDC
+ mechanism_properties:
+ ENVIRONMENT: test
+- description: should recognise the mechanism when auth source is explicitly specified and with environment (MONGODB-OIDC)
+ uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external&authMechanismProperties=ENVIRONMENT:test
+ valid: true
+ credential:
+ username:
+ password:
+ source: "$external"
+ mechanism: MONGODB-OIDC
+ mechanism_properties:
+ ENVIRONMENT: test
+- description: should throw an exception if supplied a password (MONGODB-OIDC)
+ uri: mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test
+ valid: false
+ credential:
+- description: should throw an exception if username is specified for test (MONGODB-OIDC)
+ uri: mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test
+ valid: false
+ credential:
+- description: should throw an exception if specified environment is not supported (MONGODB-OIDC)
+ uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:invalid
+ valid: false
+ credential:
+- description: should throw an exception if neither environment nor callbacks specified (MONGODB-OIDC)
+ uri: mongodb://localhost/?authMechanism=MONGODB-OIDC
+ valid: false
+ credential:
+- description: should throw an exception when unsupported auth property is specified (MONGODB-OIDC)
+ uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=UnsupportedProperty:unexisted
+ valid: false
+ credential:
+- description: should recognise the mechanism with azure provider (MONGODB-OIDC)
+ uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo
+ valid: true
+ credential:
+ username: null
+ password: null
+ source: $external
+ mechanism: MONGODB-OIDC
+ mechanism_properties:
+ ENVIRONMENT: azure
+ TOKEN_RESOURCE: foo
+- description: should accept a username with azure provider (MONGODB-OIDC)
+ uri: mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo
+ valid: true
+ credential:
+ username: user
+ password: null
+ source: $external
+ mechanism: MONGODB-OIDC
+ mechanism_properties:
+ ENVIRONMENT: azure
+ TOKEN_RESOURCE: foo
+- description: should accept a url-encoded TOKEN_RESOURCE (MONGODB-OIDC)
+ uri: mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:mongodb%3A%2F%2Ftest-cluster
+ valid: true
+ credential:
+ username: user
+ password: null
+ source: $external
+ mechanism: MONGODB-OIDC
+ mechanism_properties:
+ ENVIRONMENT: azure
+ TOKEN_RESOURCE: 'mongodb://test-cluster'
+- description: should accept an un-encoded TOKEN_RESOURCE (MONGODB-OIDC)
+ uri: mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:mongodb://test-cluster
+ valid: true
+ credential:
+ username: user
+ password: null
+ source: $external
+ mechanism: MONGODB-OIDC
+ mechanism_properties:
+ ENVIRONMENT: azure
+ TOKEN_RESOURCE: 'mongodb://test-cluster'
+- description: should handle a complicated url-encoded TOKEN_RESOURCE (MONGODB-OIDC)
+ uri: mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:abcd%25ef%3Ag%26hi
+ valid: true
+ credential:
+ username: user
+ password: null
+ source: $external
+ mechanism: MONGODB-OIDC
+ mechanism_properties:
+ ENVIRONMENT: azure
+ TOKEN_RESOURCE: 'abcd%ef:g&hi'
+- description: should url-encode a TOKEN_RESOURCE (MONGODB-OIDC)
+ uri: mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:a$b
+ valid: true
+ credential:
+ username: user
+ password: null
+ source: $external
+ mechanism: MONGODB-OIDC
+ mechanism_properties:
+ ENVIRONMENT: azure
+ TOKEN_RESOURCE: a$b
+- description: should accept a username and throw an error for a password with azure provider (MONGODB-OIDC)
+ uri: mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo
+ valid: false
+ credential: null
+- description: should throw an exception if no token audience is given for azure provider (MONGODB-OIDC)
+ uri: mongodb://username@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure
+ valid: false
+ credential: null
+- description: should recognise the mechanism with gcp provider (MONGODB-OIDC)
+ uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:foo
+ valid: true
+ credential:
+ username: null
+ password: null
+ source: $external
+ mechanism: MONGODB-OIDC
+ mechanism_properties:
+ ENVIRONMENT: gcp
+ TOKEN_RESOURCE: foo
+- description: should throw an error for a username and password with gcp provider
+ (MONGODB-OIDC)
+ uri: mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:foo
+ valid: false
+ credential: null
+- description: should throw an error if not TOKEN_RESOURCE with gcp provider (MONGODB-OIDC)
+ uri: mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp
+ valid: false
+ credential: null
+- description: should recognise the mechanism with k8s provider (MONGODB-OIDC)
+ uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:k8s
+ valid: true
+ credential:
+ username: null
+ password: null
+ source: $external
+ mechanism: MONGODB-OIDC
+ mechanism_properties:
+ ENVIRONMENT: k8s
+- description: should throw an error for a username and password with k8s provider
+ (MONGODB-OIDC)
+ uri: mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:k8s
+ valid: false
+ credential: null
diff --git a/source/auth/tests/mongodb-aws.md b/source/auth/tests/mongodb-aws.md
new file mode 100644
index 0000000000..74faa3ad85
--- /dev/null
+++ b/source/auth/tests/mongodb-aws.md
@@ -0,0 +1,197 @@
+# MongoDB AWS
+
+Drivers MUST test the following scenarios:
+
+1. `Regular Credentials`: Auth via an `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` pair
+2. `EC2 Credentials`: Auth from an EC2 instance via temporary credentials assigned to the machine
+3. `ECS Credentials`: Auth from an ECS instance via temporary credentials assigned to the task
+4. `Assume Role`: Auth via temporary credentials obtained from an STS AssumeRole request
+5. `Assume Role with Web Identity`: Auth via temporary credentials obtained from an STS AssumeRoleWithWebIdentity
+ request
+6. `AWS Lambda`: Auth via environment variables `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN`.
+7. Caching of AWS credentials fetched by the driver.
+
+For brevity, this section gives the values ``, `` and `` in place of a valid access
+key ID, secret access key and session token (also known as a security token). Sample values are below.
+
+```text
+AccessKeyId=AKIAI44QH8DHBEXAMPLE
+SecretAccessKey=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
+Token=AQoDYXdzEJr...
+```
+
+## Testing custom credential providers
+
+If the driver supports custom AWS credential providers, the driver MUST test the following:
+
+### 1. Custom Credential Provider Authenticates
+
+Scenarios 1-6 from the previous section with a user provided `AWS_CREDENTIAL_PROVIDER` auth mechanism property. This
+credentials MAY be obtained from the default credential provider from the AWS SDK. If the default provider does not
+cover all scenarios above, those not covered MAY be skipped. In these tests the driver MUST also assert that the user
+provided credential provider was called in each test. This may be via a custom function or object that wraps the calls
+to the custom provider and asserts that it was called at least once. For test scenarios where the drivers tools scripts
+put the credentials in the MONGODB_URI, drivers MAY extract the credentials from the URI and return the AWS credentials
+directly from the custom provider instead of using the AWS SDK default provider.
+
+### 2. Custom Credential Provider Authentication Precedence
+
+#### Case 1: Credentials in URI Take Precedence *Removed*
+
+#### Case 2: Custom Provider Takes Precedence Over Environment Variables
+
+Run this test in an environment with AWS credentials configured as environment variables (e.g. `AWS_ACCESS_KEY_ID`,
+`AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN`)
+
+Create a `MongoClient` configured to use AWS auth. Example: `mongodb://localhost:27017/?authMechanism=MONGODB-AWS`.
+
+Configure a custom credential provider to pass valid AWS credentials. The provider must track if it was called.
+
+Expect authentication to succeed and the custom credential provider was called.
+
+## Regular credentials
+
+Drivers MUST be able to authenticate when a valid access key id and secret access key pair are present in the
+environment. Drivers MUST provide the --nouri option to aws_tester.py in drivers-evergreen-tools for this test.
+
+```text
+mongodb://localhost/?authMechanism=MONGODB-AWS
+```
+
+## EC2 Credentials
+
+Drivers MUST be able to authenticate from an EC2 instance via temporary credentials assigned to the machine. A sample
+URI on an EC2 machine would be:
+
+```text
+mongodb://localhost/?authMechanism=MONGODB-AWS
+```
+
+> [!NOTE]
+> No username, password or session token is passed into the URI. Drivers MUST query the EC2 instance endpoint to obtain
+> these credentials.
+
+## ECS instance
+
+Drivers MUST be able to authenticate from an ECS container via temporary credentials. A sample URI in an ECS container
+would be:
+
+```text
+mongodb://localhost/?authMechanism=MONGODB-AWS
+```
+
+> [!NOTE]
+> No username, password or session token is passed into the URI. Drivers MUST query the ECS container endpoint to obtain
+> these credentials.
+
+## AssumeRole
+
+Drivers MUST be able to authenticate using temporary credentials returned from an assume role request. These temporary
+credentials consist of an access key ID, a secret access key, and a security token present in the environment. Drivers
+MUST provide the --nouri option to aws_tester.py in drivers-evergreen-tools for this test. A sample URI would be:
+
+```text
+mongodb://localhost/?authMechanism=MONGODB-AWS
+```
+
+## Assume Role with Web Identity
+
+Drivers MUST be able to authentiate using a valid OIDC token and associated role ARN taken from environment variables,
+respectively:
+
+```text
+AWS_WEB_IDENTITY_TOKEN_FILE
+AWS_ROLE_ARN
+AWS_ROLE_SESSION_NAME (optional)
+```
+
+A sample URI in for a web identity test would be:
+
+```text
+mongodb://localhost/?authMechanism=MONGODB-AWS
+```
+
+Drivers MUST test with and without AWS_ROLE_SESSION_NAME set.
+
+> [!NOTE]
+> No username, password or session token is passed into the URI.
+
+Drivers MUST check the environment variables listed above and make an
+[AssumeRoleWithWebIdentity request](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html)
+to obtain credentials.
+
+## AWS Lambda
+
+Drivers MUST be able to authenticate via an access key ID, secret access key and optional session token taken from the
+environment variables, respectively:
+
+```text
+AWS_ACCESS_KEY_ID
+AWS_SECRET_ACCESS_KEY
+AWS_SESSION_TOKEN
+```
+
+Sample URIs both with and without optional session tokens set are shown below. Drivers MUST test both cases.
+
+```bash
+# without a session token
+export AWS_ACCESS_KEY_ID=""
+export AWS_SECRET_ACCESS_KEY=""
+
+URI="mongodb://localhost/?authMechanism=MONGODB-AWS"
+```
+
+```bash
+# with a session token
+export AWS_ACCESS_KEY_ID=""
+export AWS_SECRET_ACCESS_KEY=""
+export AWS_SESSION_TOKEN=""
+
+URI="mongodb://localhost/?authMechanism=MONGODB-AWS"
+```
+
+> [!NOTE]
+> No username, password or session token is passed into the URI. Drivers MUST check the environment variables listed
+> above for these values. If the session token is set Drivers MUST use it.
+
+## Cached Credentials
+
+Drivers MUST ensure that they are testing the ability to cache credentials. Drivers will need to be able to query and
+override the cached credentials to verify usage. To determine whether to run the cache tests, the driver can check for
+the absence of the AWS_ACCESS_KEY_ID environment variable and of credentials in the URI.
+
+1. Clear the cache.
+2. Create a new client.
+3. Ensure that a `find` operation adds credentials to the cache.
+4. Override the cached credentials with an "Expiration" that is within one minute of the current UTC time.
+5. Create a new client.
+6. Ensure that a `find` operation updates the credentials in the cache.
+7. Poison the cache with an invalid access key id.
+8. Create a new client.
+9. Ensure that a `find` operation results in an error.
+10. Ensure that the cache has been cleared.
+11. Ensure that a subsequent `find` operation succeeds.
+12. Ensure that the cache has been set.
+
+If the drivers's language supports dynamically setting environment variables, add the following tests. Note that if
+integration tests are run in parallel for the driver, then these tests must be run as unit tests interacting with the
+auth provider directly instead of using a client.
+
+1. Clear the cache.
+2. Create a new client.
+3. Ensure that a `find` operation adds credentials to the cache.
+4. Set the AWS environment variables based on the cached credentials.
+5. Clear the cache.
+6. Create a new client.
+7. Ensure that a `find` operation succeeds and does not add credentials to the cache.
+8. Set the AWS environment variables to invalid values.
+9. Create a new client.
+10. Ensure that a `find` operation results in an error.
+11. Clear the AWS environment variables.
+12. Clear the cache.
+13. Create a new client.
+14. Ensure that a `find` operation adds credentials to the cache.
+15. Set the AWS environment variables to invalid values.
+16. Create a new client.
+17. Ensure that a `find` operation succeeds.
+18. Clear the AWS environment variables.
diff --git a/source/auth/tests/mongodb-oidc.md b/source/auth/tests/mongodb-oidc.md
new file mode 100644
index 0000000000..affb3e6efa
--- /dev/null
+++ b/source/auth/tests/mongodb-oidc.md
@@ -0,0 +1,593 @@
+# MongoDB OIDC
+
+## Local Testing
+
+See the detailed instructions in
+[drivers-evergreen-tools](https://github.com/mongodb-labs/drivers-evergreen-tools/blob/master/.evergreen/auth_oidc/README.md)
+for how to set up your environment for OIDC testing.
+
+______________________________________________________________________
+
+## Unified Spec Tests
+
+Drivers MUST run the unified spec tests in all supported OIDC environments. Drivers MUST set the placeholder
+authMechanism properties (`ENVIRONMENT` and `TOKEN_RESOURCE`, if applicable). These will typically be read from
+environment variables set by the test runner, e,g. `AZUREOIDC_RESOURCE`.
+
+______________________________________________________________________
+
+## Machine Authentication Flow Prose Tests
+
+Drivers MUST run these tests in all supported OIDC environments:
+
+- A callback that reads the token file for `ENVIRONMENT:test`. A callback enables testing additional behaviors. Tests
+ and assertions limited to a callback are noted with `[callback-only]`.
+- `ENVIRONMENT:test`
+- `ENVIRONMENT:gcp`
+- `ENVIRONMENT:azure`
+- `ENVIRONMENT:k8s`
+
+The token file `ENVIRONMENT:test` is located in `OIDC_TOKEN_DIR` set by
+[drivers-evergreen-tools](https://github.com/mongodb-labs/drivers-evergreen-tools/blob/master/.evergreen/auth_oidc/README.md)
+scripts.
+
+Drivers MUST implement all prose tests in this section. Unless otherwise noted, all `MongoClient` instances MUST be
+configured with `retryReads=false`.
+
+> [!NOTE]
+> For test cases that create fail points, drivers MUST use a unique `appName` to prevent interaction with other
+> environment processes, and explicitly remove the fail point to prevent interaction between test runs.
+
+After setting up your OIDC
+[environment](https://github.com/mongodb-labs/drivers-evergreen-tools/blob/master/.evergreen/auth_oidc/README.md),
+source the `secrets-export.sh` file and use the associated env variables in your tests.
+
+### Callback Authentication
+
+**1.1 Callback is called during authentication**
+
+- Create an OIDC configured client.
+- Perform a `find` operation that succeeds.
+- `[callback-only]` Assert that the callback was called 1 time.
+- Close the client.
+
+**1.2 Callback is called once for multiple connections**
+
+- Create an OIDC configured client.
+- Start 10 threads and run 100 `find` operations in each thread that all succeed.
+- `[callback-only]` Assert that the callback was called 1 time.
+- Close the client.
+
+### (2) `[callback-only]` OIDC Callback Validation
+
+**2.1 Valid Callback Inputs**
+
+- Create an OIDC configured client with an OIDC callback that validates its inputs and returns a valid access token.
+- Perform a `find` operation that succeeds.
+- Assert that the OIDC callback was called with the appropriate inputs, including the timeout parameter if possible.
+- Close the client.
+
+**2.2 OIDC Callback Returns Null**
+
+- Create an OIDC configured client with an OIDC callback that returns `null`.
+- Perform a `find` operation that fails.
+- Close the client.
+
+**2.3 OIDC Callback Returns Missing Data**
+
+- Create an OIDC configured client with an OIDC callback that returns data not conforming to the `OIDCCredential` with
+ missing fields.
+- Perform a `find` operation that fails.
+- Close the client.
+
+**2.4 Invalid Client Configuration with Callback**
+
+- Create an OIDC configured client with an OIDC callback and auth mechanism property `ENVIRONMENT:test`.
+- Assert it returns a client configuration error upon client creation, or client connect if your driver validates on
+ connection.
+
+**2.5 Invalid use of ALLOWED_HOSTS**
+
+- Create an OIDC configured client with auth mechanism properties `{"ENVIRONMENT": "azure", "ALLOWED_HOSTS": []}`.
+- Assert it returns a client configuration error upon client creation, or client connect if your driver validates on
+ connection.
+
+### (3) Authentication Failure
+
+**3.1 Authentication failure with cached tokens fetch a new token and retry auth**
+
+- Create an OIDC configured client.
+- Poison the *Client Cache* with an invalid access token.
+- Perform a `find` operation that succeeds.
+- `[callback-only]` Assert that the callback was called 1 time.
+- Close the client.
+
+**3.2 `[callback-only]` Authentication failures without cached tokens return an error**
+
+- Create an OIDC configured client with an OIDC callback that always returns invalid access tokens.
+- Perform a `find` operation that fails.
+- Assert that the callback was called 1 time.
+- Close the client.
+
+**3.3 Unexpected error code does not clear the cache**
+
+- Create an OIDC configured client.
+- Set a fail point for `saslStart` commands of the form:
+
+```javascript
+{
+ configureFailPoint: "failCommand",
+ mode: {
+ times: 1
+ },
+ data: {
+ failCommands: [
+ "saslStart"
+ ],
+ errorCode: 20 // IllegalOperation
+ }
+}
+```
+
+- Perform a `find` operation that fails.
+- `[callback-only]` Assert that the callback has been called once.
+- Perform a `find` operation that succeeds.
+- `[callback-only]` Assert that the callback has been called once.
+- Close the client.
+
+### (4) Reauthentication
+
+#### 4.1 Reauthentication Succeeds
+
+- Create an OIDC configured client.
+- Set a fail point for `find` commands of the form:
+
+```javascript
+{
+ configureFailPoint: "failCommand",
+ mode: {
+ times: 1
+ },
+ data: {
+ failCommands: [
+ "find"
+ ],
+ errorCode: 391 // ReauthenticationRequired
+ }
+}
+```
+
+- Perform a `find` operation that succeeds.
+- `[callback-only]` Assert that the callback was called 2 times (once during the connection handshake, and again during
+ reauthentication).
+- Close the client.
+
+#### `[callback-only]` 4.2 Read Commands Fail If Reauthentication Fails
+
+- Create a `MongoClient` whose OIDC callback returns one good token and then bad tokens after the first call.
+- Perform a `find` operation that succeeds.
+- Set a fail point for `find` commands of the form:
+
+```javascript
+{
+ configureFailPoint: "failCommand",
+ mode: {
+ times: 1
+ },
+ data: {
+ failCommands: [
+ "find"
+ ],
+ errorCode: 391 // ReauthenticationRequired
+ }
+}
+```
+
+- Perform a `find` operation that fails.
+- Assert that the callback was called 2 times.
+- Close the client.
+
+#### `[callback-only]` 4.3 Write Commands Fail If Reauthentication Fails
+
+- Create a `MongoClient` whose OIDC callback returns one good token and then bad tokens after the first call.
+- Perform an `insert` operation that succeeds.
+- Set a fail point for `insert` commands of the form:
+
+```javascript
+{
+ configureFailPoint: "failCommand",
+ mode: {
+ times: 1
+ },
+ data: {
+ failCommands: [
+ "insert"
+ ],
+ errorCode: 391 // ReauthenticationRequired
+ }
+}
+```
+
+- Perform a `insert` operation that fails.
+- Assert that the callback was called 2 times.
+- Close the client.
+
+#### 4.4 Speculative Authentication should be ignored on Reauthentication
+
+- Create an OIDC configured client.
+- Populate the *Client Cache* with a valid access token to enforce Speculative Authentication.
+ - This may be done by authenticating a temporary OIDC configured client and copying the cached token.
+- Perform an `insert` operation that succeeds.
+- Assert that the callback was not called.
+- Assert there were no `saslStart` commands executed.
+- Set a fail point for `insert` commands of the form:
+
+```javascript
+{
+ configureFailPoint: "failCommand",
+ mode: {
+ times: 1
+ },
+ data: {
+ failCommands: [
+ "insert"
+ ],
+ errorCode: 391 // ReauthenticationRequired
+ }
+}
+```
+
+- Perform an `insert` operation that succeeds.
+- `[callback-only]` Assert that the callback was called once.
+- Assert there were `saslStart` commands executed.
+- Close the client.
+
+#### 4.5 Reauthentication Succeeds when a Session is involved
+
+- Create an OIDC configured client.
+- Set a fail point for `find` commands of the form:
+
+```javascript
+{
+ configureFailPoint: "failCommand",
+ mode: {
+ times: 1
+ },
+ data: {
+ failCommands: [
+ "find"
+ ],
+ errorCode: 391 // ReauthenticationRequired
+ }
+}
+```
+
+- Start a new session.
+- In the started session perform a `find` operation that succeeds.
+- `[callback-only]` Assert that the callback was called 2 times (once during the connection handshake, and again during
+ reauthentication).
+- Close the session and the client.
+
+## (5) Azure Tests
+
+Drivers MUST only run the Azure tests when testing on an Azure VM. See instructions in
+[Drivers Evergreen Tools](https://github.com/mongodb-labs/drivers-evergreen-tools/blob/master/.evergreen/auth_oidc/azure/README.md)
+for test setup.
+
+# 5.1 Azure With No Username
+
+- Create an OIDC configured client with `ENVIRONMENT:azure` and a valid `TOKEN_RESOURCE` and no username.
+- Perform a `find` operation that succeeds.
+- Close the client.
+
+# 5.2 Azure with Bad Username
+
+- Create an OIDC configured client with `ENVIRONMENT:azure` and a valid `TOKEN_RESOURCE` and a username of `"bad"`.
+- Perform a `find` operation that fails.
+- Close the client.
+
+______________________________________________________________________
+
+## Human Authentication Flow Prose Tests
+
+Drivers that support the [Human Authentication Flow](../auth.md#human-authentication-flow) MUST implement all prose
+tests in this section. Unless otherwise noted, all `MongoClient` instances MUST be configured with `retryReads=false`.
+
+The human workflow tests MUST only be run when `OIDC_TOKEN_DIR` is set.
+
+> [!NOTE]
+> For test cases that create fail points, drivers MUST either use a unique `appName` or explicitly remove the fail point
+> after the test to prevent interaction between test cases.
+
+Drivers MUST be able to authenticate against a server configured with either one or two configured identity providers.
+
+Unless otherwise specified, use `MONGODB_URI_SINGLE` and the `test_user1` token in the `OIDC_TOKEN_DIR` as the
+"access_token", and a dummy "refresh_token" for all tests.
+
+When using an explicit username for the client, we use the token name and the domain name given by `OIDC_DOMAIN`, e.g.
+`test_user1@${OIDC_DOMAIN}`.
+
+### (1) OIDC Human Callback Authentication
+
+Drivers MUST be able to authenticate using OIDC callback(s) when there is one principal configured.
+
+**1.1 Single Principal Implicit Username**
+
+- Create an OIDC configured client.
+- Perform a `find` operation that succeeds.
+- Close the client.
+
+**1.2 Single Principal Explicit Username**
+
+- Create an OIDC configured client with `MONGODB_URI_SINGLE` and a username of `test_user1@${OIDC_DOMAIN}`.
+- Perform a `find` operation that succeeds.
+- Close the client.
+
+**1.3 Multiple Principal User 1**
+
+- Create an OIDC configured client with `MONGODB_URI_MULTI` and username of `test_user1@${OIDC_DOMAIN}`.
+- Perform a `find` operation that succeeds.
+- Close the client.
+
+**1.4 Multiple Principal User 2**
+
+- Create an OIDC configured client with `MONGODB_URI_MULTI` and username of `test_user2@${OIDC_DOMAIN}`. that reads the
+ `test_user2` token file.
+- Perform a `find` operation that succeeds.
+- Close the client.
+
+**1.5 Multiple Principal No User**
+
+- Create an OIDC configured client with `MONGODB_URI_MULTI` and no username.
+- Assert that a `find` operation fails.
+- Close the client.
+
+**1.6 Allowed Hosts Blocked**
+
+- Create an OIDC configured client with an `ALLOWED_HOSTS` that is an empty list.
+- Assert that a `find` operation fails with a client-side error.
+- Close the client.
+- Create a client that uses the URL `mongodb://localhost/?authMechanism=MONGODB-OIDC&ignored=example.com`, a human
+ callback, and an `ALLOWED_HOSTS` that contains `["example.com"]`.
+- Assert that a `find` operation fails with a client-side error.
+- Close the client.
+
+**1.7 Allowed Hosts in Connection String Ignored**
+
+- Create an OIDC configured client with the connection string:
+ `mongodb+srv://example.com/?authMechanism=MONGODB-OIDC&authMechanismProperties=ALLOWED_HOSTS:%5B%22example.com%22%5D`
+ and a Human Callback.
+- Assert that the creation of the client raises a configuration error.
+
+**1.8 Machine IdP with Human Callback**
+
+This test MUST only be run when `OIDC_IS_LOCAL` is set. This indicates that the server is local and not using Atlas. In
+this case, `MONGODB_URI_SINGLE` will be configured with a human user `test_user1`, and a machine user `test_machine`.
+This test uses the machine user with a human callback, ensuring that the missing `clientId` in the
+`PrincipalStepRequest` response is handled by the driver.
+
+- Create an OIDC configured client with `MONGODB_URI_SINGLE` and a username of `test_machine` that uses the
+ `test_machine` token.
+- Perform a find operation that succeeds.
+- Close the client.
+
+### (2) OIDC Human Callback Validation
+
+**2.1 Valid Callback Inputs**
+
+- Create an OIDC configured client with a human callback that validates its inputs and returns a valid access token.
+- Perform a `find` operation that succeeds. Verify that the human callback was called with the appropriate inputs,
+ including the timeout parameter if possible.
+- Close the client.
+
+**2.2 Human Callback Returns Missing Data**
+
+- Create an OIDC configured client with a human callback that returns data not conforming to the `OIDCCredential` with
+ missing fields.
+- Perform a `find` operation that fails.
+- Close the client.
+
+**2.3 Refresh Token Is Passed To The Callback**
+
+- Create a `MongoClient` with a human callback that checks for the presence of a refresh token.
+- Perform a find operation that succeeds.
+- Set a fail point for `find` commands of the form:
+
+```javascript
+{
+ configureFailPoint: "failCommand",
+ mode: {
+ times: 1
+ },
+ data: {
+ failCommands: [
+ "find"
+ ],
+ errorCode: 391
+ }
+}
+```
+
+- Perform a `find` operation that succeeds.
+- Assert that the callback has been called twice.
+- Assert that the refresh token was provided to the callback once.
+
+### (3) Speculative Authentication
+
+**3.1 Uses speculative authentication if there is a cached token**
+
+- Create an OIDC configured client with a human callback that returns a valid token.
+- Set a fail point for `find` commands of the form:
+
+```javascript
+{
+ configureFailPoint: "failCommand",
+ mode: {
+ times: 1
+ },
+ data: {
+ failCommands: [
+ "find"
+ ],
+ closeConnection: true
+ }
+}
+```
+
+- Perform a `find` operation that fails.
+- Set a fail point for `saslStart` commands of the form:
+
+```javascript
+{
+ configureFailPoint: "failCommand",
+ mode: {
+ times: 1
+ },
+ data: {
+ failCommands: [
+ "saslStart"
+ ],
+ errorCode: 18
+ }
+}
+```
+
+- Perform a `find` operation that succeeds.
+- Close the client.
+
+**3.2 Does not use speculative authentication if there is no cached token**
+
+- Create an OIDC configured client with a human callback that returns a valid token.
+- Set a fail point for `saslStart` commands of the form:
+
+```javascript
+{
+ configureFailPoint: "failCommand",
+ mode: {
+ times: 1
+ },
+ data: {
+ failCommands: [
+ "saslStart"
+ ],
+ errorCode: 18
+ }
+}
+```
+
+- Perform a `find` operation that fails.
+- Close the client.
+
+### (4) Reauthentication
+
+**4.1 Succeeds**
+
+- Create an OIDC configured client and add an event listener. The following assumes that the driver does not emit
+ `saslStart` or `saslContinue` events. If the driver does emit those events, ignore/filter them for the purposes of
+ this test.
+- Perform a `find` operation that succeeds.
+- Assert that the human callback has been called once.
+- Clear the listener state if possible.
+- Force a reauthenication using a fail point of the form:
+
+```javascript
+{
+ configureFailPoint: "failCommand",
+ mode: {
+ times: 1
+ },
+ data: {
+ failCommands: [
+ "find"
+ ],
+ errorCode: 391 // ReauthenticationRequired
+ }
+}
+```
+
+- Perform another find operation that succeeds.
+- Assert that the human callback has been called twice.
+- Assert that the ordering of list started events is \[`find`\], , `find`. Note that if the listener stat could not be
+ cleared then there will and be extra `find` command.
+- Assert that the list of command succeeded events is \[`find`\].
+- Assert that a `find` operation failed once during the command execution.
+- Close the client.
+
+**4.2 Succeeds no refresh**
+
+- Create an OIDC configured client with a human callback that does not return a refresh token.
+- Perform a `find` operation that succeeds.
+- Assert that the human callback has been called once.
+- Force a reauthenication using a fail point of the form:
+
+```javascript
+{
+ configureFailPoint: "failCommand",
+ mode: {
+ times: 1
+ },
+ data: {
+ failCommands: [
+ "find"
+ ],
+ errorCode: 391 // ReauthenticationRequired
+ }
+}
+```
+
+- Perform a `find` operation that succeeds.
+- Assert that the human callback has been called twice.
+- Close the client.
+
+**4.3 Succeeds after refresh fails**
+
+- Create an OIDC configured client with a callback that returns the `test_user1` access token and a bad refresh token.
+- Perform a `find` operation that succeeds.
+- Assert that the human callback has been called once.
+- Force a reauthenication using a fail point of the form:
+
+```javascript
+{
+ configureFailPoint: "failCommand",
+ mode: {
+ times: 1
+ },
+ data: {
+ failCommands: [
+ "find",
+ ],
+ errorCode: 391 // ReauthenticationRequired
+ }
+}
+```
+
+- Perform a `find` operation that succeeds.
+- Assert that the human callback has been called 2 times.
+- Close the client.
+
+**4.4 Fails**
+
+- Create an OIDC configured client that returns invalid refresh tokens and returns invalid access tokens after the first
+ access.
+- Perform a find operation that succeeds (to force a speculative auth).
+- Assert that the human callback has been called once.
+- Force a reauthenication using a failCommand of the form:
+
+```javascript
+{
+ configureFailPoint: "failCommand",
+ mode: {
+ times: 1
+ },
+ data: {
+ failCommands: [
+ "find",
+ ],
+ errorCode: 391 // ReauthenticationRequired
+ }
+}
+```
+
+- Perform a find operation that fails.
+- Assert that the human callback has been called three times.
+- Close the client.
diff --git a/source/auth/tests/unified/mongodb-oidc-no-retry.json b/source/auth/tests/unified/mongodb-oidc-no-retry.json
new file mode 100644
index 0000000000..b32ada172a
--- /dev/null
+++ b/source/auth/tests/unified/mongodb-oidc-no-retry.json
@@ -0,0 +1,428 @@
+{
+ "description": "MONGODB-OIDC authentication with retry disabled",
+ "schemaVersion": "1.19",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "7.0",
+ "auth": true,
+ "authMechanism": "MONGODB-OIDC",
+ "serverless": "forbid"
+ }
+ ],
+ "createEntities": [
+ {
+ "client": {
+ "id": "failPointClient",
+ "useMultipleMongoses": false
+ }
+ },
+ {
+ "client": {
+ "id": "client0",
+ "uriOptions": {
+ "authMechanism": "MONGODB-OIDC",
+ "authMechanismProperties": {
+ "$$placeholder": 1
+ },
+ "retryReads": false,
+ "retryWrites": false,
+ "appName": "mongodb-oidc-no-retry"
+ },
+ "observeEvents": [
+ "commandStartedEvent",
+ "commandSucceededEvent",
+ "commandFailedEvent"
+ ]
+ }
+ },
+ {
+ "database": {
+ "id": "database0",
+ "client": "client0",
+ "databaseName": "test"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection0",
+ "database": "database0",
+ "collectionName": "collName"
+ }
+ }
+ ],
+ "initialData": [
+ {
+ "collectionName": "collName",
+ "databaseName": "test",
+ "documents": []
+ }
+ ],
+ "tests": [
+ {
+ "description": "A read operation should succeed",
+ "operations": [
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "filter": {}
+ },
+ "expectResult": []
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "find": "collName",
+ "filter": {}
+ }
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "find"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "A write operation should succeed",
+ "operations": [
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "x": 1
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "insert": "collName",
+ "documents": [
+ {
+ "_id": 1,
+ "x": 1
+ }
+ ]
+ }
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "insert"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Read commands should reauthenticate and retry when a ReauthenticationRequired error happens",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "failPointClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "find"
+ ],
+ "errorCode": 391,
+ "appName": "mongodb-oidc-no-retry"
+ }
+ }
+ }
+ },
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "filter": {}
+ },
+ "expectResult": []
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "find": "collName",
+ "filter": {}
+ }
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "find": "collName",
+ "filter": {}
+ }
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "find"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Write commands should reauthenticate and retry when a ReauthenticationRequired error happens",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "failPointClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "insert"
+ ],
+ "errorCode": 391,
+ "appName": "mongodb-oidc-no-retry"
+ }
+ }
+ }
+ },
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "x": 1
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "insert": "collName",
+ "documents": [
+ {
+ "_id": 1,
+ "x": 1
+ }
+ ]
+ }
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "insert": "collName",
+ "documents": [
+ {
+ "_id": 1,
+ "x": 1
+ }
+ ]
+ }
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "insert"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Handshake with cached token should use speculative authentication",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "failPointClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "insert"
+ ],
+ "closeConnection": true,
+ "appName": "mongodb-oidc-no-retry"
+ }
+ }
+ }
+ },
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "x": 1
+ }
+ },
+ "expectError": {
+ "isClientError": true
+ }
+ },
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "failPointClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "saslStart"
+ ],
+ "errorCode": 18,
+ "appName": "mongodb-oidc-no-retry"
+ }
+ }
+ }
+ },
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "x": 1
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "insert": "collName",
+ "documents": [
+ {
+ "_id": 1,
+ "x": 1
+ }
+ ]
+ }
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "insert": "collName",
+ "documents": [
+ {
+ "_id": 1,
+ "x": 1
+ }
+ ]
+ }
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "insert"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Handshake without cached token should not use speculative authentication",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "failPointClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "saslStart"
+ ],
+ "errorCode": 18,
+ "appName": "mongodb-oidc-no-retry"
+ }
+ }
+ }
+ },
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "x": 1
+ }
+ },
+ "expectError": {
+ "errorCode": 18
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/source/auth/tests/unified/mongodb-oidc-no-retry.yml b/source/auth/tests/unified/mongodb-oidc-no-retry.yml
new file mode 100644
index 0000000000..dba2ae8d1d
--- /dev/null
+++ b/source/auth/tests/unified/mongodb-oidc-no-retry.yml
@@ -0,0 +1,235 @@
+---
+description: "MONGODB-OIDC authentication with retry disabled"
+schemaVersion: "1.19"
+runOnRequirements:
+- minServerVersion: "7.0"
+ auth: true
+ authMechanism: "MONGODB-OIDC"
+ serverless: forbid
+createEntities:
+- client:
+ id: &failPointClient failPointClient
+ useMultipleMongoses: false
+- client:
+ id: client0
+ uriOptions:
+ authMechanism: "MONGODB-OIDC"
+ # The $$placeholder document should be replaced by auth mechanism
+ # properties that enable OIDC auth on the target cloud platform. For
+ # example, when running the test on EC2, replace the $$placeholder
+ # document with {"ENVIRONMENT": "test"}.
+ authMechanismProperties: { $$placeholder: 1 }
+ retryReads: false
+ retryWrites: false
+ appName: &appName mongodb-oidc-no-retry
+ observeEvents:
+ - commandStartedEvent
+ - commandSucceededEvent
+ - commandFailedEvent
+- database:
+ id: database0
+ client: client0
+ databaseName: test
+- collection:
+ id: collection0
+ database: database0
+ collectionName: collName
+initialData:
+- collectionName: collName
+ databaseName: test
+ documents: []
+tests:
+- description: A read operation should succeed
+ operations:
+ - name: find
+ object: collection0
+ arguments:
+ filter: {}
+ expectResult: []
+ expectEvents:
+ - client: client0
+ events:
+ - commandStartedEvent:
+ command:
+ find: collName
+ filter: {}
+ - commandSucceededEvent:
+ commandName: find
+- description: A write operation should succeed
+ operations:
+ - name: insertOne
+ object: collection0
+ arguments:
+ document:
+ _id: 1
+ x: 1
+ expectEvents:
+ - client: client0
+ events:
+ - commandStartedEvent:
+ command:
+ insert: collName
+ documents:
+ - _id: 1
+ x: 1
+ - commandSucceededEvent:
+ commandName: insert
+- description: Read commands should reauthenticate and retry when a ReauthenticationRequired error happens
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: failPointClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode:
+ times: 1
+ data:
+ failCommands:
+ - find
+ errorCode: 391 # ReauthenticationRequired
+ appName: *appName
+ - name: find
+ object: collection0
+ arguments:
+ filter: {}
+ expectResult: []
+ expectEvents:
+ - client: client0
+ events:
+ - commandStartedEvent:
+ command:
+ find: collName
+ filter: {}
+ - commandFailedEvent:
+ commandName: find
+ - commandStartedEvent:
+ command:
+ find: collName
+ filter: {}
+ - commandSucceededEvent:
+ commandName: find
+- description: Write commands should reauthenticate and retry when a ReauthenticationRequired error happens
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: failPointClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode:
+ times: 1
+ data:
+ failCommands:
+ - insert
+ errorCode: 391 # ReauthenticationRequired
+ appName: *appName
+ - name: insertOne
+ object: collection0
+ arguments:
+ document:
+ _id: 1
+ x: 1
+ expectEvents:
+ - client: client0
+ events:
+ - commandStartedEvent:
+ command:
+ insert: collName
+ documents:
+ - _id: 1
+ x: 1
+ - commandFailedEvent:
+ commandName: insert
+ - commandStartedEvent:
+ command:
+ insert: collName
+ documents:
+ - _id: 1
+ x: 1
+ - commandSucceededEvent:
+ commandName: insert
+- description: Handshake with cached token should use speculative authentication
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: failPointClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode:
+ times: 1
+ data:
+ failCommands:
+ - insert
+ closeConnection: true
+ appName: *appName
+ - name: insertOne
+ object: collection0
+ arguments:
+ document:
+ _id: 1
+ x: 1
+ expectError:
+ isClientError: true
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: failPointClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode:
+ times: 1
+ data:
+ failCommands:
+ - saslStart
+ errorCode: 18
+ appName: *appName
+ - name: insertOne
+ object: collection0
+ arguments:
+ document:
+ _id: 1
+ x: 1
+ expectEvents:
+ - client: client0
+ events:
+ - commandStartedEvent:
+ command:
+ insert: collName
+ documents:
+ - _id: 1
+ x: 1
+ - commandFailedEvent:
+ commandName: insert
+ - commandStartedEvent:
+ command:
+ insert: collName
+ documents:
+ - _id: 1
+ x: 1
+ - commandSucceededEvent:
+ commandName: insert
+- description: Handshake without cached token should not use speculative authentication
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: failPointClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode:
+ times: 1
+ data:
+ failCommands:
+ - saslStart
+ errorCode: 18
+ appName: *appName
+ - name: insertOne
+ object: collection0
+ arguments:
+ document:
+ _id: 1
+ x: 1
+ expectError:
+ errorCode: 18
diff --git a/source/benchmarking/benchmarking.md b/source/benchmarking/benchmarking.md
new file mode 100644
index 0000000000..8a50644ed1
--- /dev/null
+++ b/source/benchmarking/benchmarking.md
@@ -0,0 +1,772 @@
+# Performance Benchmarking
+
+- Status: Accepted
+- Minimum Server Version: N/A
+
+## Abstract
+
+This document describes a standard benchmarking suite for MongoDB drivers.
+
+## Overview
+
+### Name and purpose
+
+Driver performance will be measured by the MongoDB Driver Performance Benchmark (AKA "DriverBench"). It will provide
+both "horizontal" insights into how individual language driver performance evolves over time and "vertical" insights
+into relative performance of different drivers.
+
+We do expect substantial performance differences between language families (e.g. static vs. dynamic or compiled vs.
+virtual-machine-based). However we still expect "vertical" comparison within language families to expose outlier
+behavior that might be amenable to optimization.
+
+### Task Hierarchy
+
+The benchmark consists of a number of micro-benchmarks tasks arranged into groups of increasing complexity. This allows
+us to better isolate areas within drivers that are faster or slower.
+
+- BSON -- BSON encoding/decoding tasks, to explore BSON codec efficiency
+- Single-Doc -- single-document insertion and query tasks, to explore basic wire protocol efficiency
+- Multi-Doc -- multi-document insertion and query tasks, to explore batch-write and cursor chunking efficiency
+- Parallel -- multi-process/thread ETL tasks, to explore concurrent operation efficiency
+
+### Measurement
+
+In addition to timing data, all micro-benchmark tasks will be measured in terms of "megabytes/second" (MB/s) of
+documents processed, with higher scores being better. (In this document, "megabyte" refers to the SI decimal unit, i.e.
+1,000,000 bytes.) This makes cross-benchmark comparisons easier.
+
+To avoid various types of measurement skew, tasks will be measured over numerous iterations. Each iteration will have a
+"scale" -- the number of similar operations performed -- that will vary by task. The final score for a task will be the
+median score of the iterations. Other quantiles will be recorded for diagnostic analysis.
+
+### Data sets
+
+Data sets will vary by micro-benchmark. In some cases, they it will be a synthetically generated document inserted
+repeatedly (with different `_id` fields) to construct an overall corpus of documents. In other cases, data sets will be
+synthetic line-delimited JSON files or mock binary files.
+
+### Composite scores
+
+Micro-benchmark scores will be combined into a composite for each weight class ("BSONBench", "SingleBench", etc.) and
+for read and write operations ("ReadBench" and "WriteBench"). The read and write scores will be combined into an
+aggregate composite score ("DriverBench"). The compositing formula in the DriverBench uses simple averages with equal
+weighting.
+
+### Versioning
+
+DriverBench will have vX.Y versioning. Minor updates and clarifications will increment "Y" and should have little impact
+on score comparison. Major changes, such as changing score weights, MongoDB version tested against, or hardware used,
+will increment "X" to indicate that older version scores are unlikely to be comparable.
+
+## Benchmark execution phases and measurement
+
+All micro-benchmark tasks will be conducted via a number of iterations. Each iteration will be timed and will generally
+include a large number of individual driver operations.
+
+We break up the measurement this way to better isolate the benchmark from external volatility. If we consider the
+problem of benchmarking an operation over many iterations, such as 100,000 document insertions, we want to avoid two
+extreme forms of measurement:
+
+- measuring a single insertion 100,000 times -- in this case, the timing code is likely to be a greater proportion of
+ executed code, which could routinely evict the insertion code from CPU caches or mislead a JIT optimizer and throw
+ off results
+- measuring 100,000 insertions one time -- in this case, the longer the timer runs, the higher the likelihood that an
+ external event occurs that affects the time of the run
+
+Therefore, we choose a middle ground:
+
+- measuring the same 1000 insertions over 100 iterations -- each timing run includes enough operations that insertion
+ code dominates timing code; unusual system events are likely to affect only a fraction of the 100 timing
+ measurements
+
+With 100 timings of inserting the same 1000 documents, we build up a statistical distribution of the operation timing,
+allowing a more robust estimate of performance than a single measurement. (In practice, the number of iterations could
+exceed 100, but 100 is a reasonable minimum goal.)
+
+Because a timing distribution is bounded by zero on one side, taking the mean would allow large positive outlier
+measurements to skew the result substantially. Therefore, for the benchmark score, we use the median timing measurement,
+which is robust in the face of outliers.
+
+Each benchmark is structured into discrete setup/execute/teardown phases. Phases are as follows, with specific details
+given in a subsequent section:
+
+- setup -- (ONCE PER MICRO-BENCHMARK) something to do once before any benchmarking, e.g. construct a client object, load
+ test data, insert data into a collection, etc.
+- before task -- (ONCE PER ITERATION) something to do before every task iteration, e.g. drop a collection, or reload
+ test data (if the test run modifies it), etc.
+- do task -- (ONCE PER ITERATION) smallest amount of code necessary to execute the task; e.g. insert 1000 documents one
+ by one into the database, or retrieve 1000 document of test data from the database, etc.
+- after task -- (ONCE PER ITERATION) something to do after every task iteration (if necessary)
+- teardown -- (ONCE PER MICRO-BENCHMARK) something done once after all benchmarking is complete (if necessary); e.g.
+ drop the test database
+
+The wall-clock execution time of each "do task" phase will be recorded. We use wall clock time to model user experience
+and as a lowest-common denominator across languages and threading models. Iteration timing should be done with a
+high-resolution monotonic timer (or best language approximation).
+
+Unless otherwise specified, the number of iterations to measure per micro-benchmark is variable:
+
+- iterations should loop for at least 1 minute cumulative execution time
+- iterations should stop after 100 iterations or 5 minutes cumulative execution time, whichever is shorter
+
+This balances measurement stability with a timing cap to ensure all micro-benchmarks can complete in a reasonable time.
+Languages with JIT compilers may do warm up iterations for which timings are discarded.
+
+For each micro-benchmark, the 10th, 25th, 50th, 75th, 90th, 95th, 98th and 99th percentiles will be recorded using the
+following algorithm:
+
+- Given a 0-indexed array A of N iteration wall clock times
+- Sort the array into ascending order (i.e. shortest time first)
+- Let the index i for percentile p in the range [1,100] be defined as: `i = int(N * p / 100) - 1`
+
+*N.B. This is the [Nearest Rank](https://en.wikipedia.org/wiki/Percentile#The_Nearest_Rank_method) algorithm, chosen for
+its utter simplicity given that it needs to be implemented identically across multiple languages for every driver.*
+
+The 50th percentile (i.e. the median) will be used for score composition. Other percentiles will be stored for
+visualizations and analysis (e.g. a "candlestick" chart showing benchmark volatility over time).
+
+Each task will have defined for it an associated size in megabytes (MB). The score for micro-benchmark composition will
+be the task size in MB divided by the median wall clock time.
+
+## Micro-benchmark definitions
+
+Datasets are available in the `data` directory adjacent to this spec.
+
+Note: The term "LDJSON" means "line-delimited JSON", which should be understood to mean a collection of UTF-8 encoded
+JSON documents (without embedded CR or LF characters), separated by a single LF character. (Some Internet definition of
+line-delimited JSON use CRLF delimiters, but this benchmark uses only LF.)
+
+### BSON micro-benchmarks
+
+Datasets are in the `extended_bson` tarball.
+
+BSON tests focus on BSON encoding and decoding; they are client-side only and do not involve any transmission of data to
+or from the benchmark server. When appropriate, data sets will be stored on disk as
+[extended strict JSON](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json). For drivers that don't
+support extended JSON, a BSON analogue will be provided as well.
+
+BSON micro-benchmarks include:
+
+- Flat BSON Encoding and Flat BSON Decoding -- shallow documents with only common BSON field types
+- Deep BSON Encoding and Deep BSON Decoding -- deeply nested documents with only common BSON field types
+- Full BSON Encoding and Full BSON Decoding -- shallow documents with all possible BSON field types
+
+#### Flat BSON Encoding
+
+Summary: This benchmark tests driver performance encoding documents with top level key/value pairs involving the most
+commonly-used BSON types.
+
+Dataset: The dataset, designated FLAT_BSON (ftnt4 Disk file `flat_bson.json`), will be synthetically generated and
+consist of an extended JSON document with a single `_id` key with an object ID value plus 24 top level keys/value pairs
+of the following types: string, Int32, Int64, Double, Boolean. (121 total key/value pairs) Keys will be random ASCII
+strings of length 8. String data will be random ASCII strings of length 80.
+
+Dataset size: For score purposes, the dataset size for a task is the size of the single-document source file (7531
+bytes) times 10,000 operations, which equals 75,310,000 bytes or 75.31 MB.
+
+| Phase | Description |
+| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Load the FLAT_BSON dataset into memory as a language-appropriate document types. For languages like C without a document type, the raw JSON string for each document should be used instead. |
+| Before task | n/a |
+| Do task | Encode the FLAT_BSON document to a BSON byte-string. Repeat this 10,000 times. |
+| After task | n/a |
+| Teardown | n/a |
+
+#### Flat BSON Decoding
+
+Summary: This benchmark tests driver performance decoding documents with top level key/value pairs involving the most
+commonly-used BSON types.
+
+Dataset: The dataset, designated FLAT_BSON, will be synthetically generated and consist of an extended JSON document
+with a single `_id` key with an object ID value plus 24 top level keys/value pairs of each of the following types:
+string, Int32, Int64, Double, Boolean. (121 total key/value pairs) Keys will be random ASCII strings of length 8. String
+data will be random ASCII strings of length 80.
+
+Dataset size: For score purposes, the dataset size for a task is the size of the single-document source file (7531
+bytes) times 10,000 operations, which equals 75,310,000 bytes or 75.31 MB.
+
+| Phase | Description |
+| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Load the FLAT_BSON dataset into memory as a language-appropriate document types. For languages like C without a document type, the raw JSON string for each document should be used instead. Encode it to a BSON byte-string. |
+| Before task | n/a |
+| Do task | Decode the BSON byte-string to a language-appropriate document type. Repeat this 10,000 times. For languages like C without a document type, decode to extended JSON instead. |
+| After task | n/a |
+| Teardown | n/a |
+
+#### Deep BSON Encoding
+
+Summary: This benchmark tests driver performance encoding documents with deeply nested key/value pairs involving
+subdocuments, strings, integers, doubles and booleans.
+
+Dataset: The dataset, designated DEEP_BSON (disk file `deep_bson.json`), will be synthetically generated and consist of
+an extended JSON document representing a balanced binary tree of depth 6, with "left" and "right" keys at each level
+containing a sub-document until the final level, where each leaf contains two string fields, "leftValue" and
+"rightValue", each holding a random 8-character ASCII string (126 total key/value pairs).
+
+Dataset size: For score purposes, the dataset size for a task is the size of the single-document source file (2284
+bytes) times 10,000 operations, which equals 22,840,000 bytes or 22.84 MB.
+
+| Phase | Description |
+| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Load the DEEP_BSON dataset into memory as a language-appropriate document type. For languages like C without a document type, the raw JSON string for each document should be used instead. |
+| Before task | n/a |
+| Do task | Encode the DEEP_BSON document to a BSON byte-string. Repeat this 10,000 times. |
+| After task | n/a |
+| Teardown | n/a |
+
+#### Deep BSON Decoding
+
+Summary: This benchmark tests driver performance decoding documents with deeply nested key/value pairs involving
+subdocuments, strings, integers, doubles and booleans.
+
+Dataset: The dataset, designated DEEP_BSON, will be synthetically generated and consist of an extended JSON document
+representing a balanced binary tree of depth 6, with "left" and "right" keys at each level containing a sub-document
+until the final level, where each leaf contains two string fields, "leftValue" and "rightValue", each holding a random
+8-character ASCII string (126 total key/value pairs).
+
+Dataset size: For score purposes, the dataset size for a task is the size of the single-document source file (2284
+bytes) times 10,000 operations, which equals 22,840,000 bytes or 22.84 MB.
+
+| Phase | Description |
+| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Load the DEEP_BSON dataset into memory as a language-appropriate document types. For languages like C without a document type, the raw JSON string for each document should be used instead. Encode it to a BSON byte-string. |
+| Before task | n/a |
+| Do task | Decode the BSON byte-string to a language-appropriate document type. Repeat this 10,000 times. For languages like C without a document type, decode to extended JSON instead. |
+| After task | n/a |
+| Teardown | n/a |
+
+#### Full BSON Encoding
+
+Summary: This benchmark tests driver performance encoding documents with top level key/value pairs involving the full
+range of BSON types.
+
+Dataset: The dataset, designated FULL_BSON (disk file `full_bson.json`), will be synthetically generated and consist of
+an extended JSON document with a single `_id` key with an object ID value plus 6 each of the following types: string,
+double, Int64, Int32, boolean, minkey, maxkey, array, binary data, UTC datetime, regular expression, Javascript code,
+Javascript code with context, and timestamp. (91 total keys.) Keys (other than `_id`) will be random ASCII strings of
+length 8. Strings values will be random ASCII strings with length 80.
+
+Dataset size: For score purposes, the dataset size for a task is the size of the single-document source file (5734
+bytes) times 10,000 operations, which equals 57,340,000 bytes or 57.34 MB.
+
+| Phase | Description |
+| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Load the FULL_BSON dataset into memory as a language-appropriate document type. For languages like C without a document type, the raw JSON string for each document should be used instead. |
+| Before task | n/a |
+| Do task | Encode the FULL_BSON document to a BSON byte-string. Repeat this 10,000 times. |
+| After task | n/a |
+| Teardown | n/a |
+
+#### Full BSON Decoding
+
+Summary: This benchmark tests driver performance decoding documents with top level key/value pairs involving the full
+range of BSON types.
+
+Dataset: The dataset, designated FULL_BSON, will be synthetically generated and consist of an extended JSON document
+with a single `_id` key with an object ID value plus 6 each of the following types: string, double, Int64, Int32,
+boolean, minkey, maxkey, array, binary data, UTC datetime, regular expression, Javascript code, Javascript code with
+context, and timestamp. (91 total keys.) Keys (other than `_id`) will be random ASCII strings of length 8. Strings
+values will be random ASCII strings with length 80.
+
+Dataset size: For score purposes, the dataset size for a task is the size of the single-document source file (5734
+bytes) times 10,000 operations, which equals 57,340,000 bytes or 57.34 MB.
+
+| Phase | Description |
+| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Load the FULL_BSON dataset into memory as a language-appropriate document types. For languages like C without a document type, the raw JSON string for each document should be used instead. Encode it to a BSON byte-string. |
+| Before task | n/a |
+| Do task | Decode the BSON byte-string to a language-appropriate document type. Repeat this 10,000 times. For languages like C without a document type, decode to extended JSON instead. |
+| After task | n/a |
+| Teardown | n/a |
+
+### Single-Doc Benchmarks
+
+Datasets are in the `single_and_multi_document` tarball.
+
+Single-doc tests focus on single-document read and write operations. They are designed to give insights into the
+efficiency of the driver's implementation of the basic wire protocol.
+
+The data will be stored as strict JSON with no extended types.
+
+Single-doc micro-benchmarks include:
+
+- Run command
+- Find one by ID
+- Small doc insertOne
+- Large doc insertOne
+
+#### Run command
+
+Summary: This benchmark tests driver performance sending a command to the database and reading a response.
+
+Dataset: n/a
+
+Dataset size: While there is no external dataset, for score calculation purposes use 130,000 bytes (10,000 x the size of
+a BSON {hello:true} command).
+
+*N.B. We use {hello:true} rather than {hello:1} to ensure a consistent command size.*
+
+| Phase | Description |
+| ----------- | ------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Construct a MongoClient object. Construct whatever language-appropriate objects (Database, etc.) would be required to send a command. |
+| Before task | n/a |
+| Do task | Run the command {hello:true} 10,000 times, reading (and discarding) the result each time. |
+| After task | n/a |
+| Teardown | n/a |
+
+#### Find one by ID
+
+Summary: This benchmark tests driver performance sending an indexed query to the database and reading a single document
+in response.
+
+Dataset: The dataset, designated TWEET (disk file `tweet.json`), consists of a sample tweet stored as strict JSON.
+
+Dataset size: For score purposes, the dataset size for a task is the size of the single-document source file (1622
+bytes) times 10,000 operations, which equals 16,220,000 bytes or 16.22 MB.
+
+| Phase | Description |
+| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| Setup | Construct a MongoClient object. Drop the `perftest` database. Load the TWEET document into memory as a language-appropriate document type (or JSON string for C). Construct a Collection object for the `corpus` collection to use for querying. Insert the document 10,000 times to the `perftest` database in the `corpus` collection using sequential `_id` values. (1 to 10,000) |
+| Before task | n/a |
+| Do task | For each of the 10,000 sequential `_id` numbers, issue a find command for that `_id` on the `corpus` collection and retrieve the single-document result. |
+| After task | n/a |
+| Teardown | Drop the `perftest` database. |
+
+#### Small doc insertOne
+
+Summary: This benchmark tests driver performance inserting a single, small document to the database.
+
+Dataset: The dataset, designated SMALL_DOC (disk file `small_doc.json`), consists of a JSON document with an encoded
+length of approximately 250 bytes.
+
+Dataset size: For score purposes, the dataset size for a task is the size of the single-document source file (275 bytes)
+times 10,000 operations, which equals 2,750,000 bytes or 2.75 MB.
+
+| Phase | Description |
+| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Construct a MongoClient object. Drop the `perftest` database. Load the SMALL_DOC dataset into memory as a language-appropriate document type (or JSON string for C). |
+| Before task | Drop the `corpus` collection. Create an empty `corpus` collection with the `create` command. Construct a Collection object for the `corpus` collection to use for insertion. |
+| Do task | Insert the document with the insertOne CRUD method. DO NOT manually add an `_id` field; leave it to the driver or database. Repeat this 10,000 times. |
+| After task | n/a |
+| Teardown | Drop the `perftest` database. |
+
+#### Large doc insertOne
+
+Summary: This benchmark tests driver performance inserting a single, large document to the database.
+
+Dataset: The dataset, designated LARGE_DOC (disk file `large_doc.json`), consists of a JSON document with an encoded
+length of approximately 2,500,000 bytes.
+
+Dataset size: For score purposes, the dataset size for a task is the size of the single-document source file (2,731,089
+bytes) times 10 operations, which equals 27,310,890 bytes or 27.31 MB.
+
+| Phase | Description |
+| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Construct a MongoClient object. Drop the `perftest` database. Load the LARGE_DOC dataset into memory as a language-appropriate document type (or JSON string for C). |
+| Before task | Drop the `corpus` collection. Create an empty `corpus` collection with the `create` command. Construct a Collection object for the `corpus` collection to use for insertion. |
+| Do task | Insert the document with the insertOne CRUD method. DO NOT manually add an `_id` field; leave it to the driver or database. Repeat this 10 times. |
+| After task | n/a |
+| Teardown | Drop the `perftest` database. |
+
+### Multi-Doc Benchmarks
+
+Datasets are in the `single_and_multi_document` tarball.
+
+Multi-doc benchmarks focus on multiple-document read and write operations. They are designed to give insight into the
+efficiency of the driver's implementation of bulk/batch operations such as bulk writes and cursor reads.
+
+Multi-doc micro-benchmarks include:
+
+- Find many and empty the cursor
+- Small doc bulk insert
+- Large doc bulk insert
+- Small doc Collection BulkWrite insert
+- Large doc Collection BulkWrite insert
+- Small doc Client BulkWrite insert
+- Large doc Client BulkWrite insert
+- Small doc Client BulkWrite Mixed Operations
+- Small doc Collection BulkWrite Mixed Operations
+- GridFS upload
+- GridFS download
+
+#### Find many and empty the cursor
+
+Summary: This benchmark tests driver performance retrieving multiple documents from a query.
+
+Dataset: The dataset, designated TWEET consists of a sample tweet stored as strict JSON.
+
+Dataset size: For score purposes, the dataset size for a task is the size of the single-document source file (1622
+bytes) times 10,000 operations, which equals 16,220,000 bytes or 16.22 MB.
+
+| Phase | Description |
+| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Construct a MongoClient object. Drop the `perftest` database. Load the TWEET dataset into memory as a language-appropriate document type (or JSON string for C). Construct a Collection object for the `corpus` collection to use for querying. Insert the document 10,000 times to the `perftest` database in the `corpus` collection. (Let the driver generate `_id`s). |
+| Before task | n/a |
+| Do task | Issue a find command on the `corpus` collection with an empty filter expression. Retrieve (and discard) all documents from the cursor. |
+| After task | n/a |
+| Teardown | Drop the `perftest` database. |
+
+#### Small doc bulk insert
+
+Summary: This benchmark tests driver performance inserting multiple small documents to the database using `insertMany`.
+
+Dataset: The dataset, designated SMALL_DOC consists of a JSON document with an encoded length of approximately 250
+bytes.
+
+Dataset size: For score purposes, the dataset size for a task is the size of the single-document source file (275 bytes)
+times 10,000 operations, which equals 2,750,000 bytes or 2.75 MB.
+
+| Phase | Description |
+| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Construct a MongoClient object. Drop the `perftest` database. Load the SMALL_DOC dataset into memory as a language-appropriate document type (or JSON string for C). |
+| Before task | Drop the `corpus` collection. Create an empty `corpus` collection with the `create` command. Construct a Collection object for the `corpus` collection to use for insertion. |
+| Do task | Do an ordered `insertMany` with 10,000 copies of the document. DO NOT manually add an `_id` field; leave it to the driver or database. |
+| After task | n/a |
+| Teardown | Drop the `perftest` database. |
+
+#### Large doc bulk insert
+
+Summary: This benchmark tests driver performance inserting multiple large documents to the database using `insertMany`.
+
+Dataset: The dataset, designated LARGE_DOC consists of a JSON document with an encoded length of approximately 2,500,000
+bytes.
+
+Dataset size: For score purposes, the dataset size for a task is the size of the single-document source file (2,731,089
+bytes) times 10 operations, which equals 27,310,890 bytes or 27.31 MB.
+
+| Phase | Description |
+| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Construct a MongoClient object. Drop the `perftest` database. Load the LARGE_DOC dataset into memory as a language-appropriate document type (or JSON string for C). |
+| Before task | Drop the `corpus` collection. Create an empty `corpus` collection with the `create` command. Construct a Collection object for the `corpus` collection to use for insertion. |
+| Do task | Do an ordered `insertMany` with 10 copies of the document. DO NOT manually add an `_id` field; leave it to the driver or database. |
+| After task | n/a |
+| Teardown | Drop the `perftest` database. |
+
+#### Small doc Collection BulkWrite insert
+
+Summary: This benchmark tests driver performance inserting multiple small documents to the database using the
+`Collection::bulkWrite` operation.
+
+Dataset: The dataset, designated SMALL_DOC consists of a JSON document with an encoded length of approximately 250
+bytes.
+
+Dataset size: For score purposes, the dataset size for a task is the size of the single-document source file (275 bytes)
+times 10,000 operations, which equals 2,750,000 bytes or 2.75 MB.
+
+| Phase | Description |
+| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| Setup | Construct a MongoClient object. Drop the `perftest` database. Load the SMALL_DOC dataset into memory as a language-appropriate document type (or JSON string for C). Create a list of insert models for 10,000 copies of the document, allowing the driver or database to auto-assign the `_id` field. |
+| Before task | Drop the `corpus` collection. Create an empty `corpus` collection with the `create` command. Construct a Collection object for the `corpus` collection to use for insertion. |
+| Do task | Do an ordered `Collection::bulkWrite` with the list of insert models. |
+| After task | n/a |
+| Teardown | Drop the `perftest` database. |
+
+#### Large doc Collection BulkWrite insert
+
+Summary: This benchmark tests driver performance inserting multiple large documents to the database using the
+`Collection::bulkWrite` operation.
+
+Dataset: The dataset, designated LARGE_DOC consists of a JSON document with an encoded length of approximately 2,500,000
+bytes.
+
+Dataset size: For score purposes, the dataset size for a task is the size of the single-document source file (2,731,089
+bytes) times 10 operations, which equals 27,310,890 bytes or 27.31 MB.
+
+| Phase | Description |
+| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Construct a MongoClient object. Drop the `perftest` database. Load the LARGE_DOC dataset into memory as a language-appropriate document type (or JSON string for C). Create a list of insert models for 10 copies of the document, allowing the driver or database to auto-assign the `_id` field. |
+| Before task | Drop the `corpus` collection. Create an empty `corpus` collection with the `create` command. Construct a Collection object for the `corpus` collection to use for insertion. |
+| Do task | Do an ordered `Collection::bulkWrite` with the list of insert models. |
+| After task | n/a |
+| Teardown | Drop the `perftest` database. |
+
+#### Small doc Client BulkWrite insert
+
+Summary: This benchmark tests driver performance inserting multiple small documents to the database using the
+`Client::bulkWrite` operation.
+
+Dataset: The dataset, designated SMALL_DOC consists of a JSON document with an encoded length of approximately 250
+bytes.
+
+Dataset size: For score purposes, the dataset size for a task is the size of the single-document source file (275 bytes)
+times 10,000 operations, which equals 2,750,000 bytes or 2.75 MB.
+
+| Phase | Description |
+| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| Setup | Construct a MongoClient object. Drop the `perftest` database. Load the SMALL_DOC dataset into memory as a language-appropriate document type (or JSON string for C). Create a list of insert models for 10,000 copies of the document in the same namespace ("perftest.corpus"), allowing the driver or database to auto-assign the `_id` field. |
+| Before task | Drop the `corpus` collection. Create an empty `corpus` collection with the `create` command. Construct a Collection object for the `corpus` collection to use for insertion. |
+| Do task | Do an ordered `Client::bulkWrite` with the list of insert models. |
+| After task | n/a |
+| Teardown | Drop the `perftest` database. |
+
+#### Large doc Client BulkWrite insert
+
+Summary: This benchmark tests driver performance inserting multiple large documents to the database using the
+`Client::bulkWrite` operation.
+
+Dataset: The dataset, designated LARGE_DOC consists of a JSON document with an encoded length of approximately 2,500,000
+bytes.
+
+Dataset size: For score purposes, the dataset size for a task is the size of the single-document source file (2,731,089
+bytes) times 10 operations, which equals 27,310,890 bytes or 27.31 MB.
+
+| Phase | Description |
+| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Construct a MongoClient object. Drop the `perftest` database. Load the LARGE_DOC dataset into memory as a language-appropriate document type (or JSON string for C). Create a list of insert models for 10 copies of the document in the same namespace ("perftest.corpus"), allowing the driver or database to auto-assign the `_id` field. |
+| Before task | Drop the `corpus` collection. Create an empty `corpus` collection with the `create` command. Construct a Collection object for the `corpus` collection to use for insertion.
|
+| Do task | Do an ordered `Client::bulkWrite` with the list of insert models. |
+| After task | n/a |
+| Teardown | Drop the `perftest` database. |
+
+#### Small doc Client BulkWrite Mixed Operations
+
+Summary: This benchmark tests driver performance of a `Client::bulkWrite` operation with small documents and mixed
+operations (e.g. insert, replace and delete).
+
+Dataset: The dataset, designated SMALL_DOC consists of a JSON document with an encoded length of approximately 250
+bytes.
+
+Dataset size: For score purposes, the dataset size for a task is the size of the single-document source file (275 bytes)
+times 20,000 operations (insert + replace), which equals 5,500,000 bytes or 5.5 MB.
+
+| Phase | Description |
+| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Construct a MongoClient object. Load the SMALL_DOC dataset into memory as a language-appropriate document type (or JSON string for C).
Create a list of 10 numbered collection names (`corpus_1`, `corpus_2`, ...)
Create a list of write models for 10,000 document copies. For each document (where i goes from 0 to 9,999), create three write models with the namespace set to the collection name found at index `i % 10` in your collection names list:
1. An insert model.
2. A replace model (using an empty filter and a copy of the document).
3. A delete model (using an empty filter).
Notes:
- DO NOT manually add an `_id` field; leave it to the driver or database. You should have 30,000 write models in your list. |
+| Before task | Drop the `perftest` database, create it again and create the collections found in your numbered collection names list. |
+| Do task | Do an ordered `Client::bulkWrite` with the list of write models. |
+| After task | n/a |
+| Teardown | Drop the `perftest` database. |
+
+#### Small doc Collection BulkWrite Mixed Operations
+
+Summary: This benchmark tests driver performance of a `Collection::bulkWrite` operation with small documents and mixed
+operations (e.g. insert, replace and delete).
+
+Dataset: The dataset, designated SMALL_DOC consists of a JSON document with an encoded length of approximately 250
+bytes.
+
+Dataset size: For score purposes, the dataset size for a task is the size of the single-document source file (275 bytes)
+times 20,000 operations (insert + replace), which equals 5,500,000 bytes or 5.5 MB.
+
+| Phase | Description |
+| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Construct a MongoClient object. Drop the `perftest` database. Load the SMALL_DOC dataset into memory as a language-appropriate document type (or JSON string for C). Create a list of write models for 10,000 copies of the document where for each copy of the document add the following models:
1. insert model.
2. replace model with a copy and using an empty filter.
3. delete model using empty filter.
DO NOT manually add an `_id` field; leave it to the driver or database. You should have 30,000 write models in your list. |
+| Before task | Drop the `corpus` collection. Create an empty `corpus` collection with the `create` command. Construct a Collection object for the `corpus` collection to use for insertion. |
+| Do task | Do an ordered `Collection::bulkWrite` with the list of write models. |
+| After task | n/a |
+| Teardown | Drop the `perftest` database. |
+
+#### GridFS upload
+
+Summary: This benchmark tests driver performance uploading a GridFS file from memory.
+
+Dataset: The dataset, designated GRIDFS_LARGE (disk file `gridfs_large.bin`), consists of a single file containing about
+50 MB of random data. We use a large file to ensure multiple database round-trips even if chunks are are sent in
+batches.
+
+Dataset size: For score purposes, the dataset size for a task is the size of the source file (52,428,800 bytes) times 1
+operation or 52.43 MB.
+
+| Phase | Description |
+| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Construct a MongoClient object. rop the `perftest` database. Load the GRIDFS_LARGE file as a string or other language-appropriate type for binary octet data. |
+| Before task | Drop the default GridFS bucket. Insert a 1-byte file into the bucket. (This ensures the bucket collections and indices have been created.) Construct a GridFSBucket object to use for uploads. |
+| Do task | Upload the GRIDFS_LARGE data as a GridFS file. Use whatever upload API is most natural for each language (e.g. open_upload_stream(), write the data to the stream and close the stream). |
+| After task | n/a |
+| Teardown | Drop the `perftest` database. |
+
+#### GridFS download
+
+Summary: This benchmark tests driver performance downloading a GridFS file to memory.
+
+Dataset: The dataset, designated GRIDFS_LARGE, consists of a single file containing about 50 MB of random data. We use a
+large file to ensure multiple database round-trips even if chunks are are sent in batches.
+
+Dataset size: For score purposes, the dataset size for a task is the size of the source file (52,428,800 bytes) times 1
+operation or 52.43 MB.
+
+| Phase | Description |
+| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Construct a MongoClient object. Drop the `perftest` database. Upload the GRIDFS_LARGE file to the default gridFS bucket with the name "gridfstest". Record the `_id` of the uploaded file. |
+| Before task | Construct a GridFSBucket object to use for downloads. |
+| Do task | Download the "gridfstest" file by its `_id`. Use whatever download API is most natural for each language (e.g. open_download_stream(), read from the stream into a variable). Discard the downloaded data. |
+| After task | n/a |
+| Teardown | Drop the `perftest` database. |
+
+### Parallel
+
+Datasets are in the `parallel` tarball.
+
+Parallel tests simulate ETL operations from disk to database or vice-versa. They are designed to be implemented using a
+language's preferred approach to concurrency and thus stress how drivers handle concurrency. These intentionally involve
+overhead above and beyond the driver itself to simulate -- however loosely -- the sort of "real-world" pressures that a
+drivers would be under during concurrent operation.
+
+They are intended for directional indication of which languages perform best for this sort of pseudo-real-world
+activity, but are not intended to represent real-world performance claims.
+
+Drivers teams are expected to treat these as a competitive "shoot-out" to surface optimal ETL patterns for each language
+(e.g. multi-thread, multi-process, asynchronous I/O, etc.).
+
+Parallel micro-benchmarks include:
+
+- LDJSON multi-file import
+- LDJSON multi-file export
+- GridFS multi-file upload
+- GridFS multi-file download
+
+#### LDJSON multi-file import
+
+Summary: This benchmark tests driver performance importing documents from a set of LDJSON files.
+
+Dataset: The dataset, designated LDJSON_MULTI (disk directory `ldjson_multi`), consists of 100 LDJSON files, each
+containing 5,000 JSON documents. Each document should be about 1000 bytes.
+
+Dataset size: For score purposes, the dataset size for a task is the total size of all source files: 565,000,000 bytes
+or 565 MB.
+
+| Phase | Description |
+| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Construct a MongoClient object. Drop the `perftest` database. |
+| Before task | Drop the `corpus` collection. Create an empty `corpus` collection with the `create` command. |
+| Do task | Do an unordered insert of all 500,000 documents in the dataset into the `corpus` collection as fast as possible. Data must be loaded from disk during this phase. Concurrency is encouraged. |
+| After task | n/a |
+| Teardown | Drop the `perftest` database. |
+
+#### LDJSON multi-file export
+
+Summary: This benchmark tests driver performance exporting documents to a set of LDJSON files.
+
+Dataset: The dataset, designated LDJSON_MULTI, consists of 100 LDJSON files, each containing 5,000 JSON documents. Each
+document should be about 1000 bytes.
+
+Dataset size: For score purposes, the dataset size for a task is the total size of all source files: 565,000,000 bytes
+or 565 MB.
+
+| Phase | Description |
+| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Construct a MongoClient object. Drop the `perftest` database. Drop the `corpus` collection. Do an unordered insert of all 500,000 documents in the dataset into the `corpus` collection. |
+| Before task | Construct whatever objects, threads, etc. are required for exporting the dataset. |
+| Do task | Dump all 500,000 documents in the dataset into 100 LDJSON files of 5,000 documents each as fast as possible. Data must be completely written/flushed to disk during this phase. Concurrency is encouraged. The order and distribution of documents across files does not need to match the original LDJSON_MULTI files. |
+| After task | n/a |
+| Teardown | Drop the `perftest` database. |
+
+#### GridFS multi-file upload
+
+Summary: This benchmark tests driver performance uploading files from disk to GridFS.
+
+Dataset: The dataset, designated GRIDFS_MULTI (disk directory `gridfs_multi`), consists of 50 files, each of 5MB. This
+file size corresponds roughly to the output of a (slightly dated) digital camera. Thus the task approximates uploading
+50 "photos".
+
+Dataset size: For score purposes, the dataset size for a task is the total size of all source files: 262,144,000 bytes
+or 262.144 MB.
+
+| Phase | Description |
+| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Construct a MongoClient object. Drop the `perftest` database. |
+| Before task | Drop the default GridFS bucket in the `perftest` database. Construct a GridFSBucket object for the default bucket in `perftest` to use for uploads. Insert a 1-byte file into the bucket (to initialize indexes). |
+| Do task | Upload all 50 files in the GRIDFS_MULTI dataset (reading each from disk). Concurrency is encouraged. |
+| After task | n/a |
+| Teardown | Drop the `perftest` database. |
+
+#### GridFS multi-file download
+
+Summary: This benchmark tests driver performance downloading files from GridFS to disk.
+
+Dataset: The dataset, designated GRIDFS_MULTI, consists of 50 files, each of 5MB. This file size corresponds roughly to
+the output of a (slightly dated) digital camera. Thus the task approximates downloading 50 "photos".
+
+Dataset size: For score purposes, the dataset size for a task is the total size of all source files: 262,144,000 bytes
+or 262.144 MB.
+
+| Phase | Description |
+| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| Setup | Construct a MongoClient object. Drop the `perftest` database. Construct a temporary directory for holding downloads. Drop the default GridFS bucket in the `perftest` database. Upload the 50 file dataset to the default GridFS bucket in `perftest`. |
+| Before task | Delete all files in the temporary folder for downloads. Construct a GridFSBucket object to use for downloads from the default bucket in `perftest`. |
+| Do task | Download all 50 files in the GRIDFS_MULTI dataset, saving each to a file in the temporary folder for downloads. Data must be completely written/flushed to disk during this phase. Concurrency is encouraged. |
+| After task | n/a |
+| Teardown | Drop the `perftest` database. |
+
+## Composite score calculation
+
+Every micro-benchmark has a score equal to the 50th percentile (median) of sampled timings, expressed as Megabytes
+(1,000,000 bytes) per second where the micro-benchmark "database size" given in each section above is divided by the
+50th percentile of measured wall-clock times.
+
+From these micro-benchmarks, the following composite scores must be calculated:
+
+| Composite Name | Compositing formula |
+| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| BSONBench | Average of all BSON micro-benchmarks |
+| SingleBench | Average of all Single-doc micro-benchmarks, except "Run Command" |
+| MultiBench | Average of all Multi-doc micro-benchmarks |
+| ParallelBench | Average of all Parallel micro-benchmarks |
+| ReadBench | Average of "Find one", "Find many and empty cursor", "GridFS download", "LDJSON multi-file export", and "GridFS multi-file download" microbenchmarks |
+| WriteBench | Average of "Small doc insertOne", "Large doc insertOne", "Small doc bulk insert", "Large doc bulk insert", "Small doc Collection BulkWrite insert", "Large doc Collection BulkWrite insert", "Small doc Client BulkWrite insert", "Large doc Client BulkWrite insert", "Small doc Client BulkWrite Mixed Operations", "Small doc Collection BulkWrite Mixed Operations", "GridFS upload", "LDJSON multi-file import", and "GridFS multi-file upload" micro-benchmarks |
+| DriverBench | Average of ReadBench and WriteBench |
+
+At least for this first DriverBench version, scores are combined with simple averages. In addition, the BSONBench scores
+do not factor into the overall DriverBench scores, as encoding and decoding are inherent in all other tasks.
+
+## Benchmark platform, configuration and environments
+
+### Benchmark Client
+
+TBD: spec Amazon instance size; describe in general terms how language clients will be run independently; same AWS zone
+as server
+
+All operations must be run with write concern "w:1".
+
+### Benchmark Server
+
+TBD: spec Amazon instance size; describe configuration (e.g. no auth, journal, pre-alloc sizes?, WT with compression to
+minimize disk I/O impact?); same AWS zone as client
+
+### Score Server
+
+TBD: spec system to hold scores over time
+
+### Datasets
+
+TBD: generated datasets should be park in S3 or somewhere for retrieval by URL
+
+## Changelog
+
+- 2026-06-02: Correct DEEP_BSON dataset size to 2284 bytes (22,840,000 bytes / 22.84 MB)
+
+- 2026-01-21: Update `deep_bson.json` for compatibility with strong typed benchmarks.
+
+- 2024-12-23: Add Client and Collection BulkWrite benchmarks
+
+- 2024-01-22: Migrated from reStructuredText to Markdown.
+
+- 2022-10-05: Remove spec front matter and reformat changelog.
+
+- 2021-04-06: Update run command test to use `hello` command
+
+- 2016-08-13:
+
+ - Update corpus files to allow much greater compression of data
+ - Updated LDJSON corpus size to reflect revisions to the test data
+ - Published data files on GitHub and updated instructions on how to find datasets
+ - RunCommand and query benchmark can create collection objects during setup rather than before task. (No change on
+ actual benchmark.)
+
+- 2016-01-06:
+
+ - Clarify that `bulk insert` means `insert_many`
+ - Clarify that "create a collection" means using the `create` command
+ - Add omitted "upload files" step to setup for GridFS multi-file download; also clarify that steps should be using the
+ default bucket in the `perftest` database
+
+- 2015-12-23:
+
+ - Rename benchmark names away from MMA/weight class names
+ - Split BSON encoding and decoding micro-benchmarks
+ - Rename BSON micro-benchmarks to better match dataset names
+ - Move "Run Command" micro-benchmark out of composite
+ - Reduced amount of data held in memory and sent to/from the server to decrease memory pressure and increase number of
+ iterations in a reasonable time (e.g. file sizes and number of documents in certain datasets changed)
+ - Create empty collections/indexes during the `before` phase when appropriate
+ - Updated data set sizes to account for changes in the source file structure/size
diff --git a/source/benchmarking/benchmarking.rst b/source/benchmarking/benchmarking.rst
index c09e4c7dd6..485f7c6452 100644
--- a/source/benchmarking/benchmarking.rst
+++ b/source/benchmarking/benchmarking.rst
@@ -1,1169 +1,4 @@
-=======================================
-MongoDB Driver Performance Benchmarking
-=======================================
-:Title: MongoDB Driver Performance Benchmarking
-:Author: David Golden
-:Minimum Server Version: N/A
-:Last Modified: Aug 13, 2016
-:Version: 1.3
-
-.. contents::
-
-Abstract
-========
-
-This document describes a standard benchmarking suite for MongoDB drivers.
-
-Overview
-========
-
-Name and purpose
-----------------
-
-Driver performance will be measured by the MongoDB Driver Performance
-Benchmark (AKA "DriverBench"). It will provide both "horizontal" insights
-into how individual language driver performance evolves over time and
-"vertical" insights into relative performance of different drivers.
-
-We do expect substantial performance differences between language families
-(e.g. static vs. dynamic or compiled vs. virtual-machine-based). However we
-still expect "vertical" comparison within language families to expose outlier
-behavior that might be amenable to optimization.
-
-Task Hierarchy
---------------
-
-The benchmark consists of a number of micro-benchmarks tasks arranged
-into groups of increasing complexity. This allows us to better isolate
-areas within drivers that are faster or slower.
-
-- BSON -- BSON encoding/decoding tasks, to explore BSON codec efficiency
-- Single-Doc -- single-document insertion and query tasks, to explore
- basic wire protocol efficiency
-- Multi-Doc -- multi-document insertion and query tasks, to explore
- batch-write and cursor chunking efficiency
-- Parallel -- multi-process/thread ETL tasks, to explore concurrent
- operation efficiency
-
-Measurement
------------
-
-In addition to timing data, all micro-benchmark tasks will be measured in
-terms of "megabytes/second" (MB/s) of documents processed, with higher scores
-being better. (In this document, "megabyte" refers to the SI decimal unit,
-i.e. 1,000,000 bytes.) This makes cross-benchmark comparisons easier.
-
-To avoid various types of measurement skew, tasks will be measured over
-numerous iterations. Each iteration will have a "scale" -- the number of
-similar operations performed -- that will vary by task. The final score for a
-task will be the median score of the iterations. Other quantiles will be
-recorded for diagnostic analysis.
-
-Data sets
----------
-
-Data sets will vary by micro-benchmark. In some cases, they it will be a
-synthetically generated document inserted repeatedly (with different '\_id'
-fields) to construct an overall corpus of documents. In other cases, data sets
-will be synthetic line-delimited JSON files or mock binary files.
-
-Composite scores
-----------------
-
-Micro-benchmark scores will be combined into a composite for each weight class
-("BSONBench", "SingleBench", etc.) and for read and write operations
-("ReadBench" and ("WriteBench"). The read and write scores will be combined
-into an aggregate composite score ("DriverBench"). The compositing formula in
-the DriverBench uses simple averages with equal weighting.
-
-Versioning
-----------
-
-DriverBench will have vX.Y versioning. Minor updates and clarifications will
-increment "Y" and should have little impact on score comparison. Major
-changes, such as changing score weights, MongoDB version tested against, or
-hardware used, will increment "X" to indicate that older version scores are
-unlikely to be comparable.
-
-Benchmark execution phases and measurement
-==========================================
-
-All micro-benchmark tasks will be conducted via a number of iterations. Each
-iteration will be timed and will generally include a large number of
-individual driver operations.
-
-We break up the measurement this way to better isolate the benchmark from
-external volatility. If we consider the problem of benchmarking an operation
-over many iterations, such as 100,000 document insertions, we want to avoid
-two extreme forms of measurement:
-
-- measuring a single insertion 100,000 times -- in this case, the timing
- code is likely to be a greater proportion of executed code, which
- could routinely evict the insertion code from CPU caches or mislead a
- JIT optimizer and throw off results
-- measuring 100,000 insertions one time -- in this case, the longer the
- timer runs, the higher the likelihood that an external event occurs
- that affects the time of the run
-
-Therefore, we choose a middle ground:
-
-- measuring the same 1000 insertions over 100 iterations -- each timing
- run includes enough operations that insertion code dominates timing
- code; unusual system events are likely to affect only a fraction of
- the 100 timing measurements
-
-With 100 timings of inserting the same 1000 documents, we build up a
-statistical distribution of the operation timing, allowing a more robust
-estimate of performance than a single measurement. (In practice, the
-number of iterations could exceed 100, but 100 is a reasonable minimum
-goal.)
-
-Because a timing distribution is bounded by zero on one side, taking the
-mean would allow large positive outlier measurements to skew the result
-substantially. Therefore, for the benchmark score, we use the median
-timing measurement, which is robust in the face of outliers.
-
-Each benchmark is structured into discrete setup/execute/teardown
-phases. Phases are as follows, with specific details given in a
-subsequent section:
-
-- setup -- (ONCE PER MICRO-BENCHMARK) something to do once before any
- benchmarking, e.g. construct a client object, load test data, insert
- data into a collection, etc.
-- before task -- (ONCE PER ITERATION) something to do before every task
- iteration, e.g. drop a collection, or reload test data (if the test
- run modifies it), etc.
-- do task -- (ONCE PER ITERATION) smallest amount of code necessary to
- execute the task; e.g. insert 1000 documents one by one into the
- database, or retrieve 1000 document of test data from the database,
- etc.
-- after task -- (ONCE PER ITERATION) something to do after every task
- iteration (if necessary)
-- teardown -- (ONCE PER MICRO-BENCHMARK) something done once after all
- benchmarking is complete (if necessary); e.g. drop the test database
-
-The wall-clock execution time of each "do task" phase will be recorded.
-We use wall clock time to model user experience and as a lowest-common
-denominator across languages and threading models. Iteration timing
-should be done with a high-resolution monotonic timer (or best language
-approximation).
-
-Unless otherwise specified, the number of iterations to measure per
-micro-benchmark is variable:
-
-- iterations should loop for at least 1 minute cumulative execution
- time
-- iterations should stop after 100 iterations or 5 minutes cumulative
- execution time, whichever is shorter
-
-This balances measurement stability with a timing cap to ensure all
-micro-benchmarks can complete in a reasonable time. Languages with JIT
-compilers may do warm up iterations for which timings are discarded.
-
-For each micro-benchmark, the 10th, 25th, 50th, 75th, 90th, 95th, 98th
-and 99th percentiles will be recorded using the following
-algorithm:
-
-- Given a 0-indexed array A of N iteration wall clock times
-- Sort the array into ascending order (i.e. shortest time first)
-- Let the index i for percentile p in the range [1,100] be defined as:
- i = int(N \* p / 100) - 1
-
-*N.B. This is the `Nearest Rank`_ algorithm, chosen for its utter simplicity
-given that it needs to be implemented identically across multiple languages for
-every driver.*
-
-.. _Nearest Rank: https://en.wikipedia.org/wiki/Percentile#The_Nearest_Rank_method
-
-The 50th percentile (i.e. the median) will be used for score
-composition. Other percentiles will be stored for visualizations and
-analysis (e.g. a "candlestick" chart showing benchmark volatility over
-time).
-
-Each task will have defined for it an associated size in megabytes (MB).
-The score for micro-benchmark composition will be the task size in MB
-divided by the median wall clock time.
-
-Micro-benchmark definitions
-===========================
-
-Datasets are available in the `data` directory adjacent to this spec.
-
-Note: The term "LDJSON" means "line-delimited JSON", which should be
-understood to mean a collection of UTF-8 encoded JSON documents (without
-embedded CR or LF characters), separated by a single LF character. (Some
-Internet definition of line-delimited JSON use CRLF delimiters, but this
-benchmark uses only LF.)
-
-BSON micro-benchmarks
----------------------
-
-Datasets are in the 'extended\_bson' tarball.
-
-BSON tests focus on BSON encoding and decoding; they are client-side only and
-do not involve any transmission of data to or from the benchmark server. When
-appropriate, data sets will be stored on disk as `extended strict JSON`_. For
-drivers that don't support extended JSON, a BSON analogue will be provided as
-well.
-
-.. _extended strict JSON: https://docs.mongodb.org/manual/reference/mongodb-extended-json
-
-BSON micro-benchmarks include:
-
-- Flat BSON Encoding and Flat BSON Decoding -- shallow documents with
- only common BSON field types
-- Deep BSON Encoding and Deep BSON Decoding -- deeply nested documents
- with only common BSON field types
-- Full BSON Encoding and Full BSON Decoding -- shallow documents with
- all possible BSON field types
-
-Flat BSON Encoding
-~~~~~~~~~~~~~~~~~~
-
-Summary: This benchmark tests driver performance encoding documents with
-top level key/value pairs involving the most commonly-used BSON types.
-
-Dataset: The dataset, designated FLAT\_BSON (ftnt4 Disk file
-'flat\_bson.json'), will be synthetically generated and consist of an extended
-JSON document with a single \_id key with an object ID value plus 24 top level
-keys/value pairs of the following types: string, Int32, Int64, Double,
-Boolean. (121 total key/value pairs) Keys will be random ASCII strings of
-length 8. String data will be random ASCII strings of length 80.
-
-Dataset size: For score purposes, the dataset size for a task is the
-size of the single-document source file (7531 bytes) times 10,000
-operations, which equals 75,310,000 bytes or 75.31 MB.
-
-Phases:
-
-+--------------------------------------+--------------------------------------+
-| Setup | Load the FLAT\_BSON dataset into |
-| | memory as a language-appropriate |
-| | document types. For languages like |
-| | C without a document type, the raw |
-| | JSON string for each document should |
-| | be used instead. |
-+--------------------------------------+--------------------------------------+
-| Before task | n/a |
-+--------------------------------------+--------------------------------------+
-| Do task | Encode the FLAT\_BSON document to a |
-| | BSON byte-string. Repeat this 10,000 |
-| | times. |
-+--------------------------------------+--------------------------------------+
-| After task | n/a |
-+--------------------------------------+--------------------------------------+
-| Teardown | n/a |
-+--------------------------------------+--------------------------------------+
-
-Flat BSON Decoding
-~~~~~~~~~~~~~~~~~~
-
-Summary: This benchmark tests driver performance decoding documents with
-top level key/value pairs involving the most commonly-used BSON types.
-
-Dataset: The dataset, designated FLAT\_BSON, will be synthetically
-generated and consist of an extended JSON document with a single
-\_id key with an object ID value plus 24 top level keys/value pairs of
-each of the following types: string, Int32, Int64, Double, Boolean.
-(121 total key/value pairs) Keys will be random ASCII strings of
-length 8. String data will be random ASCII strings of length 80.
-
-Dataset size: For score purposes, the dataset size for a task is the
-size of the single-document source file (7531 bytes) times 10,000
-operations, which equals 75,310,000 bytes or 75.31 MB.
-
-Phases:
-
-+--------------------------------------+--------------------------------------+
-| Setup | Load the FLAT\_BSON dataset into |
-| | memory as a language-appropriate |
-| | document types. For languages like |
-| | C without a document type, the raw |
-| | JSON string for each document should |
-| | be used instead. Encode it to a |
-| | BSON byte-string. |
-+--------------------------------------+--------------------------------------+
-| Before task | n/a |
-+--------------------------------------+--------------------------------------+
-| Do task | Decode the BSON byte-string to a |
-| | language-appropriate document type. |
-| | Repeat this 10,000 times. For |
-| | languages like C without a document |
-| | type, decode to extended JSON |
-| | instead. |
-+--------------------------------------+--------------------------------------+
-| After task | n/a |
-+--------------------------------------+--------------------------------------+
-| Teardown | n/a |
-+--------------------------------------+--------------------------------------+
-
-Deep BSON Encoding
-~~~~~~~~~~~~~~~~~~
-
-Summary: This benchmark tests driver performance encoding documents with
-deeply nested key/value pairs involving subdocuments, strings, integers,
-doubles and booleans.
-
-Dataset: The dataset, designated DEEP\_BSON (disk file 'deep\_bson.json'),
-will be synthetically generated and consist of an extended JSON document
-representing a balanced binary tree of depth 6, with "left" and "right" keys
-at each level containing a sub-document until the final level, which will
-contain a random ASCII string of length 8 (126 total key/value pairs).
-
-Dataset size: For score purposes, the dataset size for a task is the
-size of the single-document source file (1964 bytes) times 10,000
-operations, which equals 19,640,000 bytes or 19.64 MB.
-
-Phases:
-
-+--------------------------------------+--------------------------------------+
-| Setup | Load the DEEP\_BSON dataset into |
-| | memory as a language-appropriate |
-| | document type. For languages like C |
-| | without a document type, the raw |
-| | JSON string for each document should |
-| | be used instead. |
-+--------------------------------------+--------------------------------------+
-| Before task | n/a |
-+--------------------------------------+--------------------------------------+
-| Do task | Encode the DEEP\_BSON document to a |
-| | BSON byte-string. Repeat this 10,000 |
-| | times. |
-+--------------------------------------+--------------------------------------+
-| After task | n/a |
-+--------------------------------------+--------------------------------------+
-| Teardown | n/a |
-+--------------------------------------+--------------------------------------+
-
-Deep BSON Decoding
-~~~~~~~~~~~~~~~~~~
-
-Summary: This benchmark tests driver performance decoding documents with
-deeply nested key/value pairs involving subdocuments, strings, integers,
-doubles and booleans.
-
-Dataset: The dataset, designated DEEP\_BSON, will be synthetically generated
-and consist of an extended JSON document representing a balanced binary tree
-of depth 6, with "left" and "right" keys at each level containing a
-sub-document until the final level, which will contain a random ASCII string
-of length 8 (126 total key/value pairs).
-
-Dataset size: For score purposes, the dataset size for a task is the
-size of the single-document source file (1964 bytes) times 10,000
-operations, which equals 19,640,000 bytes or 19.64 MB.
-
-Phases:
-
-+--------------------------------------+--------------------------------------+
-| Setup | Load the DEEP\_BSON dataset into |
-| | memory as a language-appropriate |
-| | document types. For languages like |
-| | C without a document type, the raw |
-| | JSON string for each document should |
-| | be used instead. Encode it to a |
-| | BSON byte-string. |
-+--------------------------------------+--------------------------------------+
-| Before task | n/a |
-+--------------------------------------+--------------------------------------+
-| Do task | Decode the BSON byte-string to a |
-| | language-appropriate document type. |
-| | Repeat this 10,000 times. For |
-| | languages like C without a document |
-| | type, decode to extended JSON |
-| | instead. |
-+--------------------------------------+--------------------------------------+
-| After task | n/a |
-+--------------------------------------+--------------------------------------+
-| Teardown | n/a |
-+--------------------------------------+--------------------------------------+
-
-Full BSON Encoding
-~~~~~~~~~~~~~~~~~~
-
-Summary: This benchmark tests driver performance encoding documents with
-top level key/value pairs involving the full range of BSON types.
-
-Dataset: The dataset, designated FULL\_BSON (disk file 'full\_bson.json'),
-will be synthetically generated and consist of an extended JSON document with
-a single \_id key with an object ID value plus 6 each of the following types:
-string, double, Int64, Int32, boolean, minkey, maxkey, array, binary data, UTC
-datetime, regular expression, Javascript code, Javascript code with context,
-and timestamp. (91 total keys.) Keys (other than \_id) will be random ASCII
-strings of length 8. Strings values will be random ASCII strings with length
-80.
-
-Dataset size: For score purposes, the dataset size for a task is the
-size of the single-document source file (5734 bytes) times 10,000
-operations, which equals 57,340,000 bytes or 57.34 MB.
-
-Phases:
-
-+--------------------------------------+--------------------------------------+
-| Setup | Load the FULL\_BSON dataset into |
-| | memory as a language-appropriate |
-| | document type. For languages like |
-| | C without a document type, the raw |
-| | JSON string for each document should |
-| | be used instead. |
-+--------------------------------------+--------------------------------------+
-| Before task | n/a |
-+--------------------------------------+--------------------------------------+
-| Do task | Encode the FULL\_BSON document to a |
-| | BSON byte-string. Repeat this 10,000 |
-| | times. |
-+--------------------------------------+--------------------------------------+
-| After task | n/a |
-+--------------------------------------+--------------------------------------+
-| Teardown | n/a |
-+--------------------------------------+--------------------------------------+
-
-Full BSON Decoding
-~~~~~~~~~~~~~~~~~~
-
-Summary: This benchmark tests driver performance decoding documents with
-top level key/value pairs involving the full range of BSON types.
-
-Dataset: The dataset, designated FULL\_BSON, will be synthetically
-generated and consist of an extended JSON document with a single
-\_id key with an object ID value plus 6 each of the following types:
-string, double, Int64, Int32, boolean, minkey, maxkey, array, binary
-data, UTC datetime, regular expression, Javascript code, Javascript code
-with context, and timestamp. (91 total keys.) Keys (other than \_id)
-will be random ASCII strings of length 8. Strings values will be random
-ASCII strings with length 80.
-
-Dataset size: For score purposes, the dataset size for a task is the
-size of the single-document source file (5734 bytes) times 10,000
-operations, which equals 57,340,000 bytes or 57.34 MB.
-
-Phases:
-
-+--------------------------------------+--------------------------------------+
-| Setup | Load the FULL\_BSON dataset into |
-| | memory as a language-appropriate |
-| | document types. For languages like |
-| | C without a document type, the raw |
-| | JSON string for each document should |
-| | be used instead. Encode it to a |
-| | BSON byte-string. |
-+--------------------------------------+--------------------------------------+
-| Before task | n/a |
-+--------------------------------------+--------------------------------------+
-| Do task | Decode the BSON byte-string to a |
-| | language-appropriate document type. |
-| | Repeat this 10,000 times. For |
-| | languages like C without a document |
-| | type, decode to extended JSON |
-| | instead. |
-+--------------------------------------+--------------------------------------+
-| After task | n/a |
-+--------------------------------------+--------------------------------------+
-| Teardown | n/a |
-+--------------------------------------+--------------------------------------+
-
-Single-Doc Benchmarks
----------------------
-
-Datasets are in the 'single\_and\_multi\_document' tarball.
-
-Single-doc tests focus on single-document read and write operations.
-They are designed to give insights into the efficiency of the driver's
-implementation of the basic wire protocol.
-
-The data will be stored as strict JSON with no extended types.
-
-Single-doc micro-benchmarks include:
-
-- Run command
-- Find one by ID
-- Small doc insertOne
-- Large doc insertOne
-
-Run command
-~~~~~~~~~~~
-
-Summary: This benchmark tests driver performance sending a command to
-the database and reading a response.
-
-Dataset: n/a
-
-Dataset size: While there is no external dataset, for score calculation
-purposes use 160,000 bytes (10,000 x the size of a BSON {ismaster:true}
-command).
-
-*N.B. We use {ismaster:true} rather than {ismaster:1}
-to ensure a consistent command size.*
-
-Phases:
-
-+--------------------------------------+--------------------------------------+
-| Setup | Construct a MongoClient object. |
-| | Construct whatever |
-| | language-appropriate objects |
-| | (Database, etc.) would be required |
-| | to send a command. |
-+--------------------------------------+--------------------------------------+
-| Before task | n/a |
-+--------------------------------------+--------------------------------------+
-| Do task | Run the command {ismaster:true} |
-| | 10,000 times, reading (and |
-| | discarding) the result each time. |
-+--------------------------------------+--------------------------------------+
-| After task | n/a |
-+--------------------------------------+--------------------------------------+
-| Teardown | n/a |
-+--------------------------------------+--------------------------------------+
-
-Find one by ID
-~~~~~~~~~~~~~~
-
-Summary: This benchmark tests driver performance sending an indexed
-query to the database and reading a single document in response.
-
-Dataset: The dataset, designated TWEET (disk file 'tweet.json'), consists of
-a sample tweet stored as strict JSON.
-
-Dataset size: For score purposes, the dataset size for a task is the
-size of the single-document source file (1622 bytes) times 10,000
-operations, which equals 16,220,000 bytes or 16.22 MB.
-
-Phases:
-
-+--------------------------------------+--------------------------------------+
-| Setup | Construct a MongoClient object. Drop |
-| | the 'perftest' database. Load the |
-| | TWEET document into memory as a |
-| | language-appropriate document type |
-| | (or JSON string for C). Construct a |
-| | Collection object for the 'corpus' |
-| | collection to use for querying. |
-| | Insert the document 10,000 times to |
-| | the 'perftest' database in the |
-| | 'corpus' collection using sequential |
-| | \_id values. (1 to 10,000) |
-+--------------------------------------+--------------------------------------+
-| Before task | n/a |
-+--------------------------------------+--------------------------------------+
-| Do task | For each of the 10,000 |
-| | sequential \_id numbers, issue a |
-| | find command for that \_id on the |
-| | 'corpus' collection and retrieve the |
-| | single-document result. |
-+--------------------------------------+--------------------------------------+
-| After task | n/a |
-+--------------------------------------+--------------------------------------+
-| Teardown | Drop the 'perftest' database. |
-+--------------------------------------+--------------------------------------+
-
-Small doc insertOne
-~~~~~~~~~~~~~~~~~~~
-
-Summary: This benchmark tests driver performance inserting a single,
-small document to the database.
-
-Dataset: The dataset, designated SMALL\_DOC (disk file 'small\_doc.json'),
-consists of a JSON document with an encoded length of approximately 250 bytes.
-
-Dataset size: For score purposes, the dataset size for a task is the
-size of the single-document source file (275 bytes) times 10,000
-operations, which equals 2,750,000 bytes or 2.75 MB.
-
-Phases:
-
-+--------------------------------------+--------------------------------------+
-| Setup | Construct a MongoClient object. Drop |
-| | the 'perftest' database. Load the |
-| | SMALL\_DOC dataset into memory as a |
-| | language-appropriate document type |
-| | (or JSON string for C). |
-+--------------------------------------+--------------------------------------+
-| Before task | Drop the 'corpus' collection. |
-| | Create an empty 'corpus' collection |
-| | with the 'create' command. |
-| | Construct a Collection object for |
-| | the 'corpus' collection to use for |
-| | insertion. |
-+--------------------------------------+--------------------------------------+
-| Do task | Insert the document with the |
-| | insertOne CRUD method. |
-| | DO NOT manually add an \_id field; |
-| | leave it to the driver or |
-| | database. Repeat this 10,000 times. |
-+--------------------------------------+--------------------------------------+
-| After task | n/a |
-+--------------------------------------+--------------------------------------+
-| Teardown | Drop the 'perftest' database. |
-+--------------------------------------+--------------------------------------+
-
-Large doc insertOne
-~~~~~~~~~~~~~~~~~~~
-
-Summary: This benchmark tests driver performance inserting a single,
-large document to the database.
-
-Dataset: The dataset, designated LARGE\_DOC (disk file 'large\_doc.json'),
-consists of a JSON document with an encoded length of approximately 2,500,000
-bytes.
-
-Dataset size: For score purposes, the dataset size for a task is the
-size of the single-document source file (2,731,089 bytes) times 10
-operations, which equals 27,310,890 bytes or 27.31 MB.
-
-Phases:
-
-+--------------------------------------+--------------------------------------+
-| Setup | Construct a MongoClient object. Drop |
-| | the 'perftest' database. Load the |
-| | LARGE\_DOC dataset into memory as a |
-| | language-appropriate document type |
-| | (or JSON string for C). |
-+--------------------------------------+--------------------------------------+
-| Before task | Drop the 'corpus' collection. |
-| | Create an empty 'corpus' collection |
-| | with the 'create' command. |
-| | Construct a Collection object for |
-| | the 'corpus' collection to use for |
-| | insertion. |
-+--------------------------------------+--------------------------------------+
-| Do task | Insert the document with the |
-| | insertOne CRUD method. |
-| | DO NOT manually add an \_id field; |
-| | leave it to the driver or database. |
-| | Repeat this 10 times. |
-+--------------------------------------+--------------------------------------+
-| After task | n/a |
-+--------------------------------------+--------------------------------------+
-| Teardown | Drop the 'perftest' database. |
-+--------------------------------------+--------------------------------------+
-
-Multi-Doc Benchmarks
---------------------
-
-Datasets are in the 'single\_and\_multi\_document' tarball.
-
-Multi-doc benchmarks focus on multiple-document read and write
-operations. They are designed to give insight into the efficiency of
-the driver's implementation of bulk/batch operations such as bulk writes
-and cursor reads.
-
-Multi-doc micro-benchmarks include:
-
-- Find many and empty the cursor
-- Small doc bulk insert
-- Large doc bulk insert
-- GridFS upload
-- GridFS download
-
-Find many and empty the cursor
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Summary: This benchmark tests driver performance retrieving multiple
-documents from a query.
-
-Dataset: The dataset, designated TWEET consists of a sample tweet
-stored as strict JSON.
-
-Dataset size: For score purposes, the dataset size for a task is the
-size of the single-document source file (1622 bytes) times 10,000
-operations, which equals 16,220,000 bytes or 16.22 MB.
-
-Phases:
-
-+--------------------------------------+--------------------------------------+
-| Setup | Construct a MongoClient object. Drop |
-| | the 'perftest' database. Load the |
-| | TWEET dataset into memory as a |
-| | language-appropriate document type |
-| | (or JSON string for C). Construct a |
-| | Collection object for the 'corpus' |
-| | collection to use for |
-| | querying. Insert the document 10,000 |
-| | times to the 'perftest' database in |
-| | the 'corpus' collection. (Let the |
-| | driver generate \_ids). |
-+--------------------------------------+--------------------------------------+
-| Before task | n/a |
-+--------------------------------------+--------------------------------------+
-| Do task | Issue a find command on the 'corpus' |
-| | collection with an empty filter |
-| | expression. Retrieve (and discard) |
-| | all documents from the cursor. |
-+--------------------------------------+--------------------------------------+
-| After task | n/a |
-+--------------------------------------+--------------------------------------+
-| Teardown | Drop the 'perftest' database. |
-+--------------------------------------+--------------------------------------+
-
-Small doc bulk insert
-~~~~~~~~~~~~~~~~~~~~~
-
-Summary: This benchmark tests driver performance inserting multiple,
-small documents to the database.
-
-Dataset: The dataset, designated SMALL\_DOC consists of a JSON document
-with an encoded length of approximately 250 bytes.
-
-Dataset size: For score purposes, the dataset size for a task is the
-size of the single-document source file (275 bytes) times 10,000
-operations, which equals 2,750,000 bytes or 2.75 MB.
-
-Phases:
-
-+--------------------------------------+--------------------------------------+
-| Setup | Construct a MongoClient object. Drop |
-| | the 'perftest' database. Load the |
-| | SMALL\_DOC dataset into memory as a |
-| | language-appropriate document type |
-| | (or JSON string for C). |
-+--------------------------------------+--------------------------------------+
-| Before task | Drop the 'corpus' collection. Create |
-| | an empty 'corpus' collection with |
-| | the 'create' command. Construct a |
-| | Collection object for the 'corpus' |
-| | collection to use for insertion. |
-+--------------------------------------+--------------------------------------+
-| Do task | Do an ordered 'insert\_many' with |
-| | 10,000 copies of the document. |
-| | DO NOT manually add an \_id field; |
-| | leave it to the driver or database. |
-| | |
-+--------------------------------------+--------------------------------------+
-| After task | n/a |
-+--------------------------------------+--------------------------------------+
-| Teardown | Drop the 'perftest' database. |
-+--------------------------------------+--------------------------------------+
-
-Large doc bulk insert
-~~~~~~~~~~~~~~~~~~~~~
-
-Summary: This benchmark tests driver performance inserting multiple,
-large documents to the database.
-
-Dataset: The dataset, designated LARGE\_DOC consists of a JSON document
-with an encoded length of approximately 2,500,000 bytes.
-
-Dataset size: For score purposes, the dataset size for a task is the
-size of the single-document source file (2,731,089 bytes) times 10
-operations, which equals 27,310,890 bytes or 27.31 MB.
-
-Phases:
-
-+--------------------------------------+--------------------------------------+
-| Setup | Construct a MongoClient object. Drop |
-| | the 'perftest' database. Load the |
-| | LARGE\_DOC dataset into memory as a |
-| | language-appropriate document type |
-| | (or JSON string for C). |
-+--------------------------------------+--------------------------------------+
-| Before task | Drop the 'corpus' collection. Create |
-| | an empty 'corpus' collection with |
-| | the 'create' command. Construct a |
-| | Collection object for the 'corpus' |
-| | collection to use for insertion. |
-+--------------------------------------+--------------------------------------+
-| Do task | Do an ordered 'insert\_many' with 10 |
-| | copies of the document. |
-| | DO NOT manually add an \_id field; |
-| | leave it to the driver or database. |
-| | |
-+--------------------------------------+--------------------------------------+
-| After task | n/a |
-+--------------------------------------+--------------------------------------+
-| Teardown | Drop the 'perftest' database. |
-+--------------------------------------+--------------------------------------+
-
-GridFS upload
-~~~~~~~~~~~~~
-
-Summary: This benchmark tests driver performance uploading a GridFS file
-from memory.
-
-Dataset: The dataset, designated GRIDFS\_LARGE (disk file
-'gridfs\_large.bin'), consists of a single file containing about 50 MB of
-random data. We use a large file to ensure multiple database round-trips even
-if chunks are are sent in batches.
-
-Dataset size: For score purposes, the dataset size for a task is the
-size of the source file (52,428,800 bytes) times 1 operation or 52.43
-MB.
-
-Phases:
-
-+--------------------------------------+--------------------------------------+
-| Setup | Construct a MongoClient object. |
-| | Drop the 'perftest' database. Load |
-| | the GRIDFS\_LARGE file as a string |
-| | or other language-appropriate type |
-| | for binary octet data. |
-+--------------------------------------+--------------------------------------+
-| Before task | Drop the default GridFS bucket. |
-| | Insert a 1-byte file into the |
-| | bucket. (This ensures the bucket |
-| | collections and indices have been |
-| | created.) |
-| | Construct |
-| | a GridFSBucket object to use for |
-| | uploads. |
-+--------------------------------------+--------------------------------------+
-| Do task | Upload the GRIDFS\_LARGE data as a |
-| | GridFS file. Use whatever upload |
-| | API is most natural for each |
-| | language (e.g. |
-| | open\_upload\_stream(), write the |
-| | data to the stream and close the |
-| | stream). |
-+--------------------------------------+--------------------------------------+
-| After task | n/a |
-+--------------------------------------+--------------------------------------+
-| Teardown | Drop the 'perftest' database. |
-+--------------------------------------+--------------------------------------+
-
-GridFS download
-~~~~~~~~~~~~~~~
-
-Summary: This benchmark tests driver performance downloading a GridFS
-file to memory.
-
-Dataset: The dataset, designated GRIDFS\_LARGE, consists of a single
-file containing about 50 MB of random data. We use a large file to
-ensure multiple database round-trips even if chunks are are sent in
-batches.
-
-Dataset size: For score purposes, the dataset size for a task is the
-size of the source file (52,428,800 bytes) times 1 operation or 52.43
-MB.
-
-Phases:
-
-+--------------------------------------+--------------------------------------+
-| Setup | Construct a MongoClient object. |
-| | Drop the 'perftest' database. |
-| | Upload the GRIDFS\_LARGE file to |
-| | the default gridFS bucket with the |
-| | name "gridfstest". Record the |
-| | \_id of the uploaded file. |
-+--------------------------------------+--------------------------------------+
-| Before task | Construct a GridFSBucket object to |
-| | use for downloads. |
-+--------------------------------------+--------------------------------------+
-| Do task | Download the "gridfstest" file by |
-| | its \_id. Use whatever download API |
-| | is most natural for each language |
-| | (e.g. open\_download\_stream(), read |
-| | from the stream into a variable). |
-| | Discard the downloaded data. |
-+--------------------------------------+--------------------------------------+
-| After task | n/a |
-+--------------------------------------+--------------------------------------+
-| Teardown | Drop the 'perftest' database. |
-+--------------------------------------+--------------------------------------+
-
-Parallel
---------
-
-Datasets are in the 'parallel' tarball.
-
-Parallel tests simulate ETL operations from disk to database or
-vice-versa. They are designed to be implemented using a language's
-preferred approach to concurrency and thus stress how drivers handle
-concurrency. These intentionally involve overhead above and beyond the
-driver itself to simulate -- however loosely -- the sort of "real-world"
-pressures that a drivers would be under during concurrent operation.
-
-They are intended for directional indication of which languages perform
-best for this sort of pseudo-real-world activity, but are not intended
-to represent real-world performance claims.
-
-Drivers teams are expected to treat these as a competitive "shoot-out"
-to surface optimal ETL patterns for each language (e.g. multi-thread,
-multi-process, asynchronous I/O, etc.).
-
-Parallel micro-benchmarks include:
-
-- LDJSON multi-file import
-- LDJSON multi-file export
-- GridFS multi-file upload
-- GridFS multi-file download
-
-LDJSON multi-file import
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-Summary: This benchmark tests driver performance importing documents
-from a set of LDJSON files.
-
-Dataset: The dataset, designated LDJSON\_MULTI (disk directory
-'ldjson\_multi'), consists of 100 LDJSON files, each containing 5,000 JSON
-documents. Each document should be about 1000 bytes.
-
-Dataset size: For score purposes, the dataset size for a task is the
-total size of all source files: 565,000,000 bytes or 565 MB.
-
-Phases:
-
-+--------------------------------------+--------------------------------------+
-| Setup | Construct a MongoClient object. |
-| | Drop the 'perftest' database. |
-| | |
-+--------------------------------------+--------------------------------------+
-| Before task | Drop the 'corpus' collection. |
-| | Create an empty 'corpus' collection |
-| | with the 'create' command. |
-+--------------------------------------+--------------------------------------+
-| Do task | Do an unordered insert of all |
-| | 500,000 documents in the dataset |
-| | into the 'corpus' collection as fast |
-| | as possible. Data must be loaded |
-| | from disk during this phase. |
-| | Concurrency is encouraged. |
-+--------------------------------------+--------------------------------------+
-| After task | n/a |
-+--------------------------------------+--------------------------------------+
-| Teardown | Drop the 'perftest' database. |
-+--------------------------------------+--------------------------------------+
-
-LDJSON multi-file export
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-Summary: This benchmark tests driver performance exporting documents to
-a set of LDJSON files.
-
-Dataset: The dataset, designated LDJSON\_MULTI, consists of 100 LDJSON
-files, each containing 5,000 JSON documents. Each document should be
-about 1000 bytes.
-
-Dataset size: For score purposes, the dataset size for a task is the
-total size of all source files: 565,000,000 bytes or 565 MB.
-
-Phases:
-
-+--------------------------------------+--------------------------------------+
-| Setup | Construct a MongoClient object. |
-| | Drop the 'perftest' database. Drop |
-| | the 'corpus' collection. Do an |
-| | unordered insert of all 500,000 |
-| | documents in the dataset into the |
-| | 'corpus' collection. |
-| | |
-+--------------------------------------+--------------------------------------+
-| Before task | Construct whatever objects, threads, |
-| | etc. are required for exporting the |
-| | dataset. |
-| | |
-+--------------------------------------+--------------------------------------+
-| Do task | Dump all 500,000 documents in the |
-| | dataset into 100 LDJSON files of |
-| | 5,000 documents each as fast as |
-| | possible. Data must be completely |
-| | written/flushed to disk during this |
-| | phase. Concurrency is encouraged. |
-| | The order and distribution of |
-| | documents across files does not need |
-| | to match the original LDJSON\_MULTI |
-| | files. |
-+--------------------------------------+--------------------------------------+
-| After task | n/a |
-+--------------------------------------+--------------------------------------+
-| Teardown | Drop the 'perftest' database. |
-+--------------------------------------+--------------------------------------+
-
-GridFS multi-file upload
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-Summary: This benchmark tests driver performance uploading files from
-disk to GridFS.
-
-Dataset: The dataset, designated GRIDFS\_MULTI (disk directory
-'gridfs\_multi'), consists of 50 files, each of 5MB. This file size
-corresponds roughly to the output of a (slightly dated) digital camera. Thus
-the task approximates uploading 50 "photos".
-
-Dataset size: For score purposes, the dataset size for a task is the
-total size of all source files: 262,144,000 bytes or 262.144 MB.
-
-Phases:
-
-+--------------------------------------+--------------------------------------+
-| Setup | Construct a MongoClient object. |
-| | Drop the 'perftest' database. |
-+--------------------------------------+--------------------------------------+
-| Before task | Drop the default GridFS bucket in |
-| | the 'perftest' database. Construct |
-| | a GridFSBucket object for the |
-| | default bucket in 'perftest' to use |
-| | for uploads. Insert a 1-byte file |
-| | into the bucket (to initialize |
-| | indexes). |
-+--------------------------------------+--------------------------------------+
-| Do task | Upload all 50 files in the |
-| | GRIDFS\_MULTI dataset (reading each |
-| | from disk). Concurrency is |
-| | encouraged. |
-+--------------------------------------+--------------------------------------+
-| After task | n/a |
-+--------------------------------------+--------------------------------------+
-| Teardown | Drop the 'perftest' database. |
-+--------------------------------------+--------------------------------------+
-
-GridFS multi-file download
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Summary: This benchmark tests driver performance downloading files from
-GridFS to disk.
-
-Dataset: The dataset, designated GRIDFS\_MULTI, consists of 50 files,
-each of 5MB. This file size corresponds roughly to the output of a
-(slightly dated) digital camera. Thus the task approximates downloading
-50 "photos".
-
-Dataset size: For score purposes, the dataset size for a task is the
-total size of all source files: 262,144,000 bytes or 262.144 MB.
-
-Phases:
-
-+--------------------------------------+--------------------------------------+
-| Setup | Construct a MongoClient object. |
-| | Drop the 'perftest' database. |
-| | Construct a temporary directory for |
-| | holding downloads. Drop the default |
-| | GridFS bucket in the 'perftest' |
-| | database. Upload the 50 file |
-| | dataset to the default GridFS bucket |
-| | in 'perftest'. |
-+--------------------------------------+--------------------------------------+
-| Before task | Delete all files in the temporary |
-| | folder for downloads. Construct a |
-| | GridFSBucket object to use for |
-| | downloads from the default bucket in |
-| | 'perftest'. |
-+--------------------------------------+--------------------------------------+
-| Do task | Download all 50 files in the |
-| | GRIDFS\_MULTI dataset, saving each |
-| | to a file in the temporary folder |
-| | for downloads. Data must be |
-| | completely written/flushed to disk |
-| | during this phase. Concurrency is |
-| | encouraged. |
-+--------------------------------------+--------------------------------------+
-| After task | n/a |
-+--------------------------------------+--------------------------------------+
-| Teardown | Drop the 'perftest' database. |
-+--------------------------------------+--------------------------------------+
-
-Composite score calculation
-===========================
-
-Every micro-benchmark has a score equal to the 50th percentile (median)
-of sampled timings, expressed as Megabytes (1,000,000 bytes) per second
-where the micro-benchmark "database size" given in each section above is
-divided by the 50th percentile of measured wall-clock times.
-
-From these micro-benchmarks, the following composite scores must be
-calculated:
-
-+--------------------------------------+--------------------------------------+
-| Composite name | Compositing formula |
-+--------------------------------------+--------------------------------------+
-| BSONBench | Average of all BSON micro-benchmarks |
-+--------------------------------------+--------------------------------------+
-| SingleBench | Average of all Single-doc |
-| | micro-benchmarks, except "Run |
-| | Command" |
-+--------------------------------------+--------------------------------------+
-| MultiBench | Average of all Multi-doc |
-| | micro-benchmarks |
-+--------------------------------------+--------------------------------------+
-| ParallelBench | Average of all Parallel |
-| | micro-benchmarks |
-+--------------------------------------+--------------------------------------+
-| ReadBench | Average of "Find one", "Find many |
-| | and empty cursor", "GridFS |
-| | download", "LDJSON multi-file |
-| | export", and "GridFS multi-file |
-| | download" microbenchmarks |
-+--------------------------------------+--------------------------------------+
-| WriteBench | Average of "Small doc insertOne", |
-| | "Large doc insertOne", "Small doc |
-| | bulk insert", "Large doc bulk |
-| | insert", "GridFS upload", "LDJSON |
-| | multi-file import", and "GridFS |
-| | multi-file upload" micro-benchmarks |
-+--------------------------------------+--------------------------------------+
-| DriverBench | Average of ReadBench and WriteBench |
-+--------------------------------------+--------------------------------------+
-
-At least for this first DriverBench version, scores are combined with
-simple averages. In addition, the BSONBench scores do not factor into
-the overall DriverBench scores, as encoding and decoding are inherent in
-all other tasks.
-
-Benchmark platform, configuration and environments
-==================================================
-
-Benchmark Client
-----------------
-
-TBD: spec Amazon instance size; describe in general terms how language
-clients will be run independently; same AWS zone as server
-
-All operations must be run with write concern "w:1".
-
-Benchmark Server
-----------------
-
-TBD: spec Amazon instance size; describe configuration (e.g. no auth,
-journal, pre-alloc sizes?, WT with compression to minimize disk I/O
-impact?); same AWS zone as client
-
-Score Server
-------------
-
-TBD: spec system to hold scores over time
-
-Datasets
---------
-
-TBD: generated datasets should be park in S3 or somewhere for retrieval
-by URL
-
-FAQ
-===
-
-Question?
-
-Answer.
-
-Change log
-==========
-
-V1.3 (Aug 13, 2016)
-
-- Update corpus files to allow much greater compression of data
-- Updated LDJSON corpus size to reflect revisions to the test data
-- Published data files on GitHub and updated instructions on how to
- find datasets
-- RunCommand and query benchmark can create collection objects during
- setup rather than before task. (No change on actual benchmark.)
-
-v1.2 (Jan 6, 2016)
-
-- Clarify that 'bulk insert' means 'insert\_many'
-- Clarify that "create a collection" means using the 'create' command
-- Add omitted "upload files" step to setup for GridFS multi-file
- download; also clarify that steps should be using the default bucket
- in the 'perftest' database
-
-v1.1 (Dec 23, 2015)
-
-- Rename benchmark names away from MMA/weight class names
-- Split BSON encoding and decoding micro-benchmarks
-- Rename BSON micro-benchmarks to better match dataset names
-- Move "Run Command" micro-benchmark out of composite
-- Reduced amount of data held in memory and sent to/from the server to
- decrease memory pressure and increase number of iterations in a
- reasonable time (e.g. file sizes and number of documents in certain
- datasets changed)
-- Create empty collections/indexes during the 'before' phase when
- appropriate
-- Updated data set sizes to account for changes in the source file
- structure/size
+.. note::
+ This specification has been converted to Markdown and renamed to
+ `benchmarking.md `_.
diff --git a/source/benchmarking/data/extended_bson.tgz b/source/benchmarking/data/extended_bson.tgz
index aec31638c6..1e0950cec3 100644
Binary files a/source/benchmarking/data/extended_bson.tgz and b/source/benchmarking/data/extended_bson.tgz differ
diff --git a/source/benchmarking/data/extended_bson_legacy.tgz b/source/benchmarking/data/extended_bson_legacy.tgz
new file mode 100644
index 0000000000..aec31638c6
Binary files /dev/null and b/source/benchmarking/data/extended_bson_legacy.tgz differ
diff --git a/source/benchmarking/odm-benchmarking.md b/source/benchmarking/odm-benchmarking.md
new file mode 100644
index 0000000000..41103e10c7
--- /dev/null
+++ b/source/benchmarking/odm-benchmarking.md
@@ -0,0 +1,397 @@
+# ODM Performance Benchmarking
+
+- Version: 1.0
+- Status: In progress
+- Minimum Server Version: N/A
+
+## Abstract
+
+This document describes a standard benchmarking suite for MongoDB ODMs (Object Document Mappers). Much of this
+document's structure and content is taken from the existing MongoDB driver benchmarking suite for consistency.
+
+## Overview
+
+### Name and purpose
+
+ODM performance will be measured by the MongoDB ODM Performance Benchmark. It will provide both "horizontal" insights
+into how individual ODM performance evolves over time and two types of "vertical" insights: the relative performance of
+different ODMs, and the relative performance of ODMs and their associated language drivers.
+
+We expect substantial performance differences between ODMs based on both their language families (e.g. static vs.
+dynamic or compiled vs. virtual-machine-based) as well as their inherent design (e.g. web frameworks such as Django vs.
+application-agnostic such as Mongoose). Within families of ODMs that use similar design and language families, these
+comparisons could be used to identify potential areas of performance improvement.
+
+### Task Hierarchy
+
+The benchmark suite consists of two groups of small, independent benchmarks. This allows us to better isolate areas
+within ODMs that are faster or slower.
+
+- Flat models -- reading and writing flat models of various sizes, to explore basic operation efficiency
+- Nested models -- reading and writing nested models of various sizes, to explore basic operation efficiency for complex
+ data
+
+The suite is intentionally kept small for several reasons:
+
+- ODM feature sets vary significantly across libraries, limiting the number of benchmarks that can be run across the
+ entire collection of extant ODMs.
+- Several popular MongoDB ODMs are actively maintained by third-parties, such as Mongoose. By limiting the benchmarking
+ suite to a minimal set of representative tests that are easy to implement, we encourage adoption of the suite by
+ these third-party maintainers.
+
+### Measurement
+
+In addition to latency data, all benchmark tasks will be measured in terms of "megabytes/second" (MB/s) of documents
+processed, with higher scores being better. (In this document, "megabyte" refers to the SI decimal unit, i.e. 1,000,000
+bytes.) This makes cross-benchmark comparisons easier.
+
+To avoid various types of measurement skew, tasks will be measured over several iterations. Each iteration will have a
+number of operations performed per iteration that depends on the task being benchmarked. The final score for a task will
+be the median score of the iterations. A range of percentiles will also be recorded for diagnostic analysis.
+
+### Data sets
+
+Data sets will vary by task. In most cases, data sets will be synthetic line-delimited JSON files to be constructed by
+the ODM being benchmarked into the appropriate model. Some tasks will require additional modifications to these
+constructed models, such as adding generated ObjectIds. See the
+[Benchmark task definitions](#benchmark-task-definitions) section for details.
+
+### Versioning
+
+The MongoDB ODM Performance Benchmark will have vX.Y versioning. Minor updates and clarifications will increment "Y" and
+MUST have little impact on score comparison. Major changes, such as task modifications, MongoDB version tested against,
+or hardware used, MUST increment "X" to indicate that older version scores are unlikely to be comparable.
+
+## Benchmark execution phases and measurement
+
+All benchmark tasks will be conducted via a number of iterations. Each iteration will be timed and will generally
+include a large number of individual ODM operations.
+
+The measurement is broken up this way to better isolate the benchmark from external volatility. If we consider the
+problem of benchmarking an operation over many iterations, such as 100,000 model insertions, we want to avoid two
+extreme forms of measurement:
+
+- measuring a single insertion 100,000 times -- in this case, the timing code is likely to be a greater proportion of
+ executed code, which could routinely evict the insertion code from CPU caches or mislead a JIT optimizer and throw
+ off results
+- measuring 100,000 insertions one time -- in this case, the longer the timer runs, the higher the likelihood that an
+ external event occurs that affects the time of the run
+
+Therefore, we choose a middle ground:
+
+- measuring the same 10,000 insertions over 10 iterations -- each timing run includes enough operations that insertion
+ code dominates timing code; unusual system events are likely to affect only a fraction of the 10 timing measurements
+
+With 10 timings of inserting the same 10,000 models, we build up a statistical distribution of the operation timing,
+allowing a more robust estimate of performance than a single measurement. (In practice, the number of iterations could
+exceed 10, but 10 is a reasonable minimum goal.)
+
+Because a timing distribution is bounded by zero on one side, taking the mean would allow large positive outlier
+measurements to skew the result substantially. Therefore, for the benchmark score, we use the median timing measurement,
+which is robust in the face of outliers.
+
+Each benchmark is structured into discrete setup/execute/teardown phases. Phases are as follows, with specific details
+given in a subsequent section:
+
+- setup -- (ONCE PER TASK) something to do once before any benchmarking, e.g. construct a model object, load test data,
+ insert data into a collection, etc.
+- before operation -- (ONCE PER ITERATION) something to do before every task iteration, e.g. drop a collection, or
+ reload test data (if the test run modifies it), etc.
+- do operation -- (ONCE PER ITERATION) smallest amount of code necessary to execute the task; e.g. insert 10,000 models
+ one by one into the database, or retrieve 10,000 models of test data from the database, etc.
+- after operation -- (ONCE PER ITERATION) something to do after every task iteration (if necessary)
+- teardown -- (ONCE PER TASK) something done once after all benchmarking is complete (if necessary); e.g. drop the test
+ database
+
+The wall-clock execution time of each "do operation" phase will be recorded. We use wall clock time to model user
+experience and as a lowest-common denominator across ODMs. Iteration timing SHOULD be done with a high-resolution
+monotonic timer (or best language approximation).
+
+Unless otherwise specified, the number of iterations to measure per task is variable:
+
+- iterations MUST loop for at least 30 seconds cumulative execution time
+- once this 30 second minimum execution time is reached, iterations SHOULD stop after at least 10 iterations or 1 minute
+ cumulative execution time, whichever is shorter
+
+This balances measurement stability with a timing cap to ensure all tasks can complete in a reasonable time.
+
+For each task, the 10th, 25th, 50th, 75th, 90th, 95th, 98th and 99th percentiles will be recorded using the following
+algorithm:
+
+- Given a 0-indexed array `A` of `N` iteration wall clock times
+- Sort the array into ascending order (i.e. shortest time first)
+- Let the index `i` for percentile `p` in the range [1,100] be defined as: `i = int(N * p / 100) - 1`
+
+*N.B. This is the [Nearest Rank](https://en.wikipedia.org/wiki/Percentile#The_nearest-rank_method) algorithm, chosen for
+its utter simplicity given that it needs to be implemented identically across a wide variety of ODMs and languages.*
+
+The 50th percentile (i.e. the median) will be used for score composition. Other percentiles will be stored for
+visualizations and analysis.
+
+Each task will have defined for it an associated size in megabytes (MB). This size will be calculated using the task's
+dataset size and the number of documents processed per iteration. The benchmarking score for each task will be the task
+size in MB divided by the median wall clock time.
+
+## Benchmark task definitions
+
+Datasets are available in the `odm-data` directory adjacent to this spec.
+
+Note: The term "LDJSON" means "line-delimited JSON", which should be understood to mean a collection of UTF-8 encoded
+JSON documents (without embedded CR or LF characters), separated by a single LF character. (Some Internet definition of
+line-delimited JSON use CRLF delimiters, but this benchmark uses only LF.)
+
+### Flat models
+
+Datasets are in the `flat_models.tgz` tarball.
+
+Flat model tests focus on flatly-structured model reads and writes across data sizes. They are designed to give insights
+into the efficiency of the ODM's implementation of basic data operations.
+
+The data will be stored as strict JSON with no extended types. These JSON representations MUST be converted into
+equivalent models as part of each benchmark task.
+
+Flat model benchmark tasks include:
+
+- Small model creation
+- Small model update
+- Small model find by filter
+- Small model find foreign key by filter (if joins are supported)
+- Large model creation
+- Large model update
+
+#### Small model creation
+
+Summary: This benchmark tests ODM performance creating a single small model.
+
+Dataset: The dataset (SMALL_DOC) is contained within `small_doc.json` and consists of a sample document stored as strict
+JSON with an encoded length of approximately 250 bytes.
+
+Dataset size: For scoring purposes, the dataset size is the size of the `small_doc` source file (250 bytes) times 10,000
+operations, which equals 2,250,000 bytes or 2.5 MB.
+
+This benchmark uses a comparable dataset to the driver `small doc insertOne` benchmark, allowing for direct comparisons.
+
+| Phase | Description |
+| ----------- | -------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Load the SMALL_DOC dataset into memory. |
+| Before task | n/a. |
+| Do task | Create an ODM-appropriate model instance for the SMALL_DOC document and save it to the database. Repeat this 10,000 times. |
+| After task | Drop the collection associated with the SMALL_DOC model. |
+| Teardown | n/a. |
+
+#### Small model update
+
+Summary: This benchmark tests ODM performance updating fields on a single small model.
+
+Dataset: The dataset (SMALL_DOC) is contained within `small_doc.json` and consists of a sample document stored as strict
+JSON with an encoded length of approximately 250 bytes.
+
+Dataset size: For scoring purposes, the dataset size is the size of the `updated_value` string file (13 bytes) times
+10,000 operations, which equals 130,000 bytes or 130 KB.
+
+| Phase | Description |
+| ----------- | ------------------------------------------------------------------------------------------------------------------- |
+| Setup | Load the SMALL_DOC dataset into memory as an ODM-appropriate model object. Save 10,000 instances into the database. |
+| Before task | n/a. |
+| Do task | Update the `field1` field for each instance of the model to equal `updated_value` in an ODM-appropriate manner. |
+| After task | n/a. |
+| Teardown | Drop the collection associated with the SMALL_DOC model. |
+
+#### Small model find by filter
+
+Summary: This benchmark tests ODM performance finding documents using a basic filter.
+
+Dataset: The dataset (SMALL_DOC) is contained within `small_doc.json` and consists of a sample document stored as strict
+JSON with an encoded length of approximately 250 bytes.
+
+Dataset size: For scoring purposes, the dataset size is the size of the `small_doc` source file (250 bytes) times 10,000
+operations, which equals 2,250,000 bytes or 2.5 MB.
+
+| Phase | Description |
+| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| Setup | Load the SMALL_DOC dataset into memory as an ODM-appropriate model object. Insert 10,000 instances into the database, saving the `_id` field for each into a list. |
+| Before task | n/a. |
+| Do task | For each of the 10,000 `_id` values, perform a filter operation to find the corresponding SMALL_DOC model. |
+| After task | n/a. |
+| Teardown | Drop the collection associated with the SMALL_DOC model. |
+
+#### Small model find foreign key by filter
+
+Summary: This benchmark tests ODM performance finding documents by foreign keys. This benchmark MUST only be run by ODMs
+that support join ($lookup) operations.
+
+Dataset: The dataset (SMALL_DOC) is contained within `small_doc.json` and consists of a sample document stored as strict
+JSON with an encoded length of approximately 250 bytes. An additional model (FOREIGN_KEY) representing the foreign key,
+consisting of only a string field called `name`, MUST also be created.
+
+Dataset size: For scoring purposes, the dataset size is the size of the `small_doc` source file (250 bytes) times 10,000
+operations, which equals 2,250,000 bytes or 2.5 MB.
+
+| Phase | Description |
+| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Load the SMALL_DOC dataset into memory as an ODM-appropriate model object. For each SMALL_DOC model, create and assign a FOREIGN_KEY instance to the `field_fk` field. Insert 10,000 instances of both models into the database, saving the inserted `_id` field for each FOREIGN_KEY into a list. |
+| Before task | n/a. |
+| Do task | For each of the 10,000 FOREIGN_KEY `_id` values, perform a filter operation in an ODM-appropriate manner to find the corresponding SMALL_DOC model. |
+| After task | n/a. |
+| Teardown | Drop the collections associated with the SMALL_DOC and FOREIGN_KEY models. |
+
+#### Large model creation
+
+Summary: This benchmark tests ODM performance creating a single large model.
+
+Dataset: The dataset (LARGE_DOC) is contained within `large_doc.json` and consists of a sample document stored as strict
+JSON with an encoded length of approximately 3.2 MB.
+
+Dataset size: For scoring purposes, the dataset size is the size of the `large_doc` source file (3,226,020 bytes) times
+10,000 operations, which equals 32,260,200,000 bytes or 32.2 GB.
+
+| Phase | Description |
+| ----------- | -------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Load the LARGE_DOC dataset into memory. |
+| Before task | n/a. |
+| Do task | Create an ODM-appropriate model instance for the LARGE_DOC document and save it to the database. Repeat this 10,000 times. |
+| After task | Drop the collection associated with the LARGE_DOC model. |
+| Teardown | n/a. |
+
+#### Large model update
+
+Summary: This benchmark tests ODM performance updating fields on a single large model.
+
+Dataset: The dataset (LARGE_DOC) is contained within `large_doc.json` and consists of a sample document stored as strict
+JSON with an encoded length of approximately 3.2 MB.
+
+Dataset size: For scoring purposes, the dataset size is the size of the `updated_value` string file (13 bytes) times
+10,000 operations, which equals 130,000 bytes or 130 KB.
+
+| Phase | Description |
+| ----------- | ------------------------------------------------------------------------------------------------------------------- |
+| Setup | Load the LARGE_DOC dataset into memory as an ODM-appropriate model object. Save 10,000 instances into the database. |
+| Before task | n/a. |
+| Do task | Update the `field1` field for each instance of the model to `updated_value` in an ODM-appropriate manner. |
+| After task | Drop the collection associated with the LARGE_DOC model. |
+| Teardown | n/a. |
+
+### Nested models
+
+Datasets are in the `nested_models.tgz` tarball.
+
+Nested model tests focus performing reads and writes on models containing nested (embedded) documents. They are designed
+to give insights into the efficiency of operations on the more complex data structures enabled by the document model.
+
+The data will be stored as strict JSON with no extended types. These JSON representations MUST be converted into
+equivalent ODM models as part of each benchmark task.
+
+Nested model benchmark tasks include:
+
+- Large model creation
+- Large model update nested field
+- Large model find nested field by filter
+- Large model find nested array field by filter
+
+#### Large model creation
+
+Summary: This benchmark tests ODM performance creating a single large nested model.
+
+Dataset: The dataset (LARGE_DOC_NESTED) is contained within `large_doc_nested.json` and consists of a sample document
+stored as strict JSON with an encoded length of approximately 8,000 bytes.
+
+Dataset size: For scoring purposes, the dataset size is the size of the `large_doc_nested` source file (8,000 bytes)
+times 10,000 operations, which equals 80,000,000 bytes or 80 MB.
+
+| Phase | Description |
+| ----------- | --------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Load the LARGE_DOC_NESTED dataset into memory. |
+| Before task | n/a. |
+| Do task | Create an ODM-appropriate model instance for the LARGE_DOC_NESTED document and save it to the database. Repeat this 10,000 times. |
+| After task | Drop the collection associated with the LARGE_DOC_NESTED model. |
+| Teardown | n/a. |
+
+#### Large model update nested
+
+Summary: This benchmark tests ODM performance updating nested fields on a single large model.
+
+Dataset: The dataset (LARGE_DOC_NESTED) is contained within `large_doc_nested.json` and consists of a sample document
+stored as strict JSON with an encoded length of approximately 8,000 bytes.
+
+Dataset size: For scoring purposes, the dataset size is the size of the `updated_value` string file (13 bytes) times
+10,000 operations, which equals 130,000 bytes or 130 KB.
+
+| Phase | Description |
+| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Load the LARGE_DOC_NESTED dataset into memory as an ODM-appropriate model object. Save 10,000 instances into the database. |
+| Before task | n/a. |
+| Do task | Update the value of the `embedded_str_doc_1.field1` field to `updated_value` in an ODM-appropriate manner for each instance of the model. |
+| After task | Drop the collection associated with the LARGE_DOC_NESTED model. |
+| Teardown | n/a. |
+
+#### Large nested model find nested by filter
+
+Summary: This benchmark tests ODM performance finding nested documents using a basic filter.
+
+Dataset: The dataset (LARGE_DOC_NESTED) is contained within `large_doc_nested.json` and consists of a sample document
+stored as strict JSON with an encoded length of approximately 8,000 bytes.
+
+Dataset size: For scoring purposes, the dataset size is the size of the `large_doc_nested` source file (8,000 bytes)
+times 10,000 operations, which equals 80,000,000 bytes or 80 MB.
+
+| Phase | Description |
+| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Setup | Load the LARGE_DOC_NESTED dataset into memory as an ODM-appropriate model object. Insert 10,000 instances into the database, saving the value of the `unique_id` field for each model's `embedded_str_doc_1` nested model into a list. |
+| Before task | n/a. |
+| Do task | For each of the 10,000 `embedded_str_doc_1.unique_id` values, perform a filter operation to search for the parent LARGE_DOC_NESTED model. |
+| After task | n/a. |
+| Teardown | Drop the collection associated with the LARGE_DOC_NESTED model. |
+
+#### Large nested model find nested array by filter
+
+Summary: This benchmark tests ODM performance finding nested document arrays using a basic filter.
+
+Dataset: The dataset (LARGE_DOC_NESTED) is contained within `large_doc_nested.json` and consists of a sample document
+stored as strict JSON with an encoded length of approximately 8,000 bytes.
+
+Dataset size: For scoring purposes, the dataset size is the size of the `large_doc_nested` source file (8,000 bytes)
+times 10,000 operations, which equals 80,000,000 bytes or 80 MB.
+
+| Phase | Description |
+| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| Setup | Load the LARGE_DOC_NESTED dataset into memory as an ODM-appropriate model object. Insert 10,000 instances into the database, saving the value of the `unique_id` field for the first item in each model's `embedded_str_doc_array` nested model into a list. |
+| Before task | n/a. |
+| Do task | For each of the 10,000 `unique_id` values, perform a filter operation to search for the parent LARGE_DOC_NESTED model. |
+| After task | n/a. |
+| Teardown | Drop the collection associated with the LARGE_DOC_NESTED model. |
+
+## Benchmark platform, configuration and environments
+
+### Benchmark Client
+
+The benchmarks SHOULD be run with the most recent stable version of the ODM and the newest version of the driver it
+supports.
+
+### Benchmark Server
+
+The MongoDB ODM Performance Benchmark MUST be run against a MongoDB replica set of size 1 patch-pinned to the latest
+stable database version without authentication or SSL enabled. This database version SHOULD be updated as needed to
+better differentiate between ODM performance changes and server performance changes. The Benchmark MUST be run on the
+established internal performance distro for the sake of consistency.
+
+### Benchmark placement and scheduling
+
+The MongoDB ODM Performance Benchmark SHOULD be placed in one of two places. For first-party ODMs, the Benchmark SHOULD
+be placed within the ODM's test directory as an independent test suite. For third-party ODMs, if the external
+maintainers do not wish to have the Benchmark included as part of the in-repo test suite, it SHOULD be included in the
+ODM performance testing repository created explicitly for this purpose.
+
+Due to the relatively long runtime of the benchmarks, including them as part of an automated suite that runs against
+every PR is not recommended. Instead, scheduling benchmark runs on a regular cadence is the recommended method of
+automating this suite of tests.
+
+## ODM-specific benchmarking
+
+As discussed earlier in this document, ODM feature sets vary significantly across libraries. Many ODMs have features
+unique to them or their niche in the wider ecosystem, which makes specifying concrete benchmark test cases for every
+possible API unfeasible. Instead, ODM authors SHOULD determine what mainline use cases of their library are not covered
+by the benchmarks specified above and expand this testing suite with additional benchmarks to cover those areas.
+
+## Changelog
+
+- 2025-11-14: Release initial 1.0 version
diff --git a/source/benchmarking/odm-data/flat_models.tgz b/source/benchmarking/odm-data/flat_models.tgz
new file mode 100644
index 0000000000..b779c30b3c
Binary files /dev/null and b/source/benchmarking/odm-data/flat_models.tgz differ
diff --git a/source/benchmarking/odm-data/nested_models.tgz b/source/benchmarking/odm-data/nested_models.tgz
new file mode 100644
index 0000000000..596b59e622
Binary files /dev/null and b/source/benchmarking/odm-data/nested_models.tgz differ
diff --git a/source/bson-binary-encrypted/binary-encrypted.md b/source/bson-binary-encrypted/binary-encrypted.md
new file mode 100644
index 0000000000..31dab4870d
--- /dev/null
+++ b/source/bson-binary-encrypted/binary-encrypted.md
@@ -0,0 +1,144 @@
+# BSON Binary Encrypted
+
+- Status: Accepted
+- Minimum Server Version: 4.2
+
+______________________________________________________________________
+
+## Abstract
+
+Client side encryption requires a new binary subtype to store (1) encrypted ciphertext with metadata, and (2) binary
+markings indicating what values must be encrypted in a document. (1) is stored in the server, but (2) is only used in
+the communication protocol between libmongocrypt and mongocryptd described in
+[Driver Spec: Client Side Encryption Encryption](../client-side-encryption/client-side-encryption.md).
+
+## META
+
+The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
+"OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt).
+
+## Specification
+
+This spec introduces a new BSON binary subtype with value 6. The binary has multiple formats determined by the first
+byte, but are all related to client side encryption. The first byte indicates the type and layout of the remaining data.
+
+All values are represented in little endian. The payload is generally optimized for storage size. The exception is the
+intent-to-encrypt markings which are only used between libmongocrypt and mongocryptd and never persisted.
+
+```typescript
+struct {
+ uint8 subtype;
+ [more data - see individual type definitions]
+}
+```
+
+| | | |
+| -------- | -------- | -------------------- |
+| **Name** | **Type** | **Description** |
+| subtype | uint8 | Type of blob format. |
+
+| | | |
+| -------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
+| **Type** | **Name** | **Blob Description** |
+| 0 | Intent-to-encrypt marking. | Contains unencrypted data that will be encrypted (by libmongocrypt) along with metadata describing how to encrypt it. |
+| 1 | AEAD_AES_CBC_HMAC_SHA512 deterministic ciphertext. | The metadata and encrypted data for deterministic encrypted data. |
+| 2 | AEAD_AES_CBC_HMAC_SHA512 randomized ciphertext. | The metadata and encrypted data for random encrypted data. |
+
+### Type 0: Intent-to-encrypt marking
+
+```typescript
+struct {
+ uint8 subtype = 0;
+ [ bson ];
+}
+```
+
+bson is the raw bytes of the following BSON document:
+
+| | | | |
+| -------- | ------------- | -------- | --------------------------------------------------------------------------------------------- |
+| **Name** | **Long Name** | **Type** | **Description** |
+| v | value | any | Value to encrypt. |
+| a | algorithm | int32 | Encryption algorithm to use. Same as fle_blob_subtype: 1 for deterministic, 2 for randomized. |
+| ki | keyId | UUID | Optional. Used to query the key vault by \_id. If omitted, then "ka" must be specified. |
+| ka | keyAltName | string | Optional. Used to query the key vault by keyAltName. If omitted, then "ki" must be specified. |
+
+### Types 1 and 2: Ciphertext
+
+```typescript
+struct {
+ uint8 subtype = (1 or 2);
+ uint8 key_uuid[16];
+ uint8 original_bson_type;
+ uint8 ciphertext[ciphertext_length];
+}
+```
+
+| | |
+| ------------------ | ------------------------------------------------------------------- |
+| **Name** | **Description** |
+| subtype | Type of blob format and encryption algorithm used. |
+| key_uuid[16] | The value of \_id for the key used to decrypt the ciphertext. |
+| original_bson_type | The byte representing the original BSON type of the encrypted data. |
+| ciphertext[] | The encrypted ciphertext (includes IV prepended). |
+
+## Test Plan
+
+Covered in [Driver Spec: Client Side Encryption Encryption](../client-side-encryption/client-side-encryption.md).
+
+## Design Rationale
+
+### Why not use a new BSON type?
+
+An alternative to using a new binary subtype would be introducing a new BSON type. This would be a needless backwards
+breaking change. Since FLE is largely a client side feature, it should be possible to store encrypted data in old
+servers.
+
+Plus, encrypted ciphertext is inherently a binary blob. Packing metadata inside isolates all of the encryption related
+data into one BSON value that can be treated as an opaque blob in most contexts.
+
+### Why not use separate BSON binary subtypes instead of a nested subtype?
+
+If we used separate subtypes, we'd need to reserve three (and possibly more in the future) of our 124 remaining
+subtypes.
+
+
+
+### Why are intent-to-encrypt markings needed?
+
+Intent-to-encrypt markings provide a simple way for mongocryptd to communicate what values need to be encrypted to
+libmongocrypt. Alternatively, mongocryptd could respond with a list of field paths. But field paths are difficult to
+make unambiguous, and even the query language is not always consistent.
+
+### What happened to the "key vault alias"?
+
+In an earlier revision of this specification the notion of a "key vault alias". The key vault alias identified one of
+possibly many key vaults that stored the key to decrypt the ciphertext. However, enforcing one key vault is a reasonable
+restriction for users. Users can migrate from one key vault to another without ciphertext data including a key vault
+alias. If we find a future need for multiple key vaults, we can easily introduce a new format with the fle_blob_subtype.
+
+Why distinguish between "deterministic" and "randomized" when they contain the same fields?
+
+Deterministic and randomized ciphertext supports different behavior. Deterministic ciphertext supports exact match
+queries but randomized does not.
+
+### Why is the original BSON type not encrypted?
+
+Exposing the underlying BSON type gives some validation of the data that is encrypted. A JSONSchema on the server can
+validate that the underlying encrypted BSON type is correct.
+
+## Reference Implementation
+
+libmongocrypt and mongocryptd will be the reference implementation of how BSON binary subtype 6 is used.
+
+## Security Implication
+
+It would be a very bad security flaw if intent-to-encrypt markings were confused with ciphertexts. This could lead to a
+marking inadvertently being stored on a server – meaning that plaintext is stored where ciphertext should have been.
+
+Therefore, the leading byte of the BSON binary subtype distinguishes between marking and ciphertext.
+
+## Changelog
+
+- 2024-03-04: Migrated from reStructuredText to Markdown.
+- 2022-10-05: Remove spec front matter and create changelog.
diff --git a/source/bson-binary-uuid/uuid.md b/source/bson-binary-uuid/uuid.md
new file mode 100644
index 0000000000..3f1e886783
--- /dev/null
+++ b/source/bson-binary-uuid/uuid.md
@@ -0,0 +1,299 @@
+# BSON Binary UUID
+
+- Status: Accepted
+- Minimum Server Version: N/A
+
+______________________________________________________________________
+
+## Abstract
+
+The Java, C#, and Python drivers natively support platform types for UUID, all of which by default encode them to and
+decode them from BSON binary subtype 3. However, each encode the bytes in a different order from the others. To improve
+interoperability, BSON binary subtype 4 was introduced and defined the byte order according to
+[RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.2), and a mechanism to configure each driver to encode UUIDs
+this way was added to each driver. The legacy representation remained as the default for each driver.
+
+This specification moves MongoDB drivers further towards the standard UUID representation by requiring an application
+relying on native UUID support to explicitly specify the representation it requires.
+
+Drivers that support native UUID types will additionally create helpers on their BsonBinary class that will aid in
+conversion to and from the platform native UUID type.
+
+## META
+
+The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
+"OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt).
+
+## Specification
+
+### Terms
+
+**UUID**
+
+A Universally Unique IDentifier
+
+**BsonBinary**
+
+An object that wraps an instance of a BSON binary value
+
+### Naming Deviations
+
+All drivers MUST name operations, objects, and parameters as defined in the following sections.
+
+The following deviations are permitted:
+
+- Drivers can use the platform's name for a UUID. For instance, in C# the platform class is Guid, whereas in Java it is
+ UUID.
+- Drivers can use a "to" prefix instead of an "as" prefix for the BsonBinary method names.
+
+### Explicit encoding and decoding
+
+Any driver with a native UUID type MUST add the following UuidRepresentation enumeration, and associated methods to its
+BsonBinary (or equivalent) class:
+
+```typescript
+/**
+
+enum UuidRepresentation {
+
+ /**
+ * An unspecified representation of UUID. Essentially, this is the null
+ * representation value. This value is not required for languages that
+ * have better ways of indicating, or preventing use of, a null value.
+ */
+ UNSPECIFIED("unspecified"),
+
+ /**
+ * The canonical representation of UUID according to RFC 4122,
+ * section 4.1.2
+ *
+ * It encodes as BSON binary subtype 4
+ */
+ STANDARD("standard"),
+
+ /**
+ * The legacy representation of UUID used by the C# driver.
+ *
+ * In this representation the order of bytes 0-3 are reversed, the
+ * order of bytes 4-5 are reversed, and the order of bytes 6-7 are
+ * reversed.
+ *
+ * It encodes as BSON binary subtype 3
+ */
+ C_SHARP_LEGACY("csharpLegacy"),
+
+ /**
+ * The legacy representation of UUID used by the Java driver.
+ *
+ * In this representation the order of bytes 0-7 are reversed, and the
+ * order of bytes 8-15 are reversed.
+ *
+ * It encodes as BSON binary subtype 3
+ */
+ JAVA_LEGACY("javaLegacy"),
+
+ /**
+ * The legacy representation of UUID used by the Python driver.
+ *
+ * As with STANDARD, this representation conforms with RFC 4122, section
+ * 4.1.2
+ *
+ * It encodes as BSON binary subtype 3
+ */
+ PYTHON_LEGACY("pythonLegacy")
+}
+
+class BsonBinary {
+ /*
+ * Construct from a UUID using the standard UUID representation
+ * [Specification] This constructor SHOULD be included but MAY be
+ * omitted if it creates backwards compatibility issues
+ */
+ constructor(Uuid uuid)
+
+ /*
+ * Construct from a UUID using the given UUID representation.
+ *
+ * The representation must not be equal to UNSPECIFIED
+ */
+ constructor(Uuid uuid, UuidRepresentation representation)
+
+ /*
+ * Decode a subtype 4 binary to a UUID, erroring when the subtype is not 4.
+ */
+ Uuid asUuid()
+
+ /*
+ * Decode a subtype 3 or 4 to a UUID, according to the UUID
+ * representation, erroring when subtype does not match the
+ * representation.
+ */
+ Uuid asUuid(UuidRepresentation representation)
+}
+```
+
+### Implicit decoding and encoding
+
+A new driver for a language with a native UUID type MUST NOT implicitly encode from or decode to the native UUID type.
+Rather, explicit conversion MUST be used as described in the previous section.
+
+Drivers that already do such implicit encoding and decoding SHOULD support a URI option, uuidRepresentation, which
+controls the default behavior of the UUID codec. Alternatively, a driver MAY specify the UUID representation via global
+state.
+
+| Value | Default? | Encode to | Decode subtype 4 to | Decode subtype 3 to |
+| ------------ | -------- | ------------------------------------------------- | ------------------- | ------------------- |
+| unspecified | yes | raise error | BsonBinary | BsonBinary |
+| standard | no | BSON binary subtype 4 | native UUID | BsonBinary |
+| csharpLegacy | no | BSON binary subtype 3 with C# legacy byte order | BsonBinary | native UUID |
+| javaLegacy | no | BSON binary subtype 3 with Java legacy byte order | BsonBinary | native UUID |
+| pythonLegacy | no | BSON binary subtype 3 with standard byte order | BsonBinary | native UUID |
+
+For scenarios where the application makes the choice (e.g. a POJO with a field of type UUID), or when serializers are
+strongly typed and are constrained to always return values of a certain type, the driver will raise an exception in
+cases where otherwise it would be required to decode to a different type (e.g. BsonBinary instead of UUID or vice
+versa).
+
+Note also that none of the above applies when decoding to strictly typed maps, e.g. a `Map` like Java
+or .NET's BsonDocument class. In those cases the driver is always decoding to BsonBinary, and applications would use the
+asUuid methods to explicitly convert from BsonBinary to UUID.
+
+### Implementation Notes
+
+Since changing the default UUID representation can reasonably be considered a backwards-breaking change, drivers that
+implement the full specification should stage implementation according to semantic versioning guidelines. Specifically,
+support for this specification can be added to a minor release, but with several exceptions:
+
+The default UUID representation should be left as is (e.g. JAVA_LEGACY for the Java driver) rather than be changed to
+UNSPECIFIED. In a subsequent major release, the default UUID representation can be changed to UNSPECIFIED (along with
+appropriate documentation indicating the backwards-breaking change). Drivers MUST document this in a prior minor
+release.
+
+## Test Plan
+
+The test plan consists of a series of prose tests. They all operate on the same UUID, with the String representation of
+"00112233-4455-6677-8899-aabbccddeeff".
+
+### Explicit encoding
+
+1. Create a BsonBinary instance with the given UUID
+ - Assert that the BsonBinary instance's subtype is equal to 4 and data equal to the hex-encoded string
+ "00112233445566778899AABBCCDDEEFF"
+2. Create a BsonBinary instance with the given UUID and UuidRepresentation equal to STANDARD
+ - Assert that the BsonBinary instance's subtype is equal to 4 and data equal to the hex-encoded string
+ "00112233445566778899AABBCCDDEEFF"
+3. Create a BsonBinary instance with the given UUID and UuidRepresentation equal to JAVA_LEGACY
+ - Assert that the BsonBinary instance's subtype is equal to 3 and data equal to the hex-encoded string
+ "7766554433221100FFEEDDCCBBAA9988"
+4. Create a BsonBinary instance with the given UUID and UuidRepresentation equal to CSHARP_LEGACY
+ - Assert that the BsonBinary instance's subtype is equal to 3 and data equal to the hex-encoded string
+ "33221100554477668899AABBCCDDEEFF"
+5. Create a BsonBinary instance with the given UUID and UuidRepresentation equal to PYTHON_LEGACY
+ - Assert that the BsonBinary instance's subtype is equal to 3 and data equal to the hex-encoded string
+ "00112233445566778899AABBCCDDEEFF"
+6. Create a BsonBinary instance with the given UUID and UuidRepresentation equal to UNSPECIFIED
+ - Assert that an error is raised
+
+### Explicit Decoding
+
+1. Create a BsonBinary instance with subtype equal to 4 and data equal to the hex-encoded string
+ "00112233445566778899AABBCCDDEEFF"
+ 1. Assert that a call to BsonBinary.asUuid() returns the given UUID
+ 2. Assert that a call to BsonBinary.asUuid(STANDARD) returns the given UUID
+ 3. Assert that a call to BsonBinary.asUuid(UNSPECIFIED) raises an error
+ 4. Assert that a call to BsonBinary.asUuid(JAVA_LEGACY) raises an error
+ 5. Assert that a call to BsonBinary.asUuid(CSHARP_LEGACY) raises an error
+ 6. Assert that a call to BsonBinary.asUuid(PYTHON_LEGACY) raises an error
+2. Create a BsonBinary instance with subtype equal to 3 and data equal to the hex-encoded string
+ "7766554433221100FFEEDDCCBBAA9988"
+ 1. Assert that a call to BsonBinary.asUuid() raises an error
+ 2. Assert that a call to BsonBinary.asUuid(STANDARD) raised an error
+ 3. Assert that a call to BsonBinary.asUuid(UNSPECIFIED) raises an error
+ 4. Assert that a call to BsonBinary.asUuid(JAVA_LEGACY) returns the given UUID
+3. Create a BsonBinary instance with subtype equal to 3 and data equal to the hex-encoded string
+ "33221100554477668899AABBCCDDEEFF"
+ 1. Assert that a call to BsonBinary.asUuid() raises an error
+ 2. Assert that a call to BsonBinary.asUuid(STANDARD) raised an error
+ 3. Assert that a call to BsonBinary.asUuid(UNSPECIFIED) raises an error
+ 4. Assert that a call to BsonBinary.asUuid(CSHARP_LEGACY) returns the given UUID
+4. Create a BsonBinary instance with subtype equal to 3 and data equal to the hex-encoded string
+ "00112233445566778899AABBCCDDEEFF"
+ 1. Assert that a call to BsonBinary.asUuid() raises an error
+ 2. Assert that a call to BsonBinary.asUuid(STANDARD) raised an error
+ 3. Assert that a call to BsonBinary.asUuid(UNSPECIFIED) raises an error
+ 4. Assert that a call to BsonBinary.asUuid(PYTHON_LEGACY) returns the given UUID
+
+### Implicit encoding
+
+1. Set the uuidRepresentation of the client to "javaLegacy". Insert a document with an "\_id" key set to the given
+ native UUID value.
+ - Assert that the actual value inserted is a BSON binary with subtype 3 and data equal to the hex-encoded string
+ "7766554433221100FFEEDDCCBBAA9988"
+2. Set the uuidRepresentation of the client to "charpLegacy". Insert a document with an "\_id" key set to the given
+ native UUID value.
+ - Assert that the actual value inserted is a BSON binary with subtype 3 and data equal to the hex-encoded string
+ "33221100554477668899AABBCCDDEEFF"
+3. Set the uuidRepresentation of the client to "pythonLegacy". Insert a document with an "\_id" key set to the given
+ native UUID value.
+ - Assert that the actual value inserted is a BSON binary with subtype 3 and data equal to the hex-encoded string
+ "00112233445566778899AABBCCDDEEFF"
+4. Set the uuidRepresentation of the client to "standard". Insert a document with an "\_id" key set to the given native
+ UUID value.
+ - Assert that the actual value inserted is a BSON binary with subtype 4 and data equal to the hex-encoded string
+ "00112233445566778899AABBCCDDEEFF"
+5. Set the uuidRepresentation of the client to "unspecified". Insert a document with an "\_id" key set to the given
+ native UUID value.
+ - Assert that a BSON serialization exception is thrown
+
+### Implicit Decoding
+
+1. Set the uuidRepresentation of the client to "javaLegacy". Insert a document containing two fields. The "standard"
+ field should contain a BSON Binary created by creating a BsonBinary instance with the given UUID and the STANDARD
+ UuidRepresentation. The "legacy" field should contain a BSON Binary created by creating a BsonBinary instance with
+ the given UUID and the JAVA_LEGACY UuidRepresentation. Find the document.
+
+ 1. Assert that the value of the "standard" field is of type BsonBinary and is equal to the inserted value.
+ 2. Assert that the value of the "legacy" field is of the native UUID type and is equal to the given UUID
+
+ Repeat this test with the uuidRepresentation of the client set to "csharpLegacy" and "pythonLegacy".
+
+2. Set the uuidRepresentation of the client to "standard". Insert a document containing two fields. The "standard" field
+ should contain a BSON Binary created by creating a BsonBinary instance with the given UUID and the STANDARD
+ UuidRepresentation. The "legacy" field should contain a BSON Binary created by creating a BsonBinary instance with
+ the given UUID and the PYTHON_LEGACY UuidRepresentation. Find the document.
+
+ 1. Assert that the value of the "standard" field is of the native UUID type and is equal to the given UUID
+ 2. Assert that the value of the "legacy" field is of type BsonBinary and is equal to the inserted value.
+
+3. Set the uuidRepresentation of the client to "unspecified". Insert a document containing two fields. The "standard"
+ field should contain a BSON Binary created by creating a BsonBinary instance with the given UUID and the STANDARD
+ UuidRepresentation. The "legacy" field should contain a BSON Binary created by creating a BsonBinary instance with
+ the given UUID and the PYTHON_LEGACY UuidRepresentation. Find the document.
+
+ 1. Assert that the value of the "standard" field is of type BsonBinary and is equal to the inserted value
+ 2. Assert that the value of the "legacy" field is of type BsonBinary and is equal to the inserted value.
+
+ Repeat this test with the uuidRepresentation of the client set to "csharpLegacy" and "pythonLegacy".
+
+Note: the assertions will be different in the release prior to the major release, to avoid breaking changes. Adjust
+accordingly!
+
+## Q & A
+
+### What's the rationale for the deviations allowed by the specification?
+
+In short, the C# driver has existing behavior that make it infeasible to work the same as other drivers.
+
+The C# driver has a global serialization registry. Since it's global and not per-MongoClient, it's not feasible to
+override the UUID representation on a per-MongoClient basis, since doing so would require a per-MongoClient registry.
+Instead, the specification allows for a global override so that the C# driver can implement the specification.
+
+Additionally, the C# driver has an existing configuration parameter that controls the behavior of BSON readers and
+writers at a level below the serializers. This configuration affects the semantics of the existing BsonBinary class in a
+way that doesn't allow for the constructor(UUID) mentioned in the specification. For this reason, that constructor is
+specified as optional.
+
+## Changelog
+
+- 2024-08-01: Migrated from reStructuredText to Markdown.
+- 2022-10-05: Remove spec front matter.
diff --git a/source/bson-binary-vector/bson-binary-vector.md b/source/bson-binary-vector/bson-binary-vector.md
new file mode 100644
index 0000000000..b398ffabf3
--- /dev/null
+++ b/source/bson-binary-vector/bson-binary-vector.md
@@ -0,0 +1,272 @@
+# BSON Binary Subtype 9 - Vector
+
+- Status: Accepted
+- Minimum Server Version: N/A
+
+______________________________________________________________________
+
+## Abstract
+
+This document describes the subtype of the Binary BSON type used for efficient storage and retrieval of vectors. Vectors
+here refer to densely packed arrays of numbers, all of the same type.
+
+## Motivation
+
+These representations correspond to the numeric types supported by popular numerical libraries for vector processing,
+such as NumPy, PyTorch, TensorFlow and Apache Arrow. Storing and retrieving vector data using the same densely packed
+format used by these libraries can result in significant memory savings and processing efficiency.
+
+### META
+
+The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
+"OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt).
+
+## Specification
+
+This specification introduces a new BSON binary subtype, the vector, with value `9`.
+
+Drivers SHOULD provide idiomatic APIs to translate between arrays of numbers and this BSON Binary specification.
+
+### Data Types (dtypes)
+
+Each vector can take one of multiple data types (dtypes). The following table lists the dtypes implemented.
+
+| Vector data type | Alias | Bits per vector element | [Arrow Data Type](https://arrow.apache.org/docs/cpp/api/datatype.html) (for illustration) |
+| ---------------- | ---------- | ----------------------- | ----------------------------------------------------------------------------------------- |
+| `0x03` | INT8 | 8 | INT8 |
+| `0x27` | FLOAT32 | 32 | FLOAT |
+| `0x10` | PACKED_BIT | 1 `*` | BOOL |
+
+`*` A Binary Quantized (PACKED_BIT) Vector is a vector of 0s and 1s (bits), but it is represented in memory as a list of
+integers in [0, 255]. So, for example, the vector `[0, 255]` would be shorthand for the 16-bit vector
+`[0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1]`. The idea is that each number (a uint8) can be stored as a single byte. Of course,
+some languages, Python for one, do not have an uint8 type, so must be represented as an int in memory, but not on disk.
+
+### Byte padding
+
+As not all data types have a bit length equal to a multiple of 8, and hence do not fit squarely into a certain number of
+bytes, a second piece of metadata, the "padding" is included. This instructs the driver of the number of bits in the
+final byte that are to be ignored. The least-significant bits are ignored.
+
+### Binary structure
+
+Following the binary subtype `9`, a two-element byte array of metadata precedes the packed numbers.
+
+- The first byte (dtype) describes its data type. The table above shows those that MUST be implemented. This table may
+ increase. dtype is an unsigned integer.
+
+- The second byte (padding) prescribes the number of bits to ignore in the final byte of the value. It is a non-negative
+ integer. It must be present, even in cases where it is not applicable, and set to zero.
+
+- The remainder contains the actual vector elements packed according to dtype.
+
+All values use the little-endian format.
+
+#### Example
+
+Let's take a vector `[238, 224]` of dtype PACKED_BIT (`\x10`) with a padding of `4`.
+
+In hex, it looks like this: `b"\x10\x04\xee\xe0"`: 1 byte for dtype, 1 for padding, and 1 for each uint8.
+
+We can visualize the binary representation like so:
+
+
+
+ | 1st byte: dtype (from list in previous table) |
+ 2nd byte: padding (values in [0,7]) |
+ 1st uint8: 238 |
+ 2nd uint8: 224 |
+
+
+ | 0 |
+ 0 |
+ 0 |
+ 1 |
+ 0 |
+ 0 |
+ 0 |
+ 0 |
+ 0 |
+ 0 |
+ 0 |
+ 0 |
+ 0 |
+ 1 |
+ 0 |
+ 0 |
+ 1 |
+ 1 |
+ 1 |
+ 0 |
+ 1 |
+ 1 |
+ 1 |
+ 0 |
+ 1 |
+ 1 |
+ 1 |
+ 0 |
+ 0 |
+ 0 |
+ 0 |
+ 0 |
+
+
+
+Finally, after we remove the last 4 bits of padding, the actual bit vector has a length of 12 and looks like this!
+
+| 1 | 1 | 1 | 0 | 1 | 1 | 1 | 0 | 1 | 1 | 1 | 0 |
+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
+
+## API Guidance
+
+Drivers MUST implement methods for explicit encoding and decoding that adhere to the pattern described below while
+following idioms of the language of the driver.
+
+### Encoding
+
+```
+Function from_vector(vector: Iterable, dtype: DtypeEnum, padding: Integer = 0) -> Binary
+ # Converts a numeric vector into a binary representation based on the specified dtype and padding.
+
+ # :param vector: A sequence or iterable of numbers (either float or int)
+ # :param dtype: Data type for binary conversion (from DtypeEnum)
+ # :param padding: Optional integer specifying how many bits to ignore in the final byte
+ # :return: A binary representation of the vector
+
+ Declare binary_data as Binary
+
+ # Process each number in vector and convert according to dtype
+ For each number in vector
+ binary_element = convert_to_binary(number, dtype)
+ binary_data.append(binary_element)
+ End For
+
+ # Apply padding to the binary data if needed
+ If padding > 0
+ apply_padding(binary_data, padding)
+ End If
+
+ Return binary_data
+End Function
+```
+
+Note: If a driver chooses to implement a `Vector` type (or numerous) like that suggested in the Data Structure
+subsection below, they MAY decide that `from_vector` that has a single argument, a Vector.
+
+### Decoding
+
+```
+Function as_vector() -> Vector
+ # Unpacks binary data (BSON or similar) into a Vector structure.
+ # This process involves extracting numeric values, the data type, and padding information.
+
+ # :return: A BinaryVector containing the unpacked numeric values, dtype, and padding.
+
+ Declare binary_vector as BinaryVector # Struct to hold the unpacked data
+
+ # Extract dtype (data type) from the binary data
+ binary_vector.dtype = extract_dtype_from_binary()
+
+ # Extract padding from the binary data
+ binary_vector.padding = extract_padding_from_binary()
+
+ # Unpack the actual numeric values from the binary data according to the dtype
+ binary_vector.data = unpack_numeric_values(binary_vector.dtype)
+
+ Return binary_vector
+End Function
+```
+
+#### Validation
+
+Drivers MUST validate vector metadata and raise an exception if any invariant is violated:
+
+- When unpacking binary data into a FLOAT32 Vector structure, the length of the binary data following the dtype and
+ padding MUST be a multiple of 4 bytes.
+- Padding MUST be 0 for all dtypes where padding doesn’t apply, and MUST be within [0, 7] for PACKED_BIT.
+- A PACKED_BIT vector MUST NOT be empty if padding is in the range [1, 7].
+ - For a PACKED_BIT vector with non-zero padding, ignored bits SHOULD be zero.
+ - When encoding, if ignored bits aren't zero, drivers SHOULD raise an exception, but drivers MAY leave them as-is if
+ backwards-compatibility is a concern.
+ - When decoding, drivers SHOULD raise an exception if decoding non-zero ignored bits, but drivers MAY choose not to
+ for backwards compatibility.
+ - Drivers SHOULD use the next major release to conform to ignored bits being zero.
+
+Drivers MUST perform this validation when a numeric vector and padding are provided through the API, and when unpacking
+binary data (BSON or similar) into a Vector structure.
+
+#### Data Structures
+
+Drivers MAY find the following structures to represent the dtype and vector structure useful.
+
+```
+Enum Dtype
+ # Enum for data types (dtype)
+
+ # FLOAT32: Represents packing of list of floats as float32
+ # Value: 0x27 (hexadecimal byte value)
+
+ # INT8: Represents packing of list of signed integers in the range [-128, 127] as signed int8
+ # Value: 0x03 (hexadecimal byte value)
+
+ # PACKED_BIT: Special case where vector values are 0 or 1, packed as unsigned uint8 in range [0, 255]
+ # Packed into groups of 8 (a byte)
+ # Value: 0x10 (hexadecimal byte value)
+
+ # Documentation:
+ # Each value is a byte (length of one), a convenient choice for decoding.
+End Enum
+
+Struct Vector
+ # Numeric vector with metadata for binary interoperability
+
+ # Fields:
+ # data: Sequence of numeric values (either float or int)
+ # dtype: Data type of vector (from enum BinaryVectorDtype)
+ # padding: Number of bits to ignore in the final byte for alignment
+
+ data # Sequence of float or int
+ dtype # Type: DtypeEnum
+ padding # Integer: Number of padding bits
+ End Struct
+```
+
+## Reference Implementation
+
+- PYTHON (PYTHON-4577)
+
+## Test Plan
+
+See the [README](tests/README.md) for tests.
+
+## FAQ
+
+- What MongoDB Server version does this apply to?
+
+ - Files in the "specifications" repository have no version scheme. They are not tied to a MongoDB server version.
+
+- In PACKED_BIT, why would one choose to use integers in \[0, 256)?
+
+ - This follows a well-established precedent for packing binary-valued arrays into bytes (8 bits), This technique is
+ widely used across different fields, such as data compression, communication protocols, and file formats, where
+ you want to store or transmit binary data more efficiently by grouping 8 bits into a single byte (uint8). For an
+ example in Python, see
+ [numpy.unpackbits](https://numpy.org/doc/2.0/reference/generated/numpy.unpackbits.html#numpy.unpackbits).
+
+- In PACKED_BIT, why are ignored bits recommended to be zero?
+
+ - To ensure the same data representation has the same encoding. For drivers supporting comparison operations, this
+ avoids comparing different unused bits.
+
+## Changelog
+
+- 2025-06-23: In PACKED_BIT vectors, ignored bits MAY be zero for backwards-compatibility. Prose tests added.
+
+- 2025-04-08: In PACKED_BIT vectors, ignored bits must be zero.
+
+- 2025-03-07: Update tests to use Extended JSON representation of +/-Infinity. (DRIVERS-3095)
+
+- 2025-02-04: Update validation for decoding into a FLOAT32 vector.
+
+- 2024-11-01: BSON Binary Subtype 9 accepted DRIVERS-2926 (#1708)
diff --git a/source/bson-binary-vector/tests/README.md b/source/bson-binary-vector/tests/README.md
new file mode 100644
index 0000000000..3902aa10c9
--- /dev/null
+++ b/source/bson-binary-vector/tests/README.md
@@ -0,0 +1,124 @@
+# Testing Binary subtype 9: Vector
+
+The JSON files in this directory tree are platform-independent tests that drivers can use to prove their conformance to
+the specification.
+
+These tests focus on the roundtrip of the list of numbers as input/output, along with their data type and byte padding.
+
+Additional tests exist in `bson_corpus/tests/binary.json` but do not sufficiently test the end-to-end process of Vector
+to BSON. For this reason, drivers must create a bespoke test runner for the vector subtype.
+
+## Format
+
+The test data corpus consists of a JSON file for each data type (dtype). Each file contains a number of test cases,
+under the top-level key "tests". Each test case pertains to a single vector. The keys provide the specification of the
+vector. Valid cases also include the Canonical BSON format of a document {test_key: binary}. The "test_key" is common,
+and specified at the top level.
+
+#### Top level keys
+
+Each JSON file contains three top-level keys.
+
+- `description`: human-readable description of what is in the file
+- `test_key`: name used for key when encoding/decoding a BSON document containing the single BSON Binary for the test
+ case. Applies to *every* case.
+- `tests`: array of test case objects, each of which have the following keys. Valid cases will also contain additional
+ binary and json encoding values.
+
+#### Keys of individual tests cases
+
+- `description`: string describing the test.
+- `valid`: boolean indicating if the vector, dtype, and padding should be considered a valid input.
+- `vector`: (required if valid is true) list of numbers
+- `dtype_hex`: string defining the data type in hex (e.g. "0x10", "0x27")
+- `dtype_alias`: (optional) string defining the data dtype, perhaps as Enum.
+- `padding`: (optional) integer for byte padding. Defaults to 0.
+- `canonical_bson`: (required if valid is true) an (uppercase) big-endian hex representation of a BSON byte string.
+
+## Required tests
+
+#### To prove correct in a valid case (`valid: true`), one MUST
+
+- encode a document from the numeric values, dtype, and padding, along with the "test_key", and assert this matches the
+ canonical_bson string.
+- decode the canonical_bson into its binary form, and then assert that the numeric values, dtype, and padding all match
+ those provided in the JSON.
+
+Note: For floating point number types, exact numerical matches may not be possible. Drivers that natively support the
+floating-point type being tested (e.g., when testing float32 vector values in a driver that natively supports float32),
+MUST assert that the input float array is the same after encoding and decoding.
+
+#### To prove correct in an invalid case (`valid:false`), one MUST
+
+- if the vector field is present, raise an exception when attempting to encode a document from the numeric values,
+ dtype, and padding.
+- if the canonical_bson field is present, raise an exception when attempting to deserialize it into the corresponding
+ numeric values, as the field contains corrupted data.
+
+## Prose Tests
+
+### Treatment of non-zero ignored bits
+
+All drivers MUST test encoding and decoding behavior according to their design and version. For drivers that haven't
+been completed, raise exceptions in both cases. For those that have, update to this behavior according to semantic
+versioning rules, and update tests accordingly.
+
+In both cases, [255], a single byte PACKED_BIT vector of length 1 (hence padding of 7) provides a good example to use,
+as all of its bits are ones.
+
+#### 1. Encoding
+
+- Test encoding with non-zero ignored bits. Use the driver API that validates vector metadata.
+- If the driver validates ignored bits are zero (preferred), expect an error. Otherwise expect the ignored bits are
+ preserved.
+
+```python
+with pytest.raises(ValueError):
+ Binary.from_vector([0b11111111], BinaryVectorDtype.PACKED_BIT, padding=7)
+```
+
+### 2. Decoding
+
+- Test the behaviour of your driver when one attempts to decode from binary to vector.
+ - e.g. As of pymongo 4.14, a warning is raised. From 5.0, it will be an exception.
+
+```python
+b = Binary(b'\x10\x07\xff', subtype=9)
+with pytest.warns():
+ Binary.as_vector(b)
+```
+
+Drivers MAY skip this test if they choose not to implement a `Vector` type.
+
+### 3. Comparison
+
+Once we can guarantee that all ignored bits are non-zero, then equality can be tested on the binary subtype. Until then,
+equality is ambiguous, and depends on whether one compares by bits (uint1), or uint8. Drivers SHOULD test equality
+behavior according to their design and version.
+
+For example, in `pymongo < 5.0`, we define equality of a BinaryVector by matching padding, dtype, and integer. This
+means that two single bit vectors in which 7 bits are ignored do not match unless all bits match. This mirrors what the
+server does.
+
+```python
+b1 = Binary(b'\x10\x07\x80', subtype=9) # 1-bit vector with all 0 ignored bits.
+b2 = Binary(b'\x10\x07\xff', subtype=9) # 1-bit vector with all 1 ignored bits.
+b3 = Binary.from_vector([0b10000000], BinaryVectorDtype.PACKED_BIT, padding=7) # Same data as b1.
+
+v1 = Binary.as_vector(b1)
+v2 = Binary.as_vector(b2)
+v3 = Binary.as_vector(b3)
+
+assert b1 != b2 # Unequal at naive Binary level
+assert v2 != v1 # Also chosen to be unequal at BinaryVector level as [255] != [128]
+assert b1 == b3 # Equal at naive Binary level
+assert v1 == v3 # Equal at the BinaryVector level
+```
+
+Drivers MAY skip this test if they choose not to implement a `Vector` type, or the type does not support comparison, or
+the type cannot be constructed with non-zero ignored bits.
+
+## FAQ
+
+- What MongoDB Server version does this apply to?
+ - Files in the "specifications" repository have no version scheme. They are not tied to a MongoDB server version.
diff --git a/source/bson-binary-vector/tests/float32.json b/source/bson-binary-vector/tests/float32.json
new file mode 100644
index 0000000000..72dafce10f
--- /dev/null
+++ b/source/bson-binary-vector/tests/float32.json
@@ -0,0 +1,65 @@
+{
+ "description": "Tests of Binary subtype 9, Vectors, with dtype FLOAT32",
+ "test_key": "vector",
+ "tests": [
+ {
+ "description": "Simple Vector FLOAT32",
+ "valid": true,
+ "vector": [127.0, 7.0],
+ "dtype_hex": "0x27",
+ "dtype_alias": "FLOAT32",
+ "padding": 0,
+ "canonical_bson": "1C00000005766563746F72000A0000000927000000FE420000E04000"
+ },
+ {
+ "description": "Vector with decimals and negative value FLOAT32",
+ "valid": true,
+ "vector": [127.7, -7.7],
+ "dtype_hex": "0x27",
+ "dtype_alias": "FLOAT32",
+ "padding": 0,
+ "canonical_bson": "1C00000005766563746F72000A0000000927006666FF426666F6C000"
+ },
+ {
+ "description": "Empty Vector FLOAT32",
+ "valid": true,
+ "vector": [],
+ "dtype_hex": "0x27",
+ "dtype_alias": "FLOAT32",
+ "padding": 0,
+ "canonical_bson": "1400000005766563746F72000200000009270000"
+ },
+ {
+ "description": "Infinity Vector FLOAT32",
+ "valid": true,
+ "vector": [{"$numberDouble": "-Infinity"}, 0.0, {"$numberDouble": "Infinity"} ],
+ "dtype_hex": "0x27",
+ "dtype_alias": "FLOAT32",
+ "padding": 0,
+ "canonical_bson": "2000000005766563746F72000E000000092700000080FF000000000000807F00"
+ },
+ {
+ "description": "FLOAT32 with padding",
+ "valid": false,
+ "vector": [127.0, 7.0],
+ "dtype_hex": "0x27",
+ "dtype_alias": "FLOAT32",
+ "padding": 3,
+ "canonical_bson": "1C00000005766563746F72000A0000000927030000FE420000E04000"
+ },
+ {
+ "description": "Insufficient vector data with 3 bytes FLOAT32",
+ "valid": false,
+ "dtype_hex": "0x27",
+ "dtype_alias": "FLOAT32",
+ "canonical_bson": "1700000005766563746F7200050000000927002A2A2A00"
+ },
+ {
+ "description": "Insufficient vector data with 5 bytes FLOAT32",
+ "valid": false,
+ "dtype_hex": "0x27",
+ "dtype_alias": "FLOAT32",
+ "canonical_bson": "1900000005766563746F7200070000000927002A2A2A2A2A00"
+ }
+ ]
+}
diff --git a/source/bson-binary-vector/tests/int8.json b/source/bson-binary-vector/tests/int8.json
new file mode 100644
index 0000000000..29524fb617
--- /dev/null
+++ b/source/bson-binary-vector/tests/int8.json
@@ -0,0 +1,57 @@
+{
+ "description": "Tests of Binary subtype 9, Vectors, with dtype INT8",
+ "test_key": "vector",
+ "tests": [
+ {
+ "description": "Simple Vector INT8",
+ "valid": true,
+ "vector": [127, 7],
+ "dtype_hex": "0x03",
+ "dtype_alias": "INT8",
+ "padding": 0,
+ "canonical_bson": "1600000005766563746F7200040000000903007F0700"
+ },
+ {
+ "description": "Empty Vector INT8",
+ "valid": true,
+ "vector": [],
+ "dtype_hex": "0x03",
+ "dtype_alias": "INT8",
+ "padding": 0,
+ "canonical_bson": "1400000005766563746F72000200000009030000"
+ },
+ {
+ "description": "Overflow Vector INT8",
+ "valid": false,
+ "vector": [128],
+ "dtype_hex": "0x03",
+ "dtype_alias": "INT8",
+ "padding": 0
+ },
+ {
+ "description": "Underflow Vector INT8",
+ "valid": false,
+ "vector": [-129],
+ "dtype_hex": "0x03",
+ "dtype_alias": "INT8",
+ "padding": 0
+ },
+ {
+ "description": "INT8 with padding",
+ "valid": false,
+ "vector": [127, 7],
+ "dtype_hex": "0x03",
+ "dtype_alias": "INT8",
+ "padding": 3,
+ "canonical_bson": "1600000005766563746F7200040000000903037F0700"
+ },
+ {
+ "description": "INT8 with float inputs",
+ "valid": false,
+ "vector": [127.77, 7.77],
+ "dtype_hex": "0x03",
+ "dtype_alias": "INT8",
+ "padding": 0
+ }
+ ]
+}
diff --git a/source/bson-binary-vector/tests/packed_bit.json b/source/bson-binary-vector/tests/packed_bit.json
new file mode 100644
index 0000000000..7cc272e38b
--- /dev/null
+++ b/source/bson-binary-vector/tests/packed_bit.json
@@ -0,0 +1,83 @@
+{
+ "description": "Tests of Binary subtype 9, Vectors, with dtype PACKED_BIT",
+ "test_key": "vector",
+ "tests": [
+ {
+ "description": "Padding specified with no vector data PACKED_BIT",
+ "valid": false,
+ "vector": [],
+ "dtype_hex": "0x10",
+ "dtype_alias": "PACKED_BIT",
+ "padding": 1,
+ "canonical_bson": "1400000005766563746F72000200000009100100"
+ },
+ {
+ "description": "Simple Vector PACKED_BIT",
+ "valid": true,
+ "vector": [127, 7],
+ "dtype_hex": "0x10",
+ "dtype_alias": "PACKED_BIT",
+ "padding": 0,
+ "canonical_bson": "1600000005766563746F7200040000000910007F0700"
+ },
+ {
+ "description": "PACKED_BIT with padding",
+ "valid": true,
+ "vector": [127, 8],
+ "dtype_hex": "0x10",
+ "dtype_alias": "PACKED_BIT",
+ "padding": 3,
+ "canonical_bson": "1600000005766563746F7200040000000910037F0800"
+ },
+ {
+ "description": "Empty Vector PACKED_BIT",
+ "valid": true,
+ "vector": [],
+ "dtype_hex": "0x10",
+ "dtype_alias": "PACKED_BIT",
+ "padding": 0,
+ "canonical_bson": "1400000005766563746F72000200000009100000"
+ },
+ {
+ "description": "Overflow Vector PACKED_BIT",
+ "valid": false,
+ "vector": [256],
+ "dtype_hex": "0x10",
+ "dtype_alias": "PACKED_BIT",
+ "padding": 0
+ },
+ {
+ "description": "Underflow Vector PACKED_BIT",
+ "valid": false,
+ "vector": [-1],
+ "dtype_hex": "0x10",
+ "dtype_alias": "PACKED_BIT",
+ "padding": 0
+ },
+ {
+ "description": "Vector with float values PACKED_BIT",
+ "valid": false,
+ "vector": [127.5],
+ "dtype_hex": "0x10",
+ "dtype_alias": "PACKED_BIT",
+ "padding": 0
+ },
+ {
+ "description": "Exceeding maximum padding PACKED_BIT",
+ "valid": false,
+ "vector": [1],
+ "dtype_hex": "0x10",
+ "dtype_alias": "PACKED_BIT",
+ "padding": 8,
+ "canonical_bson": "1500000005766563746F7200030000000910080100"
+ },
+ {
+ "description": "Negative padding PACKED_BIT",
+ "valid": false,
+ "vector": [1],
+ "dtype_hex": "0x10",
+ "dtype_alias": "PACKED_BIT",
+ "padding": -1
+ }
+ ]
+}
diff --git a/source/bson-corpus/bson-corpus.md b/source/bson-corpus/bson-corpus.md
new file mode 100644
index 0000000000..7126acbd7b
--- /dev/null
+++ b/source/bson-corpus/bson-corpus.md
@@ -0,0 +1,363 @@
+# BSON Corpus
+
+- Status: Accepted
+- Minimum Server Version: N/A
+
+## Abstract
+
+The official BSON specification does not include test data, so this pseudo-specification describes tests for BSON
+encoding and decoding. It also includes tests for MongoDB's "Extended JSON" specification (hereafter abbreviated as
+`extjson`).
+
+## Meta
+
+The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
+"OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt).
+
+## Motivation for Change
+
+To ensure correct operation, we want drivers to implement identical tests for important features. BSON (and `extjson`)
+are critical for correct operation and data exchange, but historically had no common test corpus. This
+pseudo-specification provides such tests.
+
+### Goals
+
+- Provide machine-readable test data files for BSON and `extjson` encoding and decoding.
+- Cover all current and historical BSON types.
+- Define test data patterns for three cases:
+ - conversion/roundtrip,
+ - decode errors, and
+ - parse errors.
+
+### Non-Goals
+
+- Replace or extend the official BSON spec at .
+- Provide a formal specification for `extjson`.
+
+## Specification
+
+The specification for BSON lives at . The `extjson` format specification is
+[here](../extended-json/extended-json.md).
+
+## Test Plan
+
+This test plan describes a general approach for BSON testing. Future BSON specifications (such as for new types like
+Decimal128) may specialize or alter the approach described below.
+
+### Description of the BSON Corpus
+
+This BSON test data corpus consists of a JSON file for each BSON type, plus a `top.json` file for testing the overall,
+enclosing document and a `multi-type.json` file for testing a document with all BSON types. There is also a
+`multi-type-deprecated.json` that includes deprecated keys.
+
+#### Top level keys
+
+- `description`: human-readable description of what is in the file
+- `bson_type`: hex string of the first byte of a BSON element (e.g. "0x01" for type "double"); this will be the
+ synthetic value "0x00" for "whole document" tests like `top.json`.
+- `test_key`: (optional) name of a field in a single-BSON-type `valid` test case that contains the data type being
+ tested.
+- `valid` (optional): an array of validity test cases (see below).
+- `decodeErrors` (optional): an array of decode error cases (see below).
+- `parseErrors` (optional): an array of type-specific parse error case (see below).
+- `deprecated` (optional): this field will be present (and true) if the BSON type has been deprecated (i.e. Symbol,
+ Undefined and DBPointer)
+
+#### Validity test case keys
+
+Validity test cases include 'canonical' forms of BSON and Extended JSON that are deemed equivalent and may provide
+additional cases or metadata for additional assertions. For each case, keys include:
+
+- `description`: human-readable test case label.
+- `canonical_bson`: an (uppercase) big-endian hex representation of a BSON byte string. Be sure to mangle the case as
+ appropriate in any roundtrip tests.
+- `canonical_extjson`: a string containing a Canonical Extended JSON document. Because this is itself embedded as a
+ *string* inside a JSON document, characters like quote and backslash are escaped.
+- `relaxed_extjson`: (optional) a string containing a Relaxed Extended JSON document. Because this is itself embedded as
+ a *string* inside a JSON document, characters like quote and backslash are escaped.
+- `degenerate_bson`: (optional) an (uppercase) big-endian hex representation of a BSON byte string that is technically
+ parseable, but not in compliance with the BSON spec. Be sure to mangle the case as appropriate in any roundtrip
+ tests.
+- `degenerate_extjson`: (optional) a string containing an invalid form of Canonical Extended JSON that is still
+ parseable according to type-specific rules. (For example, "1e100" instead of "1E+100".)
+- `converted_bson`: (optional) an (uppercase) big-endian hex representation of a BSON byte string. It may be present for
+ deprecated types. It represents a possible conversion of the deprecated type to a non-deprecated type, e.g. symbol
+ to string.
+- `converted_extjson`: (optional) a string containing a Canonical Extended JSON document. Because this is itself
+ embedded as a *string* inside a JSON document, characters like quote and backslash are escaped. It may be present
+ for deprecated types and is the Canonical Extended JSON representation of `converted_bson`.
+- `lossy` (optional) -- boolean; present (and true) iff `canonical_bson` can't be represented exactly with extended JSON
+ (e.g. NaN with a payload).
+
+#### Decode error case keys
+
+Decode error cases provide an invalid BSON document or field that should result in an error. For each case, keys
+include:
+
+- `description`: human-readable test case label.
+- `bson`: an (uppercase) big-endian hex representation of an invalid BSON string that should fail to decode correctly.
+
+#### Parse error case keys
+
+Parse error cases are type-specific and represent some input that can not be encoded to the `bson_type` under test. For
+each case, keys include:
+
+- `description`: human-readable test case label.
+- `string`: a text or numeric representation of an input that can't be parsed to a valid value of the given type.
+
+### Extended JSON encoding, escaping and ordering
+
+Because the `canonical_extjson` and other Extended JSON fields are embedded in a JSON document, all their JSON
+metacharacters are escaped. Control characters and non-ASCII codepoints are represented with `\uXXXX`. Note that this
+means that the corpus JSON will appear to have double-escaped characters `\\uXXXX`. This is by design to ensure that the
+Extended JSON fields remain printable ASCII without embedded null characters to ensure maximum portability to different
+language JSON or extended JSON decoders.
+
+There are legal differences in JSON representation that may complicate testing for particular codecs. The JSON in the
+corpus may not resemble the JSON generated by a codec, even though they represent the same data. Some known differences
+include:
+
+- JSON only requires certain characters to be escaped but allows any character to be escaped.
+- The JSON format is *unordered* and whitespace (outside of strings) is not significant.
+
+Implementations using these tests MUST normalize JSON comparisons however necessary for effective comparison.
+
+### Language-specific differences
+
+Some programming languages may not be able to represent or transmit all types accurately. In such cases, implementations
+SHOULD ignore (or modify) any tests which are not supported on that platform.
+
+### Testing validity
+
+To test validity of a case in the `valid` array, we consider up to five possible representations:
+
+- Canonical BSON (denoted herein as "cB") -- fully valid, spec-compliant BSON
+- Degenerate BSON (denoted herein as "dB") -- invalid but still parseable BSON (bad array keys, regex options out of
+ order)
+- Canonical Extended JSON (denoted herein as "cEJ") -- A string format based on the JSON standard that emphasizes type
+ preservation at the expense of readability and interoperability.
+- Degenerate Extended JSON (denoted herin as "dEJ") -- An invalid form of Canonical Extended JSON that is still
+ parseable. (For example, "1e100" instead of "1E+100".)
+- Relaxed Extended JSON (denoted herein as "rEJ") -- A string format based on the JSON standard that emphasizes
+ readability and interoperability at the expense of type preservation.
+
+Not all input types will exist for a given test case.
+
+There are two forms of BSON/Extended JSON codecs: ones that have a language-native "intermediate" representation and
+ones that do not.
+
+For a codec *without* an intermediate representation (i.e. one that translates directly from BSON to JSON or back), the
+following assertions MUST hold (function names are for clarity of illustration only):
+
+- for cB input:
+ - bson_to_canonical_extended_json(cB) = cEJ
+ - bson_to_relaxed_extended_json(cB) = rEJ (if rEJ exists)
+- for cEJ input:
+ - json_to_bson(cEJ) = cB (unless lossy)
+- for dB input (if it exists):
+ - bson_to_canonical_extended_json(dB) = cEJ
+ - bson_to_relaxed_extended_json(dB) = rEJ (if rEJ exists)
+- for dEJ input (if it exists):
+ - json_to_bson(dEJ) = cB (unless lossy)
+- for rEJ input (if it exists):
+ - bson_to_relaxed_extended_json( json_to_bson(rEJ) ) = rEJ
+
+For a codec that has a language-native representation, we want to test both conversion and round-tripping. For these
+codecs, the following assertions MUST hold (function names are for clarity of illustration only):
+
+- for cB input:
+ - native_to_bson( bson_to_native(cB) ) = cB
+ - native_to_canonical_extended_json( bson_to_native(cB) ) = cEJ
+ - native_to_relaxed_extended_json( bson_to_native(cB) ) = rEJ (if rEJ exists)
+- for cEJ input:
+ - native_to_canonical_extended_json( json_to_native(cEJ) ) = cEJ
+ - native_to_bson( json_to_native(cEJ) ) = cB (unless lossy)
+- for dB input (if it exists):
+ - native_to_bson( bson_to_native(dB) ) = cB
+- for dEJ input (if it exists):
+ - native_to_canonical_extended_json( json_to_native(dEJ) ) = cEJ
+ - native_to_bson( json_to_native(dEJ) ) = cB (unless lossy)
+- for rEJ input (if it exists):
+ - native_to_relaxed_extended_json( json_to_native(rEJ) ) = rEJ
+
+Implementations MAY test assertions in an implementation-specific manner.
+
+### Testing decode errors
+
+The `decodeErrors` cases represent BSON documents that are sufficiently incorrect that they can't be parsed even with
+liberal interpretation of the BSON schema (e.g. reading arrays with invalid keys is possible, even though technically
+invalid, so they are *not* `decodeErrors`).
+
+Drivers SHOULD test that each case results in a decoding error. Implementations MAY test assertions in an
+implementation-specific manner.
+
+### Testing parsing errors
+
+The interpretation of `parseErrors` is type-specific. The structure of test cases within `parseErrors` is described in
+[Parse error case keys](#parse-error-case-keys).
+
+Drivers SHOULD test that each case results in a parsing error (e.g. parsing Extended JSON, constructing a language
+type). Implementations MAY test assertions in an implementation-specific manner.
+
+#### Top-level Document (type 0x00)
+
+For type "0x00" (i.e. top-level documents), the `string` field contains input for an Extended JSON parser. Drivers MUST
+parse the Extended JSON input using an Extended JSON parser and verify that doing so yields an error. Drivers that parse
+Extended JSON into language types instead of directly to BSON MAY need to additionally convert the resulting language
+type(s) to BSON to expect an error.
+
+Drivers SHOULD also parse the Extended JSON input using a regular JSON parser (not an Extended JSON one) and verify the
+input is parsed successfully. This serves to verify that the `parseErrors` test cases are testing Extended JSON-specific
+error conditions and that they do not have, for example, unintended syntax errors.
+
+Note: due to the generic nature of these tests, they may also be used to test Extended JSON parsing errors for various
+BSON types appearing within a document.
+
+#### Binary (type 0x05)
+
+For type "0x05" (i.e. binary), the rules for handling `parseErrors` are the same as those for
+[Top-level Document (type 0x00)](#top-level-document-type-0x00).
+
+#### Decimal128 (type 0x13)
+
+For type "0x13" (i.e. Decimal128), the `string` field contains input for a Decimal128 parser that converts string input
+to a binary Decimal128 value (e.g. Decimal128 constructor). Drivers MUST assert that these strings cannot be
+successfully converted to a binary Decimal128 value and that parsing the string produces an error.
+
+### Deprecated types
+
+The corpus files for deprecated types are provided for informational purposes. Implementations MAY ignore or modify them
+to match legacy treatment of deprecated types. The `converted_bson` and `converted_extjson` fields MAY be used to test
+conversion to a standard type or MAY be ignored.
+
+## Prose Tests
+
+The following tests have not yet been automated, but MUST still be tested.
+
+### 1. Prohibit null bytes in null-terminated strings when encoding BSON
+
+The BSON spec uses null-terminated strings to represent document field names and regex components (i.e. pattern and
+flags/options). Drivers MUST assert that null bytes are prohibited in the following contexts when encoding BSON (i.e.
+creating raw BSON bytes or constructing BSON-specific type classes):
+
+- Field name within a root document
+- Field name within a sub-document
+- Pattern for a regular expression
+- Flags/options for a regular expression
+
+Depending on how drivers implement BSON encoding, they MAY expect an error when constructing a type class (e.g. BSON
+Document or Regex class) or when encoding a language representation to BSON (e.g. converting a dictionary, which might
+allow null bytes in its keys, to raw BSON bytes).
+
+## Implementation Notes
+
+### A tool for visualizing BSON
+
+The test directory includes a Perl script `bsonview`, which will decompose and highlight elements of a BSON document. It
+may be used like this:
+
+```bash
+echo "0900000010610005000000" | perl bsonview -x
+```
+
+### Notes for certain types
+
+#### Array
+
+Arrays can have degenerate BSON if the array indexes are not set as "0", "1", etc.
+
+#### Boolean
+
+The only valid values are 0 and 1. Other non-zero numbers MUST be interpreted as errors rather than "true" values.
+
+#### Binary
+
+The Base64 encoded text in the extended JSON representation MUST be padded.
+
+#### Code
+
+There are multiple ways to encode Unicode characters as a JSON document. Individual implementers may need to normalize
+provided and generated extended JSON before comparison.
+
+#### Decimal
+
+NaN with payload can't be represented in extended JSON, so such conversions are lossy.
+
+#### Double
+
+There is not yet a way to represent Inf, -Inf or NaN in extended JSON. Even if a `$numberDouble` is added, it is
+unlikely to support special values with payloads, so such doubles would be lossy when converted to extended JSON.
+
+String representation of doubles is fairly unportable so it's hard to provide a single string that all
+platforms/languages will generate. Testers may need to normalize/modify the test cases.
+
+#### String
+
+There are multiple ways to encode Unicode characters as a JSON document. Individual implementers may need to normalize
+provided and generated extended JSON before comparison.
+
+#### DBPointer
+
+This type is deprecated. The provided converted form (`converted_bson`) represents them as DBRef documents, but such
+conversion is outside the scope of this spec.
+
+#### Symbol
+
+This type is deprecated. The provided converted form converts these to strings, but such conversion is outside the scope
+of this spec.
+
+#### Undefined
+
+This type is deprecated. The provided converted form converts these to Null, but such conversion is outside the scope of
+this spec.
+
+## Reference Implementation
+
+The Java, C# and Perl drivers.
+
+## Design Rationale
+
+### Use of extjson
+
+Testing conversion requires an "input" and an "output". With a BSON string as both input and output, we can only test
+that it roundtrips correctly --we can't test that the decoded value visible to the language is correct.
+
+For example, a pathological encoder/decoder could invert Boolean true and false during decoding and encoding. The BSON
+would roundtrip but the program would see the wrong values.
+
+Therefore, we need a separate, semantic description of the contents of a BSON string in a machine readable format.
+Fortunately, we already have extjson as a means of doing so. The extended JSON strings contained within the tests adhere
+to the Extended JSON Specification.
+
+### Repetition across cases
+
+Some validity cases may result in duplicate assertions across cases, particularly if the `degenerate_bson` field is
+different in different cases, but the `canonical_bson` field is the same. This is by design so that each case stands
+alone and can be confirmed to be internally consistent via the assertions. This makes for easier and safer test case
+development.
+
+## Changelog
+
+- 2024-01-22: Migrated from reStructuredText to Markdown.
+
+- 2023-06-14: Add decimal128 Extended JSON parse tests for clamped zeros with very large exponents.
+
+- 2022-10-05: Remove spec front matter and reformat changelog.
+
+- 2021-09-09: Clarify error expectation rules for `parseErrors`.
+
+- 2021-09-02: Add spec and prose tests for prohibiting null bytes in null-terminated strings within document field names
+ and regular expressions. Clarify type-specific rules for `parseErrors`.
+
+- 2017-05-26: Revised to be consistent with Extended JSON spec 2.0: valid case fields have changed, as have the test
+ assertions.
+
+- 2017-01-23: Added `multi-type.json` to test encoding and decoding all BSON types within the same document. Amended all
+ extended JSON strings to adhere to the Extended JSON Specification. Modified the "Use of extjson" section of this
+ specification to note that canonical extended JSON is now used.
+
+- 2016-11-14: Removed "invalid flags" BSON Regexp case.
+
+- 2016-10-25: Added a "non-alphabetized flags" case to the BSON Regexp corpus file; decoders must be able to read
+ non-alphabetized flags, but encoders must emit alphabetized flags. Added an "invalid flags" case to the BSON Regexp
+ corpus file.
diff --git a/source/bson-corpus/bson-corpus.rst b/source/bson-corpus/bson-corpus.rst
index b3743b2681..a388d8533d 100644
--- a/source/bson-corpus/bson-corpus.rst
+++ b/source/bson-corpus/bson-corpus.rst
@@ -1,476 +1,4 @@
-===========
-BSON Corpus
-===========
-:Title: BSON Corpus
-:Author: Luke Lovett, David Golden
-:Lead: Jeff Yemin
-:Advisors: Craig Wilson
-:Status: Approved
-:Type: Standards
-:Minimum Server Version: N/A
-:Last Modified: July 20, 2017
-:Version: 2.0
-
-.. contents::
-
-Abstract
-========
-
-The official BSON specification does not include test data, so this
-pseudo-specification describes tests for BSON encoding and decoding. It also
-includes tests for MongoDB's "Extended JSON" specification (hereafter
-abbreviated as ``extjson``).
-
-Meta
-====
-
-The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD",
-"SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be
-interpreted as described in `RFC 2119`_.
-
-.. _RFC 2119: https://www.ietf.org/rfc/rfc2119.txt
-
-Motivation for Change
-=====================
-
-To ensure correct operation, we want drivers to implement identical tests
-for important features. BSON (and ``extjson``) are critical for correct
-operation and data exchange, but historically had no common test corpus.
-This pseudo-specification provides such tests.
-
-Goals
------
-
-* Provide machine-readable test data files for BSON and ``extjson`` encoding
- and decoding.
-
-* Cover all current and historical BSON types.
-
-* Define test data patterns for three cases: (a) conversion/roundtrip, (b)
- decode errors, and (c) parse errors.
-
-Non-Goals
----------
-
-* Replace or extend the offical BSON spec at http://bsonspec.org.
-
-* Provide a formal specification for ``extjson``.
-
-Specification
-=============
-
-The specification for BSON lives at http://bsonspec.org. The ``extjson``
-format specification lives at
-https://github.com/mongodb/specifications/blob/master/source/extended-json.rst.
-
-Test Plan
-=========
-
-This test plan describes a general approach for BSON testing. Future BSON
-specifications (such as for new types like Decimal128) may specialize or
-alter the approach described below.
-
-Description of the BSON Corpus
-------------------------------
-
-This BSON test data corpus consists of a JSON file for each BSON type, plus
-a ``top.json`` file for testing the overall, enclosing document and a
-``multi-type.json`` file for testing a document with all BSON types.
-There is also a ``multi-type-deprecated.json`` that includes deprecated keys.
-
-Top level keys
-~~~~~~~~~~~~~~
-
-* ``description``: human-readable description of what is in the file
-
-* ``bson_type``: hex string of the first byte of a BSON element (e.g. "0x01"
- for type "double"); this will be the synthetic value "0x00" for "whole
- document" tests like ``top.json``.
-
-* ``test_key``: (optional) name of a field in a single-BSON-type ``valid`` test
- case that contains the data type being tested.
-
-* ``valid`` (optional): an array of validity test cases (see below).
-
-* ``decodeErrors`` (optional): an array of decode error cases (see below).
-
-* ``parseErrors`` (optional): an array of type-specific parse error case (see
- below).
-
-* ``deprecated`` (optional): this field will be present (and true) if the
- BSON type has been deprecated (i.e. Symbol, Undefined and DBPointer)
-
-Validity test case keys
-~~~~~~~~~~~~~~~~~~~~~~~
-
-Validity test cases include 'canonical' forms of BSON and Extended JSON that
-are deemed equivalent and may provide additional cases or metadata for
-additional assertions. For each case, keys include:
-
-* ``description``: human-readable test case label.
-
-* ``canonical_bson``: an (uppercase) big-endian hex representation of a BSON
- byte string. Be sure to mangle the case as appropriate in any roundtrip
- tests.
-
-* ``canonical_extjson``: a string containing a Canonical Extended JSON document.
- Because this is itself embedded as a *string* inside a JSON document,
- characters like quote and backslash are escaped.
-
-* ``relaxed_extjson``: (optional) a string containing a Relaxed Extended JSON
- document. Because this is itself embedded as a *string* inside a JSON
- document, characters like quote and backslash are escaped.
-
-* ``degenerate_bson``: (optional) an (uppercase) big-endian hex representation
- of a BSON byte string that is technically parseable, but not in compliance
- with the BSON spec. Be sure to mangle the case as appropriate in any
- roundtrip tests.
-
-* ``degenerate_extjson``: (optional) a string containing an invalid form of
- Canonical Extended JSON that is still parseable according to type-specific
- rules. (For example, "1e100" instead of "1E+100".)
-
-* ``converted_bson``: (optional) an (uppercase) big-endian hex representation
- of a BSON byte string. It may be present for deprecated types. It represents
- a possible conversion of the deprecated type to a non-deprecated type, e.g.
- symbol to string.
-
-* ``converted_extjson``: (optional) a string containing a Canonical Extended
- JSON document. Because this is itself embedded as a *string* inside a JSON
- document, characters like quote and backslash are escaped. It may be
- present for deprecated types and is the Canonical Extended JSON
- representation of ``converted_bson`.
-
-* ``lossy`` (optional) -- boolean; present (and true) iff ``canonical_bson``
- can't be represented exactly with extended JSON (e.g. NaN with a payload).
-
-Decode error case keys
-~~~~~~~~~~~~~~~~~~~~~~
-
-Decode error cases provide an invalid BSON document or field that
-should result in an error. For each case, keys include:
-
-* ``description``: human-readable test case label.
-
-* ``bson``: an (uppercase) big-endian hex representation of an invalid
- BSON string that should fail to decode correctly.
-
-Parse error case keys
-~~~~~~~~~~~~~~~~~~~~~
-
-Parse error cases are type-specific and represent some input that can not
-be encoded to the ``bson_type`` under test. For each case, keys include:
-
-* ``description``: human-readable test case label.
-
-* ``string``: a text or numeric representation of an input that can't be
- parsed to a valid value of the given type.
-
-Extended JSON encoding, escaping and ordering
----------------------------------------------
-
-Because the ``canonical_extjson`` and other Extended JSON fields are embedded
-in a JSON document, all their JSON metacharacters are escaped. Control
-characters and non-ASCII codepoints are represented with ``\uXXXX``. Note that
-this means that the corpus JSON will appear to have double-escaped characters
-``\\uXXXX``. This is by design to ensure that the Extended JSON fields remain
-printable ASCII without embedded null characters to ensure maximum portability
-to different language JSON or extended JSON decoders.
-
-There are legal differences in JSON representation that may complicate
-testing for particular codecs. The JSON in the corpus may not resemble
-the JSON generated by a codec, even though they represent the same data.
-Some known differences include:
-
-* JSON only requires certain characters to be escaped but allows any character
- to be escaped.
-
-* The JSON format is *unordered* and whitespace (outside of strings) is not
- significant.
-
-Implementations using these tests MUST normalize JSON comparisons however
-necessary for effective comparison.
-
-Language-specific differences
------------------------------
-
-Some programming languages may not be able to represent or transmit all
-types accurately. In such cases, implementations SHOULD ignore (or modify)
-any tests which are not supported on that platform.
-
-Testing validity
-----------------
-
-To test validity of a case in the ``valid`` array, we consider up to five
-possible representations:
-
-* Canonical BSON (denoted herein as "cB") -- fully valid, spec-compliant BSON
-
-* Degenerate BSON (denoted herein as "dB") -- invalid but still parseable BSON
- (bad array keys, regex options out of order)
-
-* Canonical Extended JSON (denoted herein as "cEJ") -- A string format based on
- the JSON standard that emphasizes type preservation at the expense of
- readability and interoperability.
-
-* Degenerate Extended JSON (denoted herin as "dEJ") -- An invalid form of
- Canonical Extended JSON that is still parseable. (For example, "1e100"
- instead of "1E+100".)
-
-* Relaxed Extended JSON (denoted herein as "rEJ") -- A string format based on
- the JSON standard that emphasizes readability and interoperability at the
- expense of type preservation.
-
-Not all input types will exist for a given test case.
-
-There are two forms of BSON/Extended JSON codecs: ones that have a language-native
-"intermediate" representation and ones that do not.
-
-For a codec *without* an intermediate representation (i.e. one that translates
-directly from BSON to JSON or back), the following assertions MUST hold
-(function names are for clarity of illustration only):
-
-* for cB input:
-
- * bson_to_canonical_extended_json(cB) = cEJ
-
- * bson_to_relaxed_extended_json(cB) = rEJ (if rEJ exists)
-
-* for cEJ input:
-
- * json_to_bson(cEJ) = cB (unless lossy)
-
-* for dB input (if it exists):
-
- * bson_to_canonical_extended_json(dB) = cEJ
-
- * bson_to_relaxed_extended_json(dB) = rEJ (if rEJ exists)
-
-* for dEJ input (if it exists):
-
- * json_to_bson(dEJ) = cB (unless lossy)
-
-* for rEJ input (if it exists):
-
- * bson_to_relaxed_extended_json( json_to_bson(rEJ) ) = rEJ
-
-For a codec that has a language-native representation, we want to test both
-conversion and round-tripping. For these codecs, the following assertions MUST
-hold (function names are for clarity of illustration only):
-
-* for cB input:
-
- * native_to_bson( bson_to_native(cB) ) = cB
-
- * native_to_canonical_extended_json( bson_to_native(cB) ) = cEJ
-
- * native_to_relaxed_extended_json( bson_to_native(cB) ) = rEJ (if rEJ exists)
-
-* for cEJ input:
-
- * native_to_canonical_extended_json( json_to_native(cEJ) ) = cEJ
-
- * native_to_bson( json_to_native(cEJ) ) = cB (unless lossy)
-
-* for dB input (if it exists):
-
- * native_to_bson( bson_to_native(dB) ) = cB
-
-* for dEJ input (if it exists):
-
- * native_to_canonical_extended_json( json_to_native(dEJ) ) = cEJ
-
- * native_to_bson( json_to_native(dEJ) ) = cB (unless lossy)
-
-* for rEJ input (if it exists):
-
- * native_to_relaxed_extended_json( json_to_native(rEJ) ) = rEJ
-
-Implementations MAY test assertions in an implementation-specific
-manner.
-
-Testing decode errors
----------------------
-
-The ``decodeErrors`` cases represent BSON documents that are sufficiently
-incorrect that they can't be parsed even with liberal interpretation of
-the BSON schema (e.g. reading arrays with invalid keys is possible, even
-though technically invalid, so they are *not* ``decodeErrors``).
-
-Drivers SHOULD test that each case results in a decoding error.
-Implementations MAY test assertions in an implementation-specific
-manner.
-
-Testing parsing errors
-----------------------
-
-The interpretation of ``parseErrors`` is type-specific. For example,
-helpers for creating Decimal128 values may parse strings to convert them
-to binary Decimal128 values. The ``parseErrors`` cases are strings that
-will *not* convert correctly.
-
-The documentation for a type (if any) will specify how to use these
-cases for testing.
-
-For type "0x00" (i.e. top-level documents), the ``parseErrors`` entries have a
-``description`` field and an ``string`` field. Parsing the ``string`` field
-as Extended JSON MUST result in an error.
-
-Drivers SHOULD test that each case results in a parse error.
-Implementations MAY test assertions in an implementation-specific
-manner.
-
-Deprecated types
-----------------
-
-The corpus files for deprecated types are provided for informational purposes.
-Implementations MAY ignore or modify them to match legacy treatment of
-deprecated types. The ``converted_bson`` and ``converted_extjson`` fields MAY
-be used to test conversion to a standard type or MAY be ignored.
-
-Implementation Notes
-====================
-
-A tool for visualizing BSON
----------------------------
-
-The test directory includes a Perl script ``bsonview``, which will
-decompose and highlight elements of a BSON document. It may be used like
-this::
-
- echo "0900000010610005000000" | perl bsonview -x
-
-Notes for certain types
------------------------
-
-Array
-~~~~~
-
-Arrays can have degenerate BSON if the array indexes are not set as
-"0", "1", etc.
-
-Boolean
-~~~~~~~
-
-The only valid values are 0 and 1. Other non-zero numbers MUST be
-interpreted as errors rather than "true" values.
-
-Binary
-~~~~~~
-
-The Base64 encoded text in the extended JSON representation MUST be padded.
-
-Code
-~~~~
-
-There are multiple ways to encode Unicode characters as a JSON document.
-Individual implementers may need to normalize provided and generated
-extended JSON before comparison.
-
-Decimal
-~~~~~~~
-
-NaN with payload can't be represented in extended JSON, so such conversions are
-lossy.
-
-Double
-~~~~~~
-
-There is not yet a way to represent Inf, -Inf or NaN in extended JSON. Even if
-a $numberDouble is added, it is unlikely to support special values with
-payloads, so such doubles would be lossy when converted to extended JSON.
-
-String representation of doubles is fairly unportable so it's hard to provide
-a single string that all platforms/languages will generate. Testers may
-need to normalize/modify the test cases.
-
-String
-~~~~~~
-
-There are multiple ways to encode Unicode characters as a JSON document.
-Individual implementers may need to normalize provided and generated
-extended JSON before comparison.
-
-DBPointer
-~~~~~~~~~
-
-This type is deprecated. The provided converted form (``converted_bson``)
-represents them as DBRef documents, but such conversion is outside the scope of
-this spec.
-
-Symbol
-~~~~~~
-
-This type is deprecated. The provided converted form converts these to
-strings, but such conversion is outside the scope of this spec.
-
-Undefined
-~~~~~~~~~
-
-This type is deprecated. The provided converted form converts these to Null,
-but such conversion is outside the scope of this spec.
-
-Reference Implementation
-========================
-
-The Java, C# and Perl drivers.
-
-Design Rationale
-================
-
-Use of extjson
---------------
-
-Testing conversion requires an "input" and an "output". With a BSON string
-as both input and output, we can only test that it roundtrips correctly --
-we can't test that the decoded value visible to the language is correct.
-
-For example, a pathological encoder/decoder could invert Boolean true and
-false during decoding and encoding. The BSON would roundtrip but the
-program would see the wrong values.
-
-Therefore, we need a separate, semantic description of the contents of a BSON
-string in a machine readable format. Fortunately, we already have extjson as a
-means of doing so. The extended JSON strings contained within the tests adhere
-to the Extended JSON Specification.
-
-Repetition across cases
------------------------
-
-Some validity cases may result in duplicate assertions across cases,
-particularly if the ``degenerate_bson`` field is different in different cases,
-but the ``canonical_bson`` field is the same. This is by design so that each
-case stands alone and can be confirmed to be internally consistent via the
-assertions. This makes for easier and safer test case development.
-
-Changes
-=======
-
-Version 2.0 - May 26, 2017
-
-* Revised to be consistent with Extended JSON spec 2.0: valid case fields
- have changed, as have the test assertions.
-
-Version 1.3 - January 23, 2017
-
-* Added ``multi-type.json`` to test encoding and decoding all BSON types within
- the same document.
-
-* Amended all extended JSON strings to adhere to the Extended JSON
- Specification.
-
-* Modified the "Use of extjson" section of this specification to note that
- canonical extended JSON is now used.
-
-Version 1.2 - November 14, 2016
-
-* Removed "invalid flags" BSON Regexp case.
-
-Version 1.1 – October 25, 2016
-
-* Added a "non-alphabetized flags" case to the BSON Regexp corpus file;
- decoders must be able to read non-alphabetized flags, but encoders must
- emit alphabetized flags.
-
-* Added an "invalid flags" case to the BSON Regexp corpus file.
+.. note::
+ This specification has been converted to Markdown and renamed to
+ `bson-corpus.md `_.
diff --git a/source/bson-corpus/tests/array.json b/source/bson-corpus/tests/array.json
index 1c654cf36b..9ff953e5ae 100644
--- a/source/bson-corpus/tests/array.json
+++ b/source/bson-corpus/tests/array.json
@@ -14,16 +14,22 @@
"canonical_extjson": "{\"a\" : [{\"$numberInt\": \"10\"}]}"
},
{
- "description": "Single Element Array with index set incorrectly",
+ "description": "Single Element Array with index set incorrectly to empty string",
"degenerate_bson": "130000000461000B00000010000A0000000000",
"canonical_bson": "140000000461000C0000001030000A0000000000",
"canonical_extjson": "{\"a\" : [{\"$numberInt\": \"10\"}]}"
},
{
- "description": "Single Element Array with index set incorrectly",
+ "description": "Single Element Array with index set incorrectly to ab",
"degenerate_bson": "150000000461000D000000106162000A0000000000",
"canonical_bson": "140000000461000C0000001030000A0000000000",
"canonical_extjson": "{\"a\" : [{\"$numberInt\": \"10\"}]}"
+ },
+ {
+ "description": "Multi Element Array with duplicate indexes",
+ "degenerate_bson": "1b000000046100130000001030000a000000103000140000000000",
+ "canonical_bson": "1b000000046100130000001030000a000000103100140000000000",
+ "canonical_extjson": "{\"a\" : [{\"$numberInt\": \"10\"}, {\"$numberInt\": \"20\"}]}"
}
],
"decodeErrors": [
diff --git a/source/bson-corpus/tests/binary.json b/source/bson-corpus/tests/binary.json
index 90a15c1a1c..0e0056f3a2 100644
--- a/source/bson-corpus/tests/binary.json
+++ b/source/bson-corpus/tests/binary.json
@@ -39,11 +39,27 @@
"canonical_bson": "1D000000057800100000000473FFD26444B34C6990E8E7D1DFC035D400",
"canonical_extjson": "{\"x\" : { \"$binary\" : {\"base64\" : \"c//SZESzTGmQ6OfR38A11A==\", \"subType\" : \"04\"}}}"
},
+ {
+ "description": "subtype 0x04 UUID",
+ "canonical_bson": "1D000000057800100000000473FFD26444B34C6990E8E7D1DFC035D400",
+ "canonical_extjson": "{\"x\" : { \"$binary\" : {\"base64\" : \"c//SZESzTGmQ6OfR38A11A==\", \"subType\" : \"04\"}}}",
+ "degenerate_extjson": "{\"x\" : { \"$uuid\" : \"73ffd264-44b3-4c69-90e8-e7d1dfc035d4\"}}"
+ },
{
"description": "subtype 0x05",
"canonical_bson": "1D000000057800100000000573FFD26444B34C6990E8E7D1DFC035D400",
"canonical_extjson": "{\"x\" : { \"$binary\" : {\"base64\" : \"c//SZESzTGmQ6OfR38A11A==\", \"subType\" : \"05\"}}}"
},
+ {
+ "description": "subtype 0x07",
+ "canonical_bson": "1D000000057800100000000773FFD26444B34C6990E8E7D1DFC035D400",
+ "canonical_extjson": "{\"x\" : { \"$binary\" : {\"base64\" : \"c//SZESzTGmQ6OfR38A11A==\", \"subType\" : \"07\"}}}"
+ },
+ {
+ "description": "subtype 0x08",
+ "canonical_bson": "1D000000057800100000000873FFD26444B34C6990E8E7D1DFC035D400",
+ "canonical_extjson": "{\"x\" : { \"$binary\" : {\"base64\" : \"c//SZESzTGmQ6OfR38A11A==\", \"subType\" : \"08\"}}}"
+ },
{
"description": "subtype 0x80",
"canonical_bson": "0F0000000578000200000080FFFF00",
@@ -58,6 +74,36 @@
"description": "$type query operator (conflicts with legacy $binary form with $type field)",
"canonical_bson": "180000000378001000000010247479706500020000000000",
"canonical_extjson": "{\"x\" : { \"$type\" : {\"$numberInt\": \"2\"}}}"
+ },
+ {
+ "description": "subtype 0x09 Vector FLOAT32",
+ "canonical_bson": "170000000578000A0000000927000000FE420000E04000",
+ "canonical_extjson": "{\"x\": {\"$binary\": {\"base64\": \"JwAAAP5CAADgQA==\", \"subType\": \"09\"}}}"
+ },
+ {
+ "description": "subtype 0x09 Vector INT8",
+ "canonical_bson": "11000000057800040000000903007F0700",
+ "canonical_extjson": "{\"x\": {\"$binary\": {\"base64\": \"AwB/Bw==\", \"subType\": \"09\"}}}"
+ },
+ {
+ "description": "subtype 0x09 Vector PACKED_BIT",
+ "canonical_bson": "11000000057800040000000910007F0700",
+ "canonical_extjson": "{\"x\": {\"$binary\": {\"base64\": \"EAB/Bw==\", \"subType\": \"09\"}}}"
+ },
+ {
+ "description": "subtype 0x09 Vector (Zero-length) FLOAT32",
+ "canonical_bson": "0F0000000578000200000009270000",
+ "canonical_extjson": "{\"x\": {\"$binary\": {\"base64\": \"JwA=\", \"subType\": \"09\"}}}"
+ },
+ {
+ "description": "subtype 0x09 Vector (Zero-length) INT8",
+ "canonical_bson": "0F0000000578000200000009030000",
+ "canonical_extjson": "{\"x\": {\"$binary\": {\"base64\": \"AwA=\", \"subType\": \"09\"}}}"
+ },
+ {
+ "description": "subtype 0x09 Vector (Zero-length) PACKED_BIT",
+ "canonical_bson": "0F0000000578000200000009100000",
+ "canonical_extjson": "{\"x\": {\"$binary\": {\"base64\": \"EAA=\", \"subType\": \"09\"}}}"
}
],
"decodeErrors": [
@@ -81,5 +127,27 @@
"description": "subtype 0x02 length negative one",
"bson": "130000000578000600000002FFFFFFFFFFFF00"
}
+ ],
+ "parseErrors": [
+ {
+ "description": "$uuid wrong type",
+ "string": "{\"x\" : { \"$uuid\" : { \"data\" : \"73ffd264-44b3-4c69-90e8-e7d1dfc035d4\"}}}"
+ },
+ {
+ "description": "$uuid invalid value--too short",
+ "string": "{\"x\" : { \"$uuid\" : \"73ffd264-44b3-90e8-e7d1dfc035d4\"}}"
+ },
+ {
+ "description": "$uuid invalid value--too long",
+ "string": "{\"x\" : { \"$uuid\" : \"73ffd264-44b3-4c69-90e8-e7d1dfc035d4-789e4\"}}"
+ },
+ {
+ "description": "$uuid invalid value--misplaced hyphens",
+ "string": "{\"x\" : { \"$uuid\" : \"73ff-d26444b-34c6-990e8e-7d1dfc035d4\"}}"
+ },
+ {
+ "description": "$uuid invalid value--too many hyphens",
+ "string": "{\"x\" : { \"$uuid\" : \"----d264-44b3-4--9-90e8-e7d1dfc0----\"}}"
+ }
]
}
diff --git a/source/bson-corpus/tests/code.json b/source/bson-corpus/tests/code.json
index 6f37349ad0..b8482b2541 100644
--- a/source/bson-corpus/tests/code.json
+++ b/source/bson-corpus/tests/code.json
@@ -20,48 +20,48 @@
},
{
"description": "two-byte UTF-8 (\u00e9)",
- "canonical_bson": "190000000261000D000000C3A9C3A9C3A9C3A9C3A9C3A90000",
- "canonical_extjson": "{\"a\" : \"\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\"}"
+ "canonical_bson": "190000000D61000D000000C3A9C3A9C3A9C3A9C3A9C3A90000",
+ "canonical_extjson": "{\"a\" : {\"$code\" : \"\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\"}}"
},
{
"description": "three-byte UTF-8 (\u2606)",
- "canonical_bson": "190000000261000D000000E29886E29886E29886E298860000",
- "canonical_extjson": "{\"a\" : \"\\u2606\\u2606\\u2606\\u2606\"}"
+ "canonical_bson": "190000000D61000D000000E29886E29886E29886E298860000",
+ "canonical_extjson": "{\"a\" : {\"$code\" : \"\\u2606\\u2606\\u2606\\u2606\"}}"
},
{
"description": "Embedded nulls",
- "canonical_bson": "190000000261000D0000006162006261620062616261620000",
- "canonical_extjson": "{\"a\" : \"ab\\u0000bab\\u0000babab\"}"
+ "canonical_bson": "190000000D61000D0000006162006261620062616261620000",
+ "canonical_extjson": "{\"a\" : {\"$code\" : \"ab\\u0000bab\\u0000babab\"}}"
}
],
"decodeErrors": [
{
"description": "bad code string length: 0 (but no 0x00 either)",
- "bson": "0C0000000261000000000000"
+ "bson": "0C0000000D61000000000000"
},
{
"description": "bad code string length: -1",
- "bson": "0C000000026100FFFFFFFF00"
+ "bson": "0C0000000D6100FFFFFFFF00"
},
{
"description": "bad code string length: eats terminator",
- "bson": "10000000026100050000006200620000"
+ "bson": "100000000D6100050000006200620000"
},
{
"description": "bad code string length: longer than rest of document",
- "bson": "120000000200FFFFFF00666F6F6261720000"
+ "bson": "120000000D00FFFFFF00666F6F6261720000"
},
{
"description": "code string is not null-terminated",
- "bson": "1000000002610004000000616263FF00"
+ "bson": "100000000D610004000000616263FF00"
},
{
"description": "empty code string, but extra null",
- "bson": "0E00000002610001000000000000"
+ "bson": "0E0000000D610001000000000000"
},
{
"description": "invalid UTF-8",
- "bson": "0E00000002610002000000E90000"
+ "bson": "0E0000000D610002000000E90000"
}
]
}
diff --git a/source/bson-corpus/tests/datetime.json b/source/bson-corpus/tests/datetime.json
index 60506ce174..1554341d29 100644
--- a/source/bson-corpus/tests/datetime.json
+++ b/source/bson-corpus/tests/datetime.json
@@ -24,7 +24,14 @@
{
"description" : "Y10K",
"canonical_bson" : "1000000009610000DC1FD277E6000000",
+ "relaxed_extjson" : "{\"a\":{\"$date\":{\"$numberLong\":\"253402300800000\"}}}",
"canonical_extjson" : "{\"a\":{\"$date\":{\"$numberLong\":\"253402300800000\"}}}"
+ },
+ {
+ "description": "leading zero ms",
+ "canonical_bson": "10000000096100D1D6D6CC3B01000000",
+ "relaxed_extjson": "{\"a\" : {\"$date\" : \"2012-12-24T12:15:30.001Z\"}}",
+ "canonical_extjson": "{\"a\" : {\"$date\" : {\"$numberLong\" : \"1356351330001\"}}}"
}
],
"decodeErrors": [
diff --git a/source/bson-corpus/tests/dbref.json b/source/bson-corpus/tests/dbref.json
index 1fe12c6f68..41c0b09d0e 100644
--- a/source/bson-corpus/tests/dbref.json
+++ b/source/bson-corpus/tests/dbref.json
@@ -1,5 +1,5 @@
{
- "description": "DBRef",
+ "description": "Document type (DBRef sub-documents)",
"bson_type": "0x03",
"valid": [
{
@@ -26,6 +26,26 @@
"description": "Document with key names similar to those of a DBRef",
"canonical_bson": "3e0000000224726566000c0000006e6f742d612d646272656600072469640058921b3e6e32ab156a22b59e022462616e616e6100050000007065656c0000",
"canonical_extjson": "{\"$ref\": \"not-a-dbref\", \"$id\": {\"$oid\": \"58921b3e6e32ab156a22b59e\"}, \"$banana\": \"peel\"}"
+ },
+ {
+ "description": "DBRef with additional dollar-prefixed and dotted fields",
+ "canonical_bson": "48000000036462726566003c0000000224726566000b000000636f6c6c656374696f6e00072469640058921b3e6e32ab156a22b59e10612e62000100000010246300010000000000",
+ "canonical_extjson": "{\"dbref\": {\"$ref\": \"collection\", \"$id\": {\"$oid\": \"58921b3e6e32ab156a22b59e\"}, \"a.b\": {\"$numberInt\": \"1\"}, \"$c\": {\"$numberInt\": \"1\"}}}"
+ },
+ {
+ "description": "Sub-document resembles DBRef but $id is missing",
+ "canonical_bson": "26000000036462726566001a0000000224726566000b000000636f6c6c656374696f6e000000",
+ "canonical_extjson": "{\"dbref\": {\"$ref\": \"collection\"}}"
+ },
+ {
+ "description": "Sub-document resembles DBRef but $ref is not a string",
+ "canonical_bson": "2c000000036462726566002000000010247265660001000000072469640058921b3e6e32ab156a22b59e0000",
+ "canonical_extjson": "{\"dbref\": {\"$ref\": {\"$numberInt\": \"1\"}, \"$id\": {\"$oid\": \"58921b3e6e32ab156a22b59e\"}}}"
+ },
+ {
+ "description": "Sub-document resembles DBRef but $db is not a string",
+ "canonical_bson": "4000000003646272656600340000000224726566000b000000636f6c6c656374696f6e00072469640058921b3e6e32ab156a22b59e1024646200010000000000",
+ "canonical_extjson": "{\"dbref\": {\"$ref\": \"collection\", \"$id\": {\"$oid\": \"58921b3e6e32ab156a22b59e\"}, \"$db\": {\"$numberInt\": \"1\"}}}"
}
]
}
diff --git a/source/bson-corpus/tests/decimal128-1.json b/source/bson-corpus/tests/decimal128-1.json
index 7eefec6bf7..8e7fbc93c6 100644
--- a/source/bson-corpus/tests/decimal128-1.json
+++ b/source/bson-corpus/tests/decimal128-1.json
@@ -312,6 +312,30 @@
"canonical_bson": "18000000136400000000000a5bc138938d44c64d31cc3700",
"degenerate_extjson": "{\"d\" : {\"$numberDecimal\" : \"1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}}",
"canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"1.000000000000000000000000000000000E+999\"}}"
+ },
+ {
+ "description": "Clamped zeros with a large positive exponent",
+ "canonical_bson": "180000001364000000000000000000000000000000FE5F00",
+ "degenerate_extjson": "{\"d\" : {\"$numberDecimal\" : \"0E+2147483647\"}}",
+ "canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"0E+6111\"}}"
+ },
+ {
+ "description": "Clamped zeros with a large negative exponent",
+ "canonical_bson": "180000001364000000000000000000000000000000000000",
+ "degenerate_extjson": "{\"d\" : {\"$numberDecimal\" : \"0E-2147483647\"}}",
+ "canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"0E-6176\"}}"
+ },
+ {
+ "description": "Clamped negative zeros with a large positive exponent",
+ "canonical_bson": "180000001364000000000000000000000000000000FEDF00",
+ "degenerate_extjson": "{\"d\" : {\"$numberDecimal\" : \"-0E+2147483647\"}}",
+ "canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"-0E+6111\"}}"
+ },
+ {
+ "description": "Clamped negative zeros with a large negative exponent",
+ "canonical_bson": "180000001364000000000000000000000000000000008000",
+ "degenerate_extjson": "{\"d\" : {\"$numberDecimal\" : \"-0E-2147483647\"}}",
+ "canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"-0E-6176\"}}"
}
]
}
diff --git a/source/bson-corpus/tests/document.json b/source/bson-corpus/tests/document.json
index 3ec9187044..698e7ae90a 100644
--- a/source/bson-corpus/tests/document.json
+++ b/source/bson-corpus/tests/document.json
@@ -17,6 +17,26 @@
"description": "Single-character key subdoc",
"canonical_bson": "160000000378000E0000000261000200000062000000",
"canonical_extjson": "{\"x\" : {\"a\" : \"b\"}}"
+ },
+ {
+ "description": "Dollar-prefixed key in sub-document",
+ "canonical_bson": "170000000378000F000000022461000200000062000000",
+ "canonical_extjson": "{\"x\" : {\"$a\" : \"b\"}}"
+ },
+ {
+ "description": "Dollar as key in sub-document",
+ "canonical_bson": "160000000378000E0000000224000200000061000000",
+ "canonical_extjson": "{\"x\" : {\"$\" : \"a\"}}"
+ },
+ {
+ "description": "Dotted key in sub-document",
+ "canonical_bson": "180000000378001000000002612E62000200000063000000",
+ "canonical_extjson": "{\"x\" : {\"a.b\" : \"c\"}}"
+ },
+ {
+ "description": "Dot as key in sub-document",
+ "canonical_bson": "160000000378000E000000022E000200000061000000",
+ "canonical_extjson": "{\"x\" : {\".\" : \"a\"}}"
}
],
"decodeErrors": [
@@ -31,6 +51,10 @@
{
"description": "Invalid subdocument: bad string length in field",
"bson": "1C00000003666F6F001200000002626172000500000062617A000000"
+ },
+ {
+ "description": "Null byte in sub-document key",
+ "bson": "150000000378000D00000010610000010000000000"
}
]
}
diff --git a/source/bson-corpus/tests/double.json b/source/bson-corpus/tests/double.json
index 00f75196fe..d5b8fb3d7e 100644
--- a/source/bson-corpus/tests/double.json
+++ b/source/bson-corpus/tests/double.json
@@ -28,16 +28,16 @@
"relaxed_extjson": "{\"d\" : -1.0001220703125}"
},
{
- "description": "1.23456789012345677E+18",
- "canonical_bson": "1000000001640081E97DF41022B14300",
- "canonical_extjson": "{\"d\" : {\"$numberDouble\": \"1.23456789012345677E+18\"}}",
- "relaxed_extjson": "{\"d\" : 1.23456789012345677E+18}"
+ "description": "1.2345678921232E+18",
+ "canonical_bson": "100000000164002a1bf5f41022b14300",
+ "canonical_extjson": "{\"d\" : {\"$numberDouble\": \"1.2345678921232E+18\"}}",
+ "relaxed_extjson": "{\"d\" : 1.2345678921232E+18}"
},
{
- "description": "-1.23456789012345677E+18",
- "canonical_bson": "1000000001640081E97DF41022B1C300",
- "canonical_extjson": "{\"d\" : {\"$numberDouble\": \"-1.23456789012345677E+18\"}}",
- "relaxed_extjson": "{\"d\" : -1.23456789012345677E+18}"
+ "description": "-1.2345678921232E+18",
+ "canonical_bson": "100000000164002a1bf5f41022b1c300",
+ "canonical_extjson": "{\"d\" : {\"$numberDouble\": \"-1.2345678921232E+18\"}}",
+ "relaxed_extjson": "{\"d\" : -1.2345678921232E+18}"
},
{
"description": "0.0",
diff --git a/source/bson-corpus/tests/regex.json b/source/bson-corpus/tests/regex.json
index c62b019cdf..223802169d 100644
--- a/source/bson-corpus/tests/regex.json
+++ b/source/bson-corpus/tests/regex.json
@@ -54,11 +54,11 @@
],
"decodeErrors": [
{
- "description": "embedded null in pattern",
+ "description": "Null byte in pattern string",
"bson": "0F0000000B610061006300696D0000"
},
{
- "description": "embedded null in flags",
+ "description": "Null byte in flags string",
"bson": "100000000B61006162630069006D0000"
}
]
diff --git a/source/bson-corpus/tests/symbol.json b/source/bson-corpus/tests/symbol.json
index 4e46cb9511..3dd3577ebd 100644
--- a/source/bson-corpus/tests/symbol.json
+++ b/source/bson-corpus/tests/symbol.json
@@ -50,31 +50,31 @@
"decodeErrors": [
{
"description": "bad symbol length: 0 (but no 0x00 either)",
- "bson": "0C0000000261000000000000"
+ "bson": "0C0000000E61000000000000"
},
{
"description": "bad symbol length: -1",
- "bson": "0C000000026100FFFFFFFF00"
+ "bson": "0C0000000E6100FFFFFFFF00"
},
{
"description": "bad symbol length: eats terminator",
- "bson": "10000000026100050000006200620000"
+ "bson": "100000000E6100050000006200620000"
},
{
"description": "bad symbol length: longer than rest of document",
- "bson": "120000000200FFFFFF00666F6F6261720000"
+ "bson": "120000000E00FFFFFF00666F6F6261720000"
},
{
"description": "symbol is not null-terminated",
- "bson": "1000000002610004000000616263FF00"
+ "bson": "100000000E610004000000616263FF00"
},
{
"description": "empty symbol, but extra null",
- "bson": "0E00000002610001000000000000"
+ "bson": "0E0000000E610001000000000000"
},
{
"description": "invalid UTF-8",
- "bson": "0E00000002610002000000E90000"
+ "bson": "0E0000000E610002000000E90000"
}
]
}
diff --git a/source/bson-corpus/tests/timestamp.json b/source/bson-corpus/tests/timestamp.json
index c76bc2998e..6f46564a32 100644
--- a/source/bson-corpus/tests/timestamp.json
+++ b/source/bson-corpus/tests/timestamp.json
@@ -18,6 +18,11 @@
"description": "Timestamp with high-order bit set on both seconds and increment",
"canonical_bson": "10000000116100FFFFFFFFFFFFFFFF00",
"canonical_extjson": "{\"a\" : {\"$timestamp\" : {\"t\" : 4294967295, \"i\" : 4294967295} } }"
+ },
+ {
+ "description": "Timestamp with high-order bit set on both seconds and increment (not UINT32_MAX)",
+ "canonical_bson": "1000000011610000286BEE00286BEE00",
+ "canonical_extjson": "{\"a\" : {\"$timestamp\" : {\"t\" : 4000000000, \"i\" : 4000000000} } }"
}
],
"decodeErrors": [
diff --git a/source/bson-corpus/tests/top.json b/source/bson-corpus/tests/top.json
index 68b51195ab..9c649b5e3f 100644
--- a/source/bson-corpus/tests/top.json
+++ b/source/bson-corpus/tests/top.json
@@ -3,9 +3,24 @@
"bson_type": "0x00",
"valid": [
{
- "description": "Document with keys that start with $",
+ "description": "Dollar-prefixed key in top-level document",
"canonical_bson": "0F00000010246B6579002A00000000",
"canonical_extjson": "{\"$key\": {\"$numberInt\": \"42\"}}"
+ },
+ {
+ "description": "Dollar as key in top-level document",
+ "canonical_bson": "0E00000002240002000000610000",
+ "canonical_extjson": "{\"$\": \"a\"}"
+ },
+ {
+ "description": "Dotted key in top-level document",
+ "canonical_bson": "1000000002612E620002000000630000",
+ "canonical_extjson": "{\"a.b\": \"c\"}"
+ },
+ {
+ "description": "Dot as key in top-level document",
+ "canonical_bson": "0E000000022E0002000000610000",
+ "canonical_extjson": "{\".\": \"a\"}"
}
],
"decodeErrors": [
@@ -64,28 +79,32 @@
{
"description": "Document truncated mid-key",
"bson": "1200000002666F"
+ },
+ {
+ "description": "Null byte in document key",
+ "bson": "0D000000107800000100000000"
}
],
"parseErrors": [
{
"description" : "Bad $regularExpression (extra field)",
- "string" : "{\"a\" : \"$regularExpression\": {\"pattern\": \"abc\", \"options\": \"\", \"unrelated\": true}}}"
+ "string" : "{\"a\" : {\"$regularExpression\": {\"pattern\": \"abc\", \"options\": \"\", \"unrelated\": true}}}"
},
{
"description" : "Bad $regularExpression (missing options field)",
- "string" : "{\"a\" : \"$regularExpression\": {\"pattern\": \"abc\"}}}"
+ "string" : "{\"a\" : {\"$regularExpression\": {\"pattern\": \"abc\"}}}"
},
{
"description": "Bad $regularExpression (pattern is number, not string)",
- "string": "{\"x\" : {\"$regularExpression\" : { \"pattern\": 42, \"$options\" : \"\"}}}"
+ "string": "{\"x\" : {\"$regularExpression\" : { \"pattern\": 42, \"options\" : \"\"}}}"
},
{
"description": "Bad $regularExpression (options are number, not string)",
- "string": "{\"x\" : {\"$regularExpression\" : { \"pattern\": \"a\", \"$options\" : 0}}}"
+ "string": "{\"x\" : {\"$regularExpression\" : { \"pattern\": \"a\", \"options\" : 0}}}"
},
{
"description" : "Bad $regularExpression (missing pattern field)",
- "string" : "{\"a\" : \"$regularExpression\": {\"options\":\"ix\"}}}"
+ "string" : "{\"a\" : {\"$regularExpression\": {\"options\":\"ix\"}}}"
},
{
"description": "Bad $oid (number, not string)",
@@ -151,6 +170,10 @@
"description": "Bad $code (type is number, not string)",
"string": "{\"a\" : {\"$code\" : 42}}"
},
+ {
+ "description": "Bad $code (type is number, not string) when $scope is also present",
+ "string": "{\"a\" : {\"$code\" : 42, \"$scope\" : {}}}"
+ },
{
"description": "Bad $code (extra field)",
"string": "{\"a\" : {\"$code\" : \"\", \"unrelated\": true}}"
@@ -195,14 +218,6 @@
"description": "Bad $date (extra field)",
"string": "{\"a\" : {\"$date\" : {\"$numberLong\" : \"1356351330501\"}, \"unrelated\": true}}"
},
- {
- "description": "Bad DBRef (ref is number, not string)",
- "string": "{\"x\" : {\"$ref\" : 42, \"$id\" : \"abc\"}}"
- },
- {
- "description": "Bad DBRef (db is number, not string)",
- "string": "{\"x\" : {\"$ref\" : \"a\", \"$id\" : \"abc\", \"$db\" : 42}}"
- },
{
"description": "Bad $minKey (boolean, not integer)",
"string": "{\"a\" : {\"$minKey\" : true}}"
@@ -230,7 +245,22 @@
{
"description": "Bad DBpointer (extra field)",
"string": "{\"a\": {\"$dbPointer\": {\"a\": {\"$numberInt\": \"1\"}, \"$id\": {\"$oid\": \"56e1fc72e0c917e9c4714161\"}, \"c\": {\"$numberInt\": \"2\"}, \"$ref\": \"b\"}}}"
+ },
+ {
+ "description" : "Null byte in document key",
+ "string" : "{\"a\\u0000\": 1 }"
+ },
+ {
+ "description" : "Null byte in sub-document key",
+ "string" : "{\"a\" : {\"b\\u0000\": 1 }}"
+ },
+ {
+ "description": "Null byte in $regularExpression pattern",
+ "string": "{\"a\" : {\"$regularExpression\" : { \"pattern\": \"b\\u0000\", \"options\" : \"i\"}}}"
+ },
+ {
+ "description": "Null byte in $regularExpression options",
+ "string": "{\"a\" : {\"$regularExpression\" : { \"pattern\": \"b\", \"options\" : \"i\\u0000\"}}}"
}
-
]
}
diff --git a/source/bson-decimal128/decimal128.md b/source/bson-decimal128/decimal128.md
new file mode 100644
index 0000000000..88bf630635
--- /dev/null
+++ b/source/bson-decimal128/decimal128.md
@@ -0,0 +1,354 @@
+# BSON Decimal128
+
+- Status: Accepted
+- Minimum Server Version: 3.4
+
+______________________________________________________________________
+
+## Abstract
+
+MongoDB 3.4 introduces a new BSON type representing high precision decimal (`"\x13"`), known as Decimal128. 3.4
+compatible drivers must support this type by creating a Value Object for it, possibly with accessor functions for
+retrieving its value in data types supported by the respective languages.
+
+Round-tripping Decimal128 types between driver and server MUST not change its value or representation in any way.
+Conversion to and from native language types is complicated and there are many pitfalls to represent Decimal128
+precisely in all languages
+
+While many languages offer a native decimal type, the precision of these types often does not exactly match that of the
+MongoDB implementation. To ensure error-free conversion and consistency between official MongoDB drivers, this
+specification does not allow automatically converting the `BSON Decimal128` type into a language-defined decimal type.
+
+Language drivers will wrap their native type in value objects by default and SHOULD offer accessor functions for
+retrieving its value represented by language-defined types if appropriate. A driver that offers the ability to configure
+mappings to/from BSON types to native types MAY allow the option to automatically convert the `BSON Decimal128` type to
+a native type. It should however be made abundantly clear to the user that converting to native data types risks
+incurring data loss.
+
+## META
+
+The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
+"OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt).
+
+## Terminology
+
+**IEEE 754-2008 128-bit decimal floating point (Decimal128)**
+
+The Decimal128 specification supports 34 decimal digits of precision, a max value of approximately `10^6145`, and min
+value of approximately `-10^6145`. This is the new `BSON Decimal128` type (`"\x13"`).
+
+**Clamping**
+
+Clamping happens when a value's exponent is too large for the destination format. This works by adding zeros to the
+coefficient to reduce the exponent to the largest usable value. An overflow occurs if the number of digits required is
+more than allowed in the destination format.
+
+**Binary Integer Decimal (BID)**
+
+MongoDB uses this binary encoding for the coefficient as specified in `IEEE 754-2008` section 3.5.2 using method 2
+"binary encoding" rather than method 1 "decimal encoding". The byte order is little-endian, like the rest of the BSON
+types.
+
+**Value Object**
+
+An immutable container type representing a value (e.g. Decimal128). This Value Object MAY provide accessors that
+retrieve the abstracted value as a different type (e.g. casting it). `double x = valueObject.getAsDouble();`
+
+## Specification
+
+### BSON Decimal128 implementation details
+
+The `BSON Decimal128` data type implements the
+[Decimal Arithmetic Encodings](https://speleotrove.com/decimal/decbits.html) specification, with certain exceptions
+around value integrity and the coefficient encoding. When a value cannot be represented exactly, the value will be
+rejected.
+
+The coefficient MUST be stored as an unsigned binary integer (BID) rather than the densely-packed decimal (DPD) shown in
+the specification. See either the `IEEE Std 754-2008` spec or the driver examples for further detail.
+
+The specification defines several statuses which are meant to signal exceptional
+[circumstances](https://speleotrove.com/decimal/daexcep.html), such as when overflowing occurs, and how to handle them.
+
+`BSON Decimal128` Value Objects MUST implement these actions for these exceptions:
+
+- Overflow
+
+ - When overflow occurs, the operation MUST emit an error and result in a failure
+
+- Underflow
+
+ - When underflow occurs, the operation MUST emit an error and result in a failure
+
+- Clamping
+
+ - Since clamping does not change the actual value, only the representation of it, clamping MUST occur without emitting
+ an error.
+
+- Rounding
+
+ - When the coefficient requires more digits then Decimal128 provides, rounding MUST be done without emitting an error,
+ unless it would result in inexact rounding, in which case the operation MUST emit an error and result in a
+ failure.
+
+- Conversion Syntax
+
+ - Invalid strings MUST emit an error and result in a failure.
+
+It should be noted that the given exponent is a preferred representation. If the value cannot be stored due to the value
+of the exponent being too large or too small, but can be stored using an alternative representation by clamping and or
+rounding, a `BSON Decimal128` compatible Value Object MUST do so, unless such operation results in an inexact rounding
+or other underflow or overflow.
+
+### Reading from BSON
+
+A BSON type `"\x13"` MUST be represented by an immutable Value Object by default and MUST NOT be automatically converted
+into language native numeric type by default. A driver that offers users a way to configure the exact type mapping to
+and from BSON types MAY allow the `BSON Decimal128` type to be converted to the user configured type.
+
+A driver SHOULD provide accessors for this immutable Value Object, which can return a language-specific representation
+of the Decimal128 value, after converting it into the respective type. For example, Java may choose to provide
+`Decimal128.getBigDecimal()`.
+
+All drivers MUST provide an accessor for retrieving the value as a string. Drivers MAY provide other accessors,
+retrieving the value as other types.
+
+### Serializing and writing BSON
+
+Drivers MUST provide a way of constructing the Value Object, as the driver representation of the `BSON Decimal128` is an
+immutable Value Object by default.
+
+A driver MUST have a way to construct this Value Object from a string. For example, Java MUST provide a method similar
+to `Decimal128.valueOf("2.000")`.
+
+A driver that has accessors for different types SHOULD provide a way to construct the Value Object from those types.
+
+### Reading from Extended JSON
+
+The Extended JSON representation of Decimal128 is a document with the key `$numberDecimal` and a value of the Decimal128
+as a string. Drivers that support Extended JSON formatting MUST support the `$numberDecimal` type specifier.
+
+When an Extended JSON `$numberDecimal` is parsed, its type should be the same as that of a deserialized
+`BSON Decimal128`, as described in [Reading from BSON](#reading-from-bson).
+
+The Extended JSON `$numberDecimal` value follows the same stringification rules as defined in
+[From String Representation](#from-string-representation).
+
+### Writing to Extended JSON
+
+The Extended JSON type identifier is `$numberDecimal`, while the value itself is a string. Drivers that support
+converting values to Extended JSON MUST be able to convert its Decimal128 value object to Extended JSON.
+
+Converting a Decimal128 Value Object to Extended JSON MUST follow the conversion rules in
+[To String Representation](#to-string-representation), and other stringification rules as when converting Decimal128
+Value Object to a String.
+
+### Operator overloading and math on Decimal128 Value Objects
+
+Drivers MUST NOT allow any mathematical operator overloading for the Decimal128 Value Objects. This includes adding two
+Decimal128 Value Objects and assigning the result to a new object.
+
+If a user wants to perform mathematical operations on Decimal128 Value Objects, the user must explicitly retrieve the
+native language value representations of the objects and perform the operations on those native representations. The
+user will then create a new Decimal128 Value Object and optionally overwrite the original Decimal128 Value Object.
+
+### From String Representation
+
+For finite numbers, we will use the definition at . It has been modified
+to account for a different NaN representation and whitespace rules and copied here:
+
+```text
+Strings which are acceptable for conversion to the abstract representation of
+numbers, or which might result from conversion from the abstract representation
+to a string, are called numeric strings.
+
+A numeric string is a character string that describes either a finite
+number or a special value.
+* If it describes a finite number, it includes one or more decimal digits,
+ with an optional decimal point. The decimal point may be embedded in the
+ digits, or may be prefixed or suffixed to them. The group of digits (and
+ optional point) thus constructed may have an optional sign ('+' or '-')
+ which must come before any digits or decimal point.
+* The string thus described may optionally be followed by an 'E'
+ (indicating an exponential part), an optional sign, and an integer
+ following the sign that represents a power of ten that is to be applied.
+ The 'E' may be in uppercase or lowercase.
+* If it describes a special value, it is one of the case-independent names
+ 'Infinity', 'Inf', or 'NaN' (where the first two represent infinity and
+ the second represent NaN). The name may be preceded by an optional sign,
+ as for finite numbers.
+* No blanks or other whitespace characters are permitted in a numeric string.
+
+Formally
+
+ sign ::= '+' | '-'
+ digit ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' |
+ '8' | '9'
+ indicator ::= 'e' | 'E'
+ digits ::= digit [digit]...
+ decimal-part ::= digits '.' [digits] | ['.'] digits
+ exponent-part ::= indicator [sign] digits
+ infinity ::= 'Infinity' | 'Inf'
+ nan ::= 'NaN'
+ numeric-value ::= decimal-part [exponent-part] | infinity
+ numeric-string ::= [sign] numeric-value | [sign] nan
+
+where the characters in the strings accepted for 'infinity' and 'nan' may be in
+any case. If an implementation supports the concept of diagnostic information
+on NaNs, the numeric strings for NaNs MAY include one or more digits, as shown
+above.[3] These digits encode the diagnostic information in an
+implementation-defined manner; however, conversions to and from string for
+diagnostic NaNs should be reversible if possible. If an implementation does not
+support diagnostic information on NaNs, these digits should be ignored where
+necessary. A plain 'NaN' is usually the same as 'NaN0'.
+
+Drivers MAY choose to support signed NaN (sNaN), along with sNaN with
+diagnostic information.
+
+Examples::
+Some numeric strings are:
+ "0" -- zero
+ "12" -- a whole number
+ "-76" -- a signed whole number
+ "12.70" -- some decimal places
+ "+0.003" -- a plus sign is allowed, too
+ "017." -- the same as 17
+ ".5" -- the same as 0.5
+ "4E+9" -- exponential notation
+ "0.73e-7" -- exponential notation, negative power
+ "Inf" -- the same as Infinity
+ "-infinity" -- the same as -Infinity
+ "NaN" -- not-a-Number
+
+Notes:
+1. A single period alone or with a sign is not a valid numeric string.
+2. A sign alone is not a valid numeric string.
+3. Significant (after the decimal point) and insignificant leading zeros
+ are permitted.
+```
+
+### To String Representation
+
+For finite numbers, we will use the definition at . It has been copied
+here:
+
+```text
+The coefficient is first converted to a string in base ten using the characters
+0 through 9 with no leading zeros (except if its value is zero, in which case a
+single 0 character is used).
+
+Next, the adjusted exponent is calculated; this is the exponent, plus the
+number of characters in the converted coefficient, less one. That is,
+exponent+(clength-1), where clength is the length of the coefficient in decimal
+digits.
+
+If the exponent is less than or equal to zero and the adjusted exponent is
+greater than or equal to -6, the number will be converted to a character form
+without using exponential notation. In this case, if the exponent is zero then
+no decimal point is added. Otherwise (the exponent will be negative), a decimal
+point will be inserted with the absolute value of the exponent specifying the
+number of characters to the right of the decimal point. '0' characters are
+added to the left of the converted coefficient as necessary. If no character
+precedes the decimal point after this insertion then a conventional '0'
+character is prefixed.
+
+Otherwise (that is, if the exponent is positive, or the adjusted exponent is
+less than -6), the number will be converted to a character form using
+exponential notation. In this case, if the converted coefficient has more than
+one digit a decimal point is inserted after the first digit. An exponent in
+character form is then suffixed to the converted coefficient (perhaps with
+inserted decimal point); this comprises the letter 'E' followed immediately by
+the adjusted exponent converted to a character form. The latter is in base ten,
+using the characters 0 through 9 with no leading zeros, always prefixed by a
+sign character ('-' if the calculated exponent is negative, '+' otherwise).
+```
+
+This corresponds to the following code snippet:
+
+```c
+var adjusted_exponent = _exponent + (clength - 1);
+if (_exponent > 0 || adjusted_exponent < -6) {
+ // exponential notation
+} else {
+ // character form without using exponential notation
+}
+```
+
+For special numbers such as infinity or the not a number (NaN) variants, the below table is used:
+
+| Value | String |
+| --------------------- | --------- |
+| Positive Infinite | Infinity |
+| Negative Infinite | -Infinity |
+| Positive NaN | NaN |
+| Negative NaN | NaN |
+| Signaled NaN | NaN |
+| Negative Signaled NaN | NaN |
+| NaN with a payload | NaN |
+
+Finally, there are certain other invalid representations that must be treated as zeros, as per `IEEE 754-2008`. The
+tests will verify that each special value has been accounted for.
+
+The server log files as well as the Extended JSON Format for Decimal128 use this format.
+
+## Motivation for Change
+
+BSON already contains support for `double` (`"\x01"`), but this type is insufficient for certain values that require
+strict precision and representation, such as money, where it is necessary to perform exact decimal rounding.
+
+The new BSON type is the 128-bit `IEEE 754-2008` decimal floating point number, which is specifically designed to cope
+with these issues.
+
+## Design Rationale
+
+For simplicity and consistency between drivers, drivers must not automatically convert this type into a native type by
+default. This also ensures original data preservation, which is crucial to Decimal128. It is however recommended that
+drivers offer a way to convert the Value Object to a native type through accessors, and to create a new BSON type from
+native types. This forces the user to explicitly do the conversion and thus understand the difference between the
+MongoDB type and possible language precision and representation. Representations via conversions done outside MongoDB
+are not guaranteed to be identical.
+
+## Backwards Compatibility
+
+There should be no backwards compatibility concerns. This specification merely deals with how to encode and decode
+BSON/Extended JSON Decimal128.
+
+## Reference Implementations
+
+- [Libbson](https://github.com/mongodb/libbson/blob/master/src/bson/bson-decimal128.c)
+- [Ruby](https://github.com/estolfo/bson-ruby/blob/RUBY-1098-decimal128/lib/bson/decimal128.rb)
+- [.NET](https://github.com/craiggwilson/mongo-csharp-driver/tree/decimal)
+- [PyMongo](https://github.com/mongodb/mongo-python-driver/blob/master/bson/decimal128.py)
+- [Node](https://github.com/mongodb/js-bson/blob/0.5/lib/bson/decimal128.js)
+- [Java](https://github.com/mongodb/mongo-java-driver/blob/master/bson/src/main/org/bson/BsonDecimal128.java)
+
+## Tests
+
+See the [BSON Corpus](../bson-corpus/bson-corpus.md) for tests.
+
+Most of the tests are converted from the
+[General Decimal Arithmetic Testcases](https://speleotrove.com/decimal/dectest.html).
+
+## Q&A
+
+- Is it true Decimal128 doesn't normalize the value?
+
+ - Yes. As a result of non-normalization rules of the Decimal128 data type, precision is represented exactly. For
+ example, '2.00' always remains stored as 200E-2 in Decimal128, and it differs from the representation of '2.0'
+ (20E-1). These two values compare equally, but represent different ideas.
+
+- How does Decimal128 "2.000" look in the shell?
+
+ - NumberDecimal("2.000")
+
+- Should a driver avoid sending Decimal128 values to pre-3.4 servers?
+
+ - No
+
+- Is there a wire version bump or something for Decimal128?
+
+ - No
+
+## Changelog
+
+- 2024-02-08: Migrated from reStructuredText to Markdown.
+- 2022-10-05: Remove spec front matter.
diff --git a/source/bson-decimal128/decimal128.rst b/source/bson-decimal128/decimal128.rst
index c15e33d234..1ad1cd7a8b 100644
--- a/source/bson-decimal128/decimal128.rst
+++ b/source/bson-decimal128/decimal128.rst
@@ -1,447 +1,3 @@
-========================================
-BSON Decimal128 Type Handling in Drivers
-========================================
-
-:Spec Title: BSON Decimal128 Type Handling In Drivers
-:Spec Version: 1.0
-:Author: Hannes Magnusson
-:Advisory Group: Emily Stolfo and Craig Wilson
-:Kernel Advisory: Geert Bosch
-:Original Work: David Hatch and Raymond Jacobson
-:Status: Approved
-:Type: Standards
-:Minimum Server Version: 3.4
-:Last Modified: 2016-06-06
-
-
-.. contents::
-
---------
-
-
-Abstract
-========
-
-MongoDB 3.4 introduces a new BSON type representing high precision decimal
-(``"\x13"``), known as Decimal128. 3.4 compatible drivers must support this
-type by creating a Value Object for it, possibly with accessor functions for
-retrieving its value in data types supported by the respective languages.
-
-
-Round-tripping Decimal128 types between driver and server MUST not change its
-value or representation in any way. Conversion to and from native language
-types is complicated and there are many pitfalls to represent Decimal128
-precisely in all languages
-
-
-While many languages offer a native decimal type, the precision of these types
-often does not exactly match that of the MongoDB implementation. To ensure
-error-free conversion and consistency between official MongoDB drivers, this
-specification does not allow automatically converting the ``BSON Decimal128`` type
-into a language-defined decimal type.
-
-
-Language drivers will wrap their native type in value objects by default and
-SHOULD offer accessor functions for retrieving its value represented by
-language-defined types if appropriate. A driver that offers the ability to
-configure mappings to/from BSON types to native types MAY allow the option to
-automatically convert the ``BSON Decimal128`` type to a native type. It should
-however be made abundantly clear to the user that converting to native data
-types risks incurring data loss.
-
-
-META
-====
-
-The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD",
-"SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be
-interpreted as described in `RFC 2119 `_.
-
-
-Terminology
-===========
-
-IEEE 754-2008 128-bit decimal floating point (Decimal128):
- The Decimal128 specification supports 34 decimal digits of precision, a max
- value of approximately ``10^6145``, and min value of approximately
- ``-10^6145``. This is the new ``BSON Decimal128`` type (``"\x13"``).
-
-
-Clamping:
- Clamping happens when a value’s exponent is too large for the destination
- format. This works by adding zeros to the coefficient to reduce the exponent to
- the largest usable value. An overflow occurs if the number of digits required
- is more than allowed in the destination format.
-
-
-Binary Integer Decimal (BID):
- MongoDB uses this binary encoding for the coefficient as specified in ``IEEE
- 754-2008``. The byte order is little-endian, like the rest of the BSON types.
-
-
-Value Object:
- An immutable container type representing a value (e.g. Decimal128). This Value
- Object MAY provide accessors that retrieve the abstracted value as a different
- type (e.g. casting it). ``double x = valueObject.getAsDouble();``
-
-
-Specification
-=============
-
-
---------------------------------------
-BSON Decimal128 implementation details
---------------------------------------
-
-The ``BSON Decimal128`` data type implements the `Decimal Arithmetic Encodings
-`_ specification, with certain
-exceptions around value integrity. When a value cannot be represented exactly,
-the value will be rejected.
-
-
-The specification defines several statuses which are meant to signal
-exceptional `circumstances `_,
-such as when overflowing occurs, and how to handle them.
-
-
-``BSON Decimal128`` Value Objects MUST implement these actions for these exceptions:
-
-* Overflow
- * When overflow occurs, the operation MUST emit an error and result in a failure
-* Underflow
- * When underflow occurs, the operation MUST emit an error and result in a failure
-* Clamping
- * Since clamping does not change the actual value, only the representation
- of it, clamping MUST occur without emitting an error.
-* Rounding
- * When the coefficient requires more digits then Decimal128 provides,
- rounding MUST be done without emitting an error, unless it would result in
- inexact rounding, in which case the operation MUST emit an error and
- result in a failure.
-* Conversion Syntax
- * Invalid strings MUST emit an error and result in a failure.
-
-
-It should be noted that the given exponent is a preferred representation. If
-the value cannot be stored due to the value of the exponent being too large or
-too small, but can be stored using an alternative representation by clamping
-and or rounding, a ``BSON Decimal128`` compatible Value Object MUST do so, unless
-such operation results in an inexact rounding or other underflow or overflow.
-
-
------------------
-Reading from BSON
------------------
-
-A BSON type ``"\x13"`` MUST be represented by an immutable Value Object by
-default and MUST NOT be automatically converted into language native numeric
-type by default. A driver that offers users a way to configure the exact type
-mapping to and from BSON types MAY allow the ``BSON Decimal128`` type to be
-converted to the user configured type.
-
-
-A driver SHOULD provide accessors for this immutable Value Object, which can
-return a language-specific representation of the Decimal128 value, after
-converting it into the respective type. For example, Java may choose to provide
-``Decimal128.getBigDecimal()``.
-
-
-All drivers MUST provide an accessor for retrieving the value as a string.
-Drivers MAY provide other accessors, retrieving the value as other types.
-
-
-----------------------------
-Serializing and writing BSON
-----------------------------
-
-Drivers MUST provide a way of constructing the Value Object, as the driver
-representation of the ``BSON Decimal128`` is an immutable Value Object by default.
-
-
-A driver MUST have a way to construct this Value Object from a string. For
-example, Java MUST provide a method similar to ``Decimal128.valueOf("2.000")``.
-
-
-A driver that has accessors for different types SHOULD provide a way to
-construct the Value Object from those types.
-
-
---------------------------
-Reading from Extended JSON
---------------------------
-
-The Extended JSON representation of Decimal128 is a document with the key
-``$numberDecimal`` and a value of the Decimal128 as a string. Drivers that support
-Extended JSON formatting MUST support the ``$numberDecimal`` type specifier.
-
-
-When an Extended JSON ``$numberDecimal`` is parsed, its type should be the same as
-that of a deserialized ``BSON Decimal128``, as described in `Reading from BSON`_.
-
-
-The Extended JSON ``$numberDecimal`` value follows the same stringification rules
-as defined in `From String Representation`_.
-
-
-------------------------
-Writing to Extended JSON
-------------------------
-
-The Extended JSON type identifier is ``$numberDecimal``, while the value itself is
-a string. Drivers that support converting values to Extended JSON MUST be able
-to convert its Decimal128 value object to Extended JSON.
-
-
-Converting a Decimal128 Value Object to Extended JSON MUST follow the
-conversion rules in `To String Representation`_, and other stringification rules
-as when converting Decimal128 Value Object to a String.
-
-
----------------------------------------------------------
-Operator overloading and math on Decimal128 Value Objects
----------------------------------------------------------
-
-Drivers MUST NOT allow any mathematical operator overloading for the Decimal128
-Value Objects. This includes adding two Decimal128 Value Objects and assigning
-the result to a new object.
-
-
-If a user wants to perform mathematical operations on Decimal128 Value Objects,
-the user must explicitly retrieve the native language value representations of
-the objects and perform the operations on those native representations. The
-user will then create a new Decimal128 Value Object and optionally overwrite
-the original Decimal128 Value Object.
-
-
---------------------------
-From String Representation
---------------------------
-
-For finite numbers, we will use the definition at
-http://speleotrove.com/decimal/daconvs.html. It has been modified to account
-for a different NaN representation and whitespace rules and copied here::
-
-
- Strings which are acceptable for conversion to the abstract representation of
- numbers, or which might result from conversion from the abstract representation
- to a string, are called numeric strings.
-
-
- A numeric string is a character string that describes either a finite
- number or a special value.
- * If it describes a finite number, it includes one or more decimal digits,
- with an optional decimal point. The decimal point may be embedded in the
- digits, or may be prefixed or suffixed to them. The group of digits (and
- optional point) thus constructed may have an optional sign (‘+’ or ‘-’)
- which must come before any digits or decimal point.
- * The string thus described may optionally be followed by an ‘E’
- (indicating an exponential part), an optional sign, and an integer
- following the sign that represents a power of ten that is to be applied.
- The ‘E’ may be in uppercase or lowercase.
- * If it describes a special value, it is one of the case-independent names
- ‘Infinity’, ‘Inf’, or ‘NaN’ (where the first two represent infinity and
- the second represent NaN). The name may be preceded by an optional sign,
- as for finite numbers.
- * No blanks or other whitespace characters are permitted in a numeric string.
-
- Formally
-
- sign ::= ’+’ | ’-’
- digit ::= ’0’ | ’1’ | ’2’ | ’3’ | ’4’ | ’5’ | ’6’ | ’7’ |
- ’8’ | ’9’
- indicator ::= ’e’ | ’E’
- digits ::= digit [digit]...
- decimal-part ::= digits ’.’ [digits] | [’.’] digits
- exponent-part ::= indicator [sign] digits
- infinity ::= ’Infinity’ | ’Inf’
- nan ::= ’NaN’
- numeric-value ::= decimal-part [exponent-part] | infinity
- numeric-string ::= [sign] numeric-value | [sign] nan
-
- where the characters in the strings accepted for ‘infinity’ and ‘nan’ may be in
- any case. If an implementation supports the concept of diagnostic information
- on NaNs, the numeric strings for NaNs MAY include one or more digits, as shown
- above.[3] These digits encode the diagnostic information in an
- implementation-defined manner; however, conversions to and from string for
- diagnostic NaNs should be reversible if possible. If an implementation does not
- support diagnostic information on NaNs, these digits should be ignored where
- necessary. A plain ‘NaN’ is usually the same as ‘NaN0’.
-
-
- Drivers MAY choose to support signed NaN (sNaN), along with sNaN with
- diagnostic information.
-
-
-
- Examples::
- Some numeric strings are:
- "0" -- zero
- "12" -- a whole number
- "-76" -- a signed whole number
- "12.70" -- some decimal places
- "+0.003" -- a plus sign is allowed, too
- "017." -- the same as 17
- ".5" -- the same as 0.5
- "4E+9" -- exponential notation
- "0.73e-7" -- exponential notation, negative power
- "Inf" -- the same as Infinity
- "-infinity" -- the same as -Infinity
- "NaN" -- not-a-Number
-
- Notes:
- 1. A single period alone or with a sign is not a valid numeric string.
- 2. A sign alone is not a valid numeric string.
- 3. Significant (after the decimal point) and insignificant leading zeros
- are permitted.
-
-
-------------------------
-To String Representation
-------------------------
-
-For finite numbers, we will use the definition at
-http://speleotrove.com/decimal/daconvs.html. It has been copied here::
-
-
- The coefficient is first converted to a string in base ten using the characters
- 0 through 9 with no leading zeros (except if its value is zero, in which case a
- single 0 character is used).
-
-
- Next, the adjusted exponent is calculated; this is the exponent, plus the
- number of characters in the converted coefficient, less one. That is,
- exponent+(clength-1), where clength is the length of the coefficient in decimal
- digits.
-
-
- If the exponent is less than or equal to zero and the adjusted exponent is
- greater than or equal to -6, the number will be converted to a character form
- without using exponential notation. In this case, if the exponent is zero then
- no decimal point is added. Otherwise (the exponent will be negative), a decimal
- point will be inserted with the absolute value of the exponent specifying the
- number of characters to the right of the decimal point. ‘0’ characters are
- added to the left of the converted coefficient as necessary. If no character
- precedes the decimal point after this insertion then a conventional ‘0’
- character is prefixed.
-
-
- Otherwise (that is, if the exponent is positive, or the adjusted exponent is
- less than -6), the number will be converted to a character form using
- exponential notation. In this case, if the converted coefficient has more than
- one digit a decimal point is inserted after the first digit. An exponent in
- character form is then suffixed to the converted coefficient (perhaps with
- inserted decimal point); this comprises the letter ‘E’ followed immediately by
- the adjusted exponent converted to a character form. The latter is in base ten,
- using the characters 0 through 9 with no leading zeros, always prefixed by a
- sign character (‘-’ if the calculated exponent is negative, ‘+’ otherwise).
-
-
-This corresponds to the following code snippet:
-
-
- .. code:: c
-
- var adjusted_exponent = _exponent + (clength - 1);
- if (_exponent > 0 || adjusted_exponent < -6) {
- // exponential notation
- } else {
- // character form without using exponential notation
- }
-
-
-For special numbers such as infinity or the not a number (NaN) variants, the
-below table is used:
-
-
-============================== ============
- Value String
-============================== ============
-Positive Infinite Infinity
-Negative Infinite -Infinity
-Positive NaN NaN
-Negative NaN NaN
-Signaled NaN NaN
-Negative Signaled NaN NaN
-NaN with a payload NaN
-Signaled NaN with a payload NaN
-============================== ============
-
-
-
-Finally, there are certain other invalid representations that must be treated
-as zeros, as per ``IEEE 754-2008``. The tests will verify that each special value
-has been accounted for.
-
-
-The server log files as well as the Extended JSON Format for Decimal128 use
-this format.
-
-
-Motivation for Change
-=====================
-
-BSON already contains support for ``double`` (``"\x01"``), but this type is
-insufficient for certain values that require strict precision and
-representation, such as money, where it is necessary to perform exact decimal
-rounding.
-
-
-The new BSON type is the 128-bit ``IEEE 754-2008`` decimal floating point number,
-which is specifically designed to cope with these issues.
-
-
-Design Rationale
-================
-
-For simplicity and consistency between drivers, drivers must not automatically
-convert this type into a native type by default. This also ensures original
-data preservation, which is crucial to Decimal128. It is however recommended
-that drivers offer a way to convert the Value Object to a native type through
-accessors, and to create a new BSON type from native types. This forces the
-user to explicitly do the conversion and thus understand the difference between
-the MongoDB type and possible language precision and representation.
-Representations via conversions done outside MongoDB are not guaranteed to be
-identical.
-
-
-Backwards Compatibility
-=======================
-
-There should be no backwards compatibility concerns. This specification merely
-deals with how to encode and decode BSON/Extended JSON Decimal128.
-
-
-Reference Implementations
-=========================
-
-* `Libbson `_
-* `Ruby `_
-* `.NET `_
-* `PyMongo `_
-* `Node `_
-* `Java `_
-
-
-Tests
-=====
-
-See the `BSON Corpus `_ for tests.
-
-Most of the tests are converted from the
-[General Decimal Arithmetic Testcases](http://speleotrove.com/decimal/dectest.html>).
-
-Q&A
-===
-
-* Is it true Decimal128 doesn’t normalize the value?
- * Yes. As a result of non-normalization rules of the Decimal128 data type,
- precision is represented exactly. For example, ‘2.00’ always remains
- stored as 200E-2 in Decimal128, and it differs from the representation of
- ‘2.0’ (20E-1). These two values compare equally, but represent different
- ideas.
-* How does Decimal128 "2.000" look in the shell?
- * NumberDecimal("2.000")
-* Should a driver avoid sending Decimal128 values to pre-3.4 servers?
- * No
-* Is there a wire version bump or something for Decimal128?
- * No
+.. note::
+ This specification has been converted to Markdown and renamed to
+ `decimal128.md `_.
diff --git a/source/bson-objectid/objectid.md b/source/bson-objectid/objectid.md
new file mode 100644
index 0000000000..5d39b1733f
--- /dev/null
+++ b/source/bson-objectid/objectid.md
@@ -0,0 +1,136 @@
+# BSON ObjectID
+
+- Status: Accepted
+- Minimum Server Version: N/A
+
+______________________________________________________________________
+
+## Abstract
+
+This specification documents the format and data contents of ObjectID BSON values that the drivers and the server
+generate when no field values have been specified (e.g. creating an ObjectID BSON value when no `_id` field is present
+in a document). It is primarily aimed to provide an alternative to the historical use of the MD5 hashing algorithm for
+the machine information field of the ObjectID, which is problematic when providing a FIPS compliant implementation. It
+also documents existing best practices for the timestamp and counter fields.
+
+## META
+
+The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
+"OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt).
+
+## Specification
+
+The [ObjectID](https://www.mongodb.com/docs/manual/reference/method/ObjectId/) BSON type is a 12-byte value consisting
+of three different portions (fields):
+
+- a 4-byte value representing the seconds since the Unix epoch in the highest order bytes,
+- a 5-byte random number unique to a machine and process,
+- a 3-byte counter, starting with a random value.
+
+```text
+4 byte timestamp 5 byte process unique 3 byte counter
+|<----------------->|<---------------------->|<------------>|
+[----|----|----|----|----|----|----|----|----|----|----|----]
+0 4 8 12
+```
+
+### Timestamp Field
+
+This 4-byte big endian field represents the seconds since the Unix epoch (Jan 1st, 1970, midnight UTC). It is an ever
+increasing value that will have a range until about Jan 7th, 2106.
+
+Drivers MUST create ObjectIDs with this value representing the number of seconds since the Unix epoch.
+
+Drivers MUST interpret this value as an **unsigned 32-bit integer** when conversions to language specific date/time
+values are created, and when converting this to a timestamp.
+
+Drivers SHOULD have an accessor method on an ObjectID class for obtaining the timestamp value.
+
+### Random Value
+
+A 5-byte field consisting of a random value generated once per process. This random value is unique to the machine and
+process.
+
+Drivers MUST NOT have an accessor method on an ObjectID class for obtaining this value.
+
+The random number does not have to be cryptographic. If possible, use a PRNG with OS supplied entropy that SHOULD NOT
+block to wait for more entropy to become available. Otherwise, seed a deterministic PRNG to ensure uniqueness of process
+and machine by combining time, process ID, and hostname.
+
+### Counter
+
+A 3-byte big endian counter.
+
+This counter MUST be initialised to a random value when the driver is first activated. After initialisation, the counter
+MUST be increased by 1 for every ObjectID creation.
+
+When the counter overflows (i.e., hits 16777215+1), the counter MUST be reset to 0.
+
+Drivers MUST NOT have an accessor method on an ObjectID class for obtaining this value.
+
+The random number does not have to be cryptographic. If possible, use a PRNG with OS supplied entropy that SHOULD NOT
+block to wait for more entropy to become available. Otherwise, seed a deterministic PRNG to ensure uniqueness of process
+and machine by combining time, process ID, and hostname.
+
+## Test Plan
+
+Drivers MUST:
+
+- Ensure that the Timestamp field is represented as an unsigned 32-bit representing the number of seconds since the
+ Epoch for the Timestamp values:
+ - `0x00000000`: To match `"Jan 1st, 1970 00:00:00 UTC"`
+ - `0x7FFFFFFF`: To match `"Jan 19th, 2038 03:14:07 UTC"`
+ - `0x80000000`: To match `"Jan 19th, 2038 03:14:08 UTC"`
+ - `0xFFFFFFFF`: To match `"Feb 7th, 2106 06:28:15 UTC"`
+- Ensure that the Counter field successfully overflows its sequence from `0xFFFFFF` to `0x000000`.
+- Ensure that after a new process is created through a fork() or similar process creation operation, the "random number
+ unique to a machine and process" is no longer the same as the parent process that created the new process.
+
+## Motivation for Change
+
+Besides the specific exclusion of MD5 as an allowed hashing algorithm, the information in this specification is meant to
+align the ObjectID generation algorithm of both drivers and the server.
+
+## Design Rationale
+
+**Timestamp:** The timestamp is a 32-bit **unsigned** integer, as it allows us to extend the furthest date that the
+timestamp can represent from the year 2038 to 2106. There is no reason why MongoDB would generate a timestamp to mean a
+date before 1970, as MongoDB did not exist back then.
+
+**Random Value:** Originally, this field consisted of the Machine ID and Process ID fields. There were numerous
+divergences between drivers due to implementation choices, and the Machine ID field traditionally used the MD5 hashing
+algorithm which can't be used on FIPS compliant machines. In order to allow for a similar behaviour among all drivers
+**and** the MongoDB Server, these two fields have been collated together into a single 5-byte random value, unique to a
+machine and process.
+
+**Counter:** The counter makes it possible to have multiple ObjectIDs per second, per server, and per process. As the
+counter can overflow, there is a possibility of having duplicate ObjectIDs if you create more than 16 million ObjectIDs
+per second in the same process on a single machine.
+
+**Endianness:** The *Timestamp* and *Counter* are big endian because we can then use `memcmp` to order ObjectIDs, and we
+want to ensure an increasing order.
+
+## Backwards Compatibility
+
+This specification requires that the existing *Machine ID* and *Process ID* fields are merged into a single 5-byte
+value. This will change the behaviour of ObjectID generation, as well as the behaviour of drivers that currently have
+getters and setters for the original *Machine ID* and *Process ID* fields.
+
+## Reference Implementation
+
+Currently there is no full reference implementation yet.
+
+## Changelog
+
+- 2024-07-30: Migrated from reStructuredText to Markdown.
+
+- 2022-10-05: Remove spec front matter and reformat changelog.
+
+- 2019-01-14: Clarify that the random numbers don't need to be cryptographically secure. Add a test to test that the
+ unique value is different in forked processes.
+
+- 2018-10-11: Clarify that the *Timestamp* and *Counter* fields are big endian, and add the reason why.
+
+- 2018-07-02: Replaced Machine ID and Process ID fields with a single 5-byte unique value
+
+- 2018-05-22: Initial Release
diff --git a/source/causal-consistency/causal-consistency.md b/source/causal-consistency/causal-consistency.md
new file mode 100644
index 0000000000..07f45b2715
--- /dev/null
+++ b/source/causal-consistency/causal-consistency.md
@@ -0,0 +1,434 @@
+# Causal Consistency Specification
+
+- Status: Accepted
+- Minimum Server Version: 3.6
+
+______________________________________________________________________
+
+## Abstract
+
+Version 3.6 of the server introduces support for causal consistency. This spec builds upon the Sessions Specification to
+define how an application requests causal consistency and how a driver interacts with the server to implement causal
+consistency.
+
+## Definitions
+
+### META
+
+The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
+"OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt).
+
+### Terms
+
+**Causal consistency**
+
+A property that guarantees that an application can read its own writes and that a later read will never observe a
+version of the data that is older than an earlier read.
+
+**ClientSession**
+
+The driver object representing a client session and the operations that can be performed on it.
+
+**Cluster time**
+
+The current cluster time. The server reports its view of the current cluster time in the `$clusterTime` field in
+responses from the server and the driver participates in distributing the current cluster time to all nodes (called
+"gossipping the cluster time") by sending the highest `$clusterTime` it has seen so far in messages it sends to mongos
+servers. The current cluster time is a logical time, but is digitally signed to prevent malicious clients from
+propagating invalid cluster times. Cluster time is only used in replica sets and sharded clusters.
+
+**Logical time**
+
+A time-like quantity that can be used to determine the order in which events occurred. Logical time is represented as a
+BsonTimestamp.
+
+**MongoClient**
+
+The root object of a driver's API. MAY be named differently in some drivers.
+
+**MongoCollection**
+
+The driver object representing a collection and the operations that can be performed on it. MAY be named differently in
+some drivers.
+
+**MongoDatabase**
+
+The driver object representing a database and the operations that can be performed on it. MAY be named differently in
+some drivers.
+
+**Operation time**
+
+The logical time at which an operation occurred. The server reports the operation time in the response to all commands,
+including error responses. The operation time by definition is always less than or equal to the cluster time. Operation
+times are tracked on a per `ClientSession` basis, so the `operationTime` of each `ClientSession` corresponds to the time
+of the last operation performed in that particular `ClientSession`.
+
+**ServerSession**
+
+The driver object representing a server session.
+
+**Session**
+
+A session is an abstract concept that represents a set of sequential operations executed by an application that are
+related in some way. This specification defines how sessions are used to implement causal consistency.
+
+**Unacknowledged writes**
+
+Unacknowledged writes are write operations that are sent to the server without waiting for a reply acknowledging the
+write. See the "Unacknowledged Writes" section below for information on how unacknowledged writes interact with causal
+consistency.
+
+## Specification
+
+An application requests causal consistency by creating a `ClientSession` with options that specify that causal
+consistency is desired. An application then passes the session as an argument to methods in the `MongoDatabase` and
+`MongoCollection` classes. Any operations performed against that session will then be causally consistent.
+
+## Naming variations
+
+This specification defines names for new methods and types. To the extent possible you SHOULD use these names in your
+driver. However, where your driver's and/or language's naming conventions differ you SHOULD continue to use them
+instead. For example, you might use `StartSession` or `start_session` instead of `startSession`.
+
+## High level summary of the API changes for causal consistency
+
+Causal consistency is built on top of client sessions.
+
+Applications will start a new client session for causal consistency like this:
+
+```typescript
+options = new SessionOptions(causalConsistency = true);
+session = client.startSession(options);
+```
+
+All read and write operations performed using this session will now be causally consistent.
+
+If no value is provided for `causalConsistency` and snapshot reads are not requested a value of true is implied. See the
+`causalConsistency` section.
+
+## MongoClient changes
+
+There are no API changes to `MongoClient` to support causal consistency. Applications indicate whether they want causal
+consistency by setting the `causalConsistency` field in the options passed to the `startSession` method.
+
+## SessionOptions changes
+
+`SessionOptions` change summary
+
+```typescript
+class SessionOptions {
+ Optional causalConsistency;
+
+ // other options defined by other specs
+}
+```
+
+In order to support causal consistency a new property named `causalConsistency` is added to `SessionOptions`.
+Applications set `causalConsistency` when starting a client session to indicate whether they want causal consistency.
+All read and write operations performed using that client session are then causally consistent.
+
+Each new member is documented below.
+
+### causalConsistency
+
+Applications set `causalConsistency` when starting a session to indicate whether they want causal consistency.
+
+Note that the `causalConsistency` property is optional. For explicit sessions, the default value of this property is
+`not supplied`. If no value is supplied for `causalConsistency` the value will be inherited. Currently it is inherited
+from the global default which is defined to be true. In the future it *might* be inherited from client settings. For
+implicit sessions, the value of this property MUST be set to `false` in order to avoid potential conflicts with an
+operation's read concern level.
+
+Causal consistency is provided at the session level by tracking the `clusterTime` and `operationTime` for each session.
+In some cases an application may wish subsequent operations in one session to be causally consistent with operations
+that were executed in a different session. In that case the application can call the `advanceClusterTime` and
+`advanceOperationTime` methods in `ClientSession` to advance the `clusterTime` and `operationTime` of one session to the
+`clusterTime` and `operationTime` from another session.
+
+## ClientSession changes
+
+`ClientSession` changes summary
+
+```typescript
+interface ClientSession {
+ Optional operationTime;
+
+ void advanceOperationTime(BsonTimestamp operationTime);
+
+ // other members as defined in other specs
+}
+```
+
+Each new member is documented below.
+
+### operationTime
+
+This property returns the operation time of the most recent operation performed using this session. If no operations
+have been performed using this session the value will be null unless `advanceOperationTime` has been called. This value
+will also be null when the cluster does not report operation times.
+
+### advanceOperationTime
+
+This method advances the `operationTime` for a session. If the new `operationTime` is greater than the session's current
+`operationTime` then the session's `operationTime` MUST be advanced to the new `operationTime`. If the new
+`operationTime` is less than or equal to the session's current `operationTime` then the session's `operationTime` MUST
+NOT be changed.
+
+Drivers MUST NOT attempt to validate the supplied `operationTime`. While the server requires that `operationTime` be
+less than or equal to `clusterTime` we don't want to check that when `advanceOperationTime` is called. This allows an
+application to call `advanceClusterTime` and `advanceOperationTime` in any order, or perhaps to not call
+`advanceClusterTime` at all and let the `clusterTime` that is sent to the server be implied by the `clusterTime` in
+`MongoClient`.
+
+## MongoDatabase changes
+
+There are no additional API changes to `MongoDatabase` beyond those specified in the Sessions Specification. All
+`MongoDatabase` methods that talk to the server have been overloaded to take a session parameter. If that session was
+started with `causalConsistency = true` then all operations using that session will be causally consistent.
+
+## MongoCollection changes
+
+There are no additional API changes to `MongoCollection` beyond those specified in the Sessions Specification. All
+`MongoCollection` methods that talk to the server have been overloaded to take a session parameter. If that session was
+started with `causalConsistency = true` then all operations using that session will be causally consistent.
+
+## Server Commands
+
+There are no new server commands related to causal consistency. Instead, causal consistency is implemented by:
+
+1. Saving the `operationTime` returned by 3.6+ servers for all operations in a property of the `ClientSession` object.
+ The server reports the `operationTime` whether the operation succeeded or not and drivers MUST save the
+ `operationTime` in the `ClientSession` whether the operation succeeded or not.
+2. Passing that `operationTime` in the `afterClusterTime` field of the `readConcern` field for subsequent causally
+ consistent read and write operations (for all commands that support a `readConcern`)
+3. Gossiping clusterTime (described in the Driver Session Specification)
+
+## Server Command Responses
+
+To support causal consistency the server returns the `operationTime` in responses it sends to the driver (for both read
+and write operations).
+
+```typescript
+{
+ ok : 1 or 0,
+ ... // the rest of the command reply
+ operationTime :
+ $clusterTime : // only in deployments that support cluster times
+}
+```
+
+The `operationTime` MUST be stored in the `ClientSession` to later be passed as the `afterClusterTime` field of the
+`readConcern` field in subsequent causally consistent read and write operations. The `operationTime` is returned whether
+the command succeeded or not and MUST be stored in either case.
+
+Drivers MUST examine all responses from the server for the presence of an `operationTime` field and store the value in
+the `ClientSession`.
+
+When connected to a standalone node command replies do not include an `operationTime` field. All operations against a
+standalone node are causally consistent automatically because there is only one node.
+
+When connected to a deployment that supports cluster times the command response also includes a field called
+`$clusterTime` that drivers MUST use to gossip the cluster time. See the Sessions Specification for details.
+
+## Causally consistent read and write commands
+
+For causal consistency the driver MUST send the `operationTime` saved in the `ClientSession` as the value of the
+`afterClusterTime` field of the `readConcern` field for read and write commands:
+
+```typescript
+{
+ find : , // or other read or write command
+ ... // the rest of the command parameters
+ readConcern :
+ {
+ level : ..., // from the operation's read concern (only if specified)
+ afterClusterTime :
+ }
+}
+```
+
+For the list of commands that support causally consistent reads and writes, see the
+[Read Concern](../read-write-concern/read-write-concern.md#afterclustertime) spec.
+
+The driver MUST merge the `ReadConcern` specified for the operation with the `operationTime` from the `ClientSession`
+(which goes in the `afterClusterTime` field) to generate the combined `readConcern` to send to the server. If the level
+property of the read concern for the operation is null then the driver MUST NOT include a `level` field alongside the
+`afterClusterTime` of the `readConcern` value sent to the server. Drivers MUST NOT attempt to verify whether the server
+supports causally consistent reads or not for a given read concern level. The server will return an error if a given
+level does not support causal consistency.
+
+The Read and Write Concern specification states that when a user has not specified a `ReadConcern` or has specified the
+server's default `ReadConcern`, drivers MUST omit the `ReadConcern` parameter when sending the command. For causally
+consistent reads and writes this requirement is modified to state that when the `ReadConcern` parameter would normally
+be omitted drivers MUST send a `ReadConcern` after all because that is how the `afterClusterTime` value is sent to the
+server.
+
+The Read and Write Concern Specification states that drivers MUST NOT add a `readConcern` field to commands that are run
+using a generic `runCommand` method. The same is true for causal consistency, so commands that are run using
+`runCommand` MUST NOT have an `afterClusterTime` field added to them.
+
+When executing a causally consistent operation, the `afterClusterTime` field MUST be sent when connected to a deployment
+that supports cluster times, and MUST NOT be sent when connected to a deployment that does not support cluster times.
+
+## Unacknowledged writes
+
+The implementation of causal consistency relies on the `operationTime` returned by the server in the acknowledgement of
+a write. Since unacknowledged writes don't receive a response from the server (or don't wait for a response) the
+`ClientSession`'s `operationTime` is not updated after an unacknowledged write. That means that a causally consistent
+read after an unacknowledged write cannot be causally consistent with the unacknowledged write. Rather than prohibiting
+unacknowledged writes in a causally consistent session we have decided to accept this limitation. Drivers MUST document
+that causally consistent operations are not causally consistent with unacknowledged writes.
+
+## Test Plan
+
+Below is a list of test cases to write.
+
+Note: some tests are only relevant to certain deployments. For the purpose of deciding which tests to run assume that
+any deployment that is version 3.6 or higher and is either a replica set or a sharded cluster supports cluster times.
+
+1. When a `ClientSession` is first created the `operationTime` has no value.
+ - `session = client.startSession()`
+ - assert `session.operationTime` has no value
+2. The first read in a causally consistent session must not send `afterClusterTime` to the server (because the
+ `operationTime` has not yet been determined)
+ - `session = client.startSession(causalConsistency = true)`
+ - `document = collection.anyReadOperation(session, ...)`
+ - capture the command sent to the server (using APM or other mechanism)
+ - assert that the command does not have an `afterClusterTime`
+3. The first read or write on a `ClientSession` should update the `operationTime` of the `ClientSession`, even if there
+ is an error.
+ - skip this test if connected to a deployment that does not support cluster times
+ - `session = client.startSession() // with or without causal consistency`
+ - `collection.anyReadOrWriteOperation(session, ...) // test with errors also if possible`
+ - capture the response sent from the server (using APM or other mechanism)
+ - assert `session.operationTime` has the same value that is in the response from the server
+4. A `findOne` followed by any other read operation (test them all) should include the `operationTime` returned by the
+ server for the first operation in the `afterClusterTime` parameter of the second operation
+ - skip this test if connected to a deployment that does not support cluster times
+ - `session = client.startSession(causalConsistency = true)`
+ - `collection.findOne(session, {})`
+ - `operationTime = session.operationTime`
+ - `collection.anyReadOperation(session, ...)`
+ - capture the command sent to the server (using APM or other mechanism)
+ - assert that the command has an `afterClusterTime` field with a value of `operationTime`
+5. Any write operation (test them all) followed by a `findOne` operation should include the `operationTime` of the
+ first operation in the `afterClusterTime` parameter of the second operation, including the case where the first
+ operation returned an error.
+ - skip this test if connected to a deployment that does not support cluster times
+ - `session = client.startSession(causalConsistency = true)`
+ - `collection.anyWriteOperation(session, ...) // test with errors also where possible`
+ - `operationTime = session.operationTime`
+ - `collection.findOne(session, {})`
+ - capture the command sent to the server (using APM or other mechanism)
+ - assert that the command has an `afterClusterTime` field with a value of `operationTime`
+6. A read operation in a `ClientSession` that is not causally consistent should not include the `afterClusterTime`
+ parameter in the command sent to the server.
+ - skip this test if connected to a deployment that does not support cluster times
+ - `session = client.startSession(causalConsistency = false)`
+ - `collection.anyReadOperation(session, {})`
+ - `operationTime = session.operationTime`
+ - capture the command sent to the server (using APM or other mechanism)
+ - assert that the command does not have an `afterClusterTime` field
+7. A read operation in a causally consistent session against a deployment that does not support cluster times does not
+ include the `afterClusterTime` parameter in the command sent to the server.
+ - skip this test if connected to a deployment that does support cluster times
+ - `session = client.startSession(causalConsistency = true)`
+ - `collection.anyReadOperation(session, {})`
+ - capture the command sent to the server (using APM or other mechanism)
+ - assert that the command does not have an `afterClusterTime` field
+8. When using the default server `ReadConcern` the `readConcern` parameter in the command sent to the server should not
+ include a `level` field.
+ - skip this test if connected to a deployment that does not support cluster times
+ - `session = client.startSession(causalConsistency = true)`
+ - configure `collection` to use default server `ReadConcern`
+ - `collection.findOne(session, {})`
+ - `operationTime = session.operationTime`
+ - `collection.anyReadOperation(session, ...)`
+ - capture the command sent to the server (using APM or other mechanism)
+ - assert that the command does not have a `` `level `` field
+ - assert that the command has a `afterClusterTime` field with a value of `operationTime`
+9. When using a custom `ReadConcern` the `readConcern` field in the command sent to the server should be a merger of
+ the `ReadConcern` value and the `afterClusterTime` field.
+ - skip this test if connected to a deployment that does not support cluster times
+ - `session = client.startSession(causalConsistency = true)`
+ - configure collection to use a custom ReadConcern
+ - `collection.findOne(session, {})`
+ - `operationTime = session.operationTime`
+ - `collection.anyReadOperation(session, ...)`
+ - capture the command sent to the server (using APM or other mechanism)
+ - assert that the command has a `level` field with a value matching the custom readConcern
+ - assert that the command has an `afterClusterTime` field with a value of `operationTime`
+10. **Removed**
+11. When connected to a deployment that does not support cluster times messages sent to the server should not include
+ `$clusterTime`.
+ - skip this test when connected to a deployment that does support cluster times
+ - `document = collection.findOne({})`
+ - capture the command sent to the server
+ - assert that the command does not include a `$clusterTime` field
+12. When connected to a deployment that does support cluster times messages sent to the server should include
+ `$clusterTime`.
+ - skip this test when connected to a deployment that does not support cluster times
+ - `document = collection.findOne({})`
+ - capture the command sent to the server
+ - assert that the command includes a `$clusterTime` field
+
+## Motivation
+
+To support causal consistency. Only supported with server version 3.6 or newer.
+
+## Design Rationale
+
+The goal is to modify the driver API as little as possible so that existing programs that don't need causal consistency
+don't have to be changed. This goal is met by defining a `SessionOptions` field that applications use to start a
+`ClientSession` that can be used for causal consistency. Any operations performed with such a session are then causally
+consistent.
+
+The `operationTime` is tracked on a per `ClientSession` basis. This allows each `ClientSession` to have an
+`operationTime` that is sufficiently new to guarantee causal consistency for that session, but no newer. Using an
+`operationTime` that is newer than necessary can cause reads to block longer than necessary when sent to a lagging
+secondary. The goal is to block for just long enough to guarantee causal consistency and no longer.
+
+## Backwards Compatibility
+
+The API changes to support sessions extend the existing API but do not introduce any backward breaking changes. Existing
+programs that don't use causal consistency continue to compile and run correctly.
+
+## Reference Implementation
+
+A reference implementation must be completed before any spec is given status "Final", but it need not be completed
+before the spec is "Accepted". While there is merit to the approach of reaching consensus on the specification and
+rationale before writing code, the principle of "rough consensus and running code" is still useful when it comes to
+resolving many discussions of spec details. A final reference implementation must include test code and documentation.
+
+## Q&A
+
+## Changelog
+
+- 2026-05-04: Require `afterClusterTime` on all write commands in causally-consistent sessions, not only on read
+ commands.
+
+- 2024-02-08: Migrated from reStructuredText to Markdown.
+
+- 2022-11-11: Require `causalConsistency=false` for implicit sessions.
+
+- 2022-10-05: Remove spec front matter and reformat changelog.
+
+- 2022-01-28: Fix formatting for prose tests
+
+- 2022-01-22: Remove outdated prose test #10
+
+- 2021-06-26: Default value for causalConsistency is influenced by snapshot reads
+
+- 2017-11-17: Added link to ReadConcern spec which lists commands that support readConcern
+
+- 2017-10-06: advanceOperationTime MUST NOT validate operationTime
+
+- 2017-10-05: How to handle default read concern
+
+- 2017-10-04: Added advanceOperationTime
+
+- 2017-09-28: Remove remaining references to collections being associated with sessions. Update spec to reflect that
+ replica sets use $clusterTime also now.
+
+- 2017-09-13: Renamed "causally consistent reads" to "causal consistency". If no value is supplied for
+ `causallyConsistent` assume true.
diff --git a/source/causal-consistency/causal-consistency.rst b/source/causal-consistency/causal-consistency.rst
index 1b16d69dc3..1853a1eafd 100644
--- a/source/causal-consistency/causal-consistency.rst
+++ b/source/causal-consistency/causal-consistency.rst
@@ -1,524 +1,4 @@
-================================
-Causal Consistency Specification
-================================
-:Spec Title: Causal Consistency Specification (See the registry of specs)
-:Spec Version: 1.0
-:Author: Robert Stam
-:Spec Lead: A\. Jesse Jiryu Davis
-:Advisory Group: Jeremy Mikola, Jeff Yemin, Misha Tyulene, A. Jesse Jiryu Davis
-:Approver(s): A\. Jesse Jiryu Davis, Jeff Yemin, Bernie Hackett, David Golden, Matt Broadstone, Andy Schwerin, Kal Manassiev, Eliot
-:Informed: drivers@, Bryan Reinero, Christopher Hendel
-:Status: Accepted (Could be Draft, Accepted, Rejected, Final, or Replaced)
-:Type: Standards
-:Minimum Server Version: 3.6 (The minimum server version this spec applies to)
-:Last Modified: 17-Nov-2017
-
-.. contents::
-
---------
-
-Abstract
-========
-
-Version 3.6 of the server introduces support for causal consistency.
-This spec builds upon the Sessions Specification to define how an application
-requests causal consistency and how a driver interacts with the server
-to implement causal consistency.
-
-Definitions
-===========
-
-META
-----
-
-The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”,
-“SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be
-interpreted as described in `RFC 2119 `_.
-
-Terms
------
-
-Causal consistency
- A property that guarantees that an application can read its own writes and that
- a later read will never observe a version of the data that is older than an
- earlier read.
-
-ClientSession
- The driver object representing a client session and the operations that can be
- performed on it.
-
-Cluster time
- The current cluster time. The server reports its view of the current cluster
- time in the ``$clusterTime`` field in responses from the server and the driver
- participates in distributing the current cluster time to all nodes (called
- "gossipping the cluster time") by sending the highest ``$clusterTime`` it has seen
- so far in messages it sends to mongos servers. The current cluster time is a
- logical time, but is digitally signed to prevent malicious clients from
- propagating invalid cluster times. Cluster time is only used in replica sets
- and sharded clusters.
-
-Logical time
- A time-like quantity that can be used to determine the order in which events
- occurred. Logical time is represented as a BsonTimestamp.
-
-MongoClient
- The root object of a driver's API. MAY be named differently in some drivers.
-
-MongoCollection
- The driver object representing a collection and the operations that can be
- performed on it. MAY be named differently in some drivers.
-
-MongoDatabase
- The driver object representing a database and the operations that can be
- performed on it. MAY be named differently in some drivers.
-
-Operation time
- The logical time at which an operation occurred. The server reports the
- operation time in the response to all commands, including error responses. The
- operation time by definition is always less than or equal to the cluster time.
- Operation times are tracked on a per ``ClientSession`` basis, so the ``operationTime``
- of each ``ClientSession`` corresponds to the time of the last operation performed
- in that particular ``ClientSession``.
-
-ServerSession
- The driver object representing a server session.
-
-Session
- A session is an abstract concept that represents a set of sequential
- operations executed by an application that are related in some way. This
- specification defines how sessions are used to implement causal
- consistency.
-
-Unacknowledged writes
- Unacknowledged writes are write operations that are sent to the server without
- waiting for a reply acknowledging the write. See the "Unacknowledged Writes"
- section below for information on how unacknowledged writes interact with
- causal consistency.
-
-Specification
-=============
-
-An application requests causal consistency by creating a ``ClientSession``
-with options that specify that causal consistency is desired. An
-application then passes the session as an argument to methods in the
-``MongoDatabase`` and ``MongoCollection`` classes. Any operations performed against
-that session will then be causally consistent.
-
-Naming variations
-=================
-
-This specification defines names for new methods and types. To the extent
-possible you SHOULD use these names in your driver. However, where your
-driver's and/or language's naming conventions differ you SHOULD continue to use
-them instead. For example, you might use ``StartSession`` or ``start_session`` instead
-of ``startSession``.
-
-High level summary of the API changes for causal consistency
-============================================================
-
-Causal consistency is built on top of client sessions.
-
-Applications will start a new client session for causal consistency like
-this:
-
-.. code:: typescript
-
- options = new SessionOptions(causalConsistency = true);
- session = client.startSession(options);
-
-All read operations performed using this session will now be causally
-consistent.
-
-If no value is provided for ``causalConsistency`` a value of true is
-implied. See the ``causalConsistency`` section.
-
-MongoClient changes
-===================
-
-There are no API changes to ``MongoClient`` to support causal consistency.
-Applications indicate whether they want causal consistency by setting the
-``causalConsistency`` field in the options passed to the ``startSession`` method.
-
-SessionOptions changes
-======================
-
-``SessionOptions`` change summary
-
-.. code:: typescript
-
- class SessionOptions {
- Optional causalConsistency;
-
- // other options defined by other specs
- }
-
-In order to support causal consistency a new property named
-``causalConsistency`` is added to ``SessionOptions``. Applications set
-``causalConsistency`` when starting a client session to indicate
-whether they want causal consistency. All read operations performed
-using that client session are then causally consistent.
-
-Each new member is documented below.
-
-causalConsistency
------------------
-
-Applications set ``causalConsistency`` when starting a session to
-indicate whether they want causal consistency.
-
-Note that the ``causalConsistency`` property is optional. The default value of
-this property is ``not supplied``. If no value is supplied for
-``causalConsistency`` the value will be inherited. Currently it is inherited
-from the global default which is defined to be true. In the future it *might*
-be inherited from client settings.
-
-Causal consistency is provided at the session level by tracking the ``clusterTime``
-and ``operationTime`` for each session. In some cases an application may wish
-subsequent operations in one session to be causally consistent with operations
-that were executed in a different session. In that case the application can call
-the ``advanceClusterTime`` and ``advanceOperationTime`` methods in ``ClientSession`` to
-advance the ``clusterTime`` and ``operationTime`` of one session to the ``clusterTime`` and
-``operationTime`` from another session.
-
-ClientSession changes
-=====================
-
-``ClientSession`` changes summary
-
-.. code:: typescript
-
- interface ClientSession {
- Optional operationTime;
-
- void advanceOperationTime(BsonTimestamp operationTime);
-
- // other members as defined in other specs
- }
-
-Each new member is documented below.
-
-operationTime
--------------
-
-This property returns the operation time of the most recent operation performed
-using this session. If no operations have been performed using this session the value will be
-null unless ``advanceOperationTime`` has been called.
-This value will also be null when the cluster does not report
-operation times.
-
-advanceOperationTime
---------------------
-
-This method advances the ``operationTime`` for a session. If the new
-``operationTime`` is greater than the session's current ``operationTime`` then the
-session's ``operationTime`` MUST be advanced to the new ``operationTime``. If the
-new ``operationTime`` is less than or equal to the session's current
-``operationTime`` then the session's ``operationTime`` MUST NOT be changed.
-
-Drivers MUST NOT attempt to validate the supplied ``operationTime``. While the
-server requires that ``operationTime`` be less than or equal to ``clusterTime``
-we don't want to check that when ``advanceOperationTime`` is called. This
-allows an application to call ``advanceClusterTime`` and
-``advanceOperationTime`` in any order, or perhaps to not call
-``advanceClusterTime`` at all and let the ``clusterTime`` that is sent to the
-server be implied by the ``clusterTime`` in ``MongoClient``.
-
-MongoDatabase changes
-=====================
-
-There are no additional API changes to ``MongoDatabase`` beyond those specified in
-the Sessions Specification. All ``MongoDatabase`` methods that talk to the server
-have been overloaded to take a session parameter. If that session was started
-with ``causalConsistency = true`` then all operations using that session will
-be causally consistent.
-
-MongoCollection changes
-=======================
-
-There are no additional API changes to ``MongoCollection`` beyond those specified
-in the Sessions Specification. All ``MongoCollection`` methods that talk to the
-server have been overloaded to take a session parameter. If that session was
-started with ``causalConsistency = true`` then all operations using that
-session will be causally consistent.
-
-Server Commands
-===============
-
-There are no new server commands related to causal consistency. Instead,
-causal consistency is implemented by:
-
-1. Saving the ``operationTime`` returned by 3.6+ servers for all operations in a
- property of the ``ClientSession`` object. The server reports the ``operationTime``
- whether the operation succeeded or not and drivers MUST save the ``operationTime``
- in the ``ClientSession`` whether the operation succeeded or not.
-
-2. Passing that ``operationTime`` in the ``afterClusterTime`` field of the ``readConcern`` field
- for subsequent causally consistent read operations (for all commands that
- support a ``readConcern``)
-
-3. Gossiping clusterTime (described in the Driver Session Specification)
-
-Server Command Responses
-========================
-
-To support causal consistency the server returns the ``operationTime`` in
-responses it sends to the driver (for both read and write operations).
-
-.. code:: typescript
-
- {
- ok : 1 or 0,
- ... // the rest of the command reply
- operationTime :
- $clusterTime : // only in deployments that support cluster times
- }
-
-The ``operationTime`` MUST be stored in the ``ClientSession`` to later be passed as the
-``afterClusterTime`` field of the ``readConcern`` field in subsequent read operations. The
-``operationTime`` is returned whether the command succeeded or not and MUST be
-stored in either case.
-
-Drivers MUST examine all responses from the server for the presence of an
-``operationTime`` field and store the value in the ``ClientSession``.
-
-When connected to a standalone node command replies do not include an
-``operationTime`` field. All operations against a standalone node are causally
-consistent automatically because there is only one node.
-
-When connected to a deployment that supports cluster times the command response also includes a field
-called ``$clusterTime`` that drivers MUST use to gossip the cluster time. See the
-Sessions Specification for details.
-
-Causally consistent read commands
-=================================
-
-For causal consistency the driver MUST send the ``operationTime`` saved in
-the ``ClientSession`` as the value of the ``afterClusterTime`` field of the
-``readConcern`` field:
-
-.. code:: typescript
-
- {
- find : , // or other read command
- ... // the rest of the command parameters
- readConcern :
- {
- level : ..., // from the operation's read concern (only if specified)
- afterClusterTime :
- }
- }
-
-For the lists of commands that support causally consistent reads, see `ReadConcern`_ spec.
-
-.. _ReadConcern: https://github.com/mongodb/specifications/blob/master/source/read-write-concern/read-write-concern.rst#read-concern/
-
-The driver MUST merge the ``ReadConcern`` specified for the operation with the
-``operationTime`` from the ``ClientSession`` (which goes in the ``afterClusterTime`` field)
-to generate the combined ``readConcern`` to send to the server. If the level
-property of the read concern for the operation is null then the driver MUST NOT
-include a ``level`` field alongside the ``afterClusterTime`` of the ``readConcern``
-value sent to the
-server. Drivers MUST NOT attempt to verify whether the server supports causally
-consistent reads or not for a given read concern level. The server will return
-an error if a given level does not support causal consistency.
-
-The Read and Write Concern specification states that when a user has not specified a
-``ReadConcern`` or has specified the server's default ``ReadConcern``, drivers MUST
-omit the ``ReadConcern`` parameter when sending the command. For causally
-consistent reads this requirement is modified to state that when the
-``ReadConcern`` parameter would normally be omitted drivers MUST send a ``ReadConcern``
-after all because that is how the ``afterClusterTime`` value is sent to the server.
-
-The Read and Write Concern Specification states that drivers MUST NOT add a
-``readConcern`` field to commands that are run using a generic ``runCommand`` method.
-The same is true for causal consistency, so commands that are run using ``runCommand``
-MUST NOT have an ``afterClusterTime`` field added to them.
-
-When executing a causally consistent read, the ``afterClusterTime`` field MUST be
-sent when connected to a deployment that supports cluster times, and MUST NOT be sent
-when connected to a deployment that does not support cluster times.
-
-Unacknowledged writes
-=====================
-
-The implementation of causal consistency relies on the ``operationTime``
-returned by the server in the acknowledgement of a write. Since unacknowledged
-writes don't receive a response from the server (or don't wait for a response)
-the ``ClientSession``'s ``operationTime`` is not updated after an unacknowledged write.
-That means that a causally consistent read after an unacknowledged write cannot
-be causally consistent with the unacknowledged write. Rather than prohibiting
-unacknowledged writes in a causally consistent session we have decided to
-accept this limitation. Drivers MUST document that causally consistent reads
-are not causally consistent with unacknowledged writes.
-
-Test Plan
-=========
-
-Below is a list of test cases to write.
-
-Note: some tests are only relevant to certain deployments. For the purpose of deciding
-which tests to run assume that any deployment that is version 3.6 or higher and is either a
-replica set or a sharded cluster supports cluster times.
-
-1. When a ``ClientSession`` is first created the ``operationTime`` has no value
- * ``session = client.startSession()``
- * assert ``session.operationTime`` has no value
-
-2. The first read in a causally consistent session must not send
- ``afterClusterTime`` to the server (because the ``operationTime`` has not yet
- been determined)
- * ``session = client.startSession(causalConsistency = true)``
- * ``document = collection.anyReadOperation(session, ...)``
- * capture the command sent to the server (using APM or other mechanism)
- * assert that the command does not have an ``afterClusterTime``
-
-3. The first read or write on a ``ClientSession`` should update the
- ``operationTime`` of the ``ClientSession``, even if there is an error
- * skip this test if connected to a deployment that does not support cluster times
- * ``session = client.startSession() // with or without causal consistency``
- * ``collection.anyReadOrWriteOperation(session, ...) // test with errors also if possible``
- * capture the response sent from the server (using APM or other mechanism)
- * assert ``session.operationTime`` has the same value that is in the response from the server
-
-4. A ``findOne`` followed by any other read operation (test them all) should
- include the ``operationTime`` returned by the server for the first operation in
- the ``afterClusterTime`` parameter of the second operation
- * skip this test if connected to a deployment that does not support cluster times
- * ``session = client.startSession(causalConsistency = true)``
- * ``collection.findOne(session, {})``
- * ``operationTime = session.operationTime``
- * ``collection.anyReadOperation(session, ...)``
- * capture the command sent to the server (using APM or other mechanism)
- * assert that the command has an ``afterClusterTime`` field with a value of ``operationTime``
-
-5. Any write operation (test them all) followed by a ``findOne`` operation should
- include the ``operationTime`` of the first operation in the ``afterClusterTime``
- parameter of the second operation, including the case where the first operation
- returned an error
- * skip this test if connected to a deployment that does not support cluster times
- * ``session = client.startSession(causalConsistency = true)``
- * ``collection.anyWriteOperation(session, ...) // test with errors also where possible``
- * ``operationTime = session.operationTime``
- * ``collection.findOne(session, {})``
- * capture the command sent to the server (using APM or other mechanism)
- * assert that the command has an ``afterClusterTime`` field with a value of ``operationTime``
-
-6. A read operation in a ``ClientSession`` that is not causally consistent
- should not include the ``afterClusterTime`` parameter in the command sent to the
- server
- * skip this test if connected to a deployment that does not support cluster times
- * ``session = client.startSession(causalConsistency = false)``
- * ``collection.anyReadOperation(session, {})``
- * ``operationTime = session.operationTime``
- * capture the command sent to the server (using APM or other mechanism)
- * assert that the command does not have an ``afterClusterTime`` field
-
-7. A read operation in a causally consistent session against a deployment that does not support cluster times does
- not include the ``afterClusterTime`` parameter in the command sent to the server
- * skip this test if connected to a deployment that does support cluster times
- * ``session = client.startSession(causalConsistency = true)``
- * ``collection.anyReadOperation(session, {})``
- * capture the command sent to the server (using APM or other mechanism)
- * assert that the command does not have an ``afterClusterTime`` field
-
-8. When using the default server ``ReadConcern`` the ``readConcern`` parameter in the
- command sent to the server should not include a ``level`` field
- * skip this test if connected to a deployment that does not support cluster times
- * ``session = client.startSession(causalConsistency = true)``
- * configure ``collection`` to use default server ``ReadConcern``
- * ``collection.findOne(session, {})``
- * ``operationTime = session.operationTime``
- * ``collection.anyReadOperation(session, ...)``
- * capture the command sent to the server (using APM or other mechanism)
- * assert that the command does not have a ```level`` field
- * assert that the command has a ``afterClusterTime`` field with a value of ``operationTime``
-
-9. When using a custom ``ReadConcern`` the ``readConcern`` field in the command sent to
- the server should be a merger of the ``ReadConcern`` value and the ``afterClusterTime``
- field
- * skip this test if connected to a deployment that does not support cluster times
- * ``session = client.startSession(causalConsistency = true)``
- * configure collection to use a custom ReadConcern
- * ``collection.findOne(session, {})``
- * ``operationTime = session.operationTime``
- * ``collection.anyReadOperation(session, ...)``
- * capture the command sent to the server (using APM or other mechanism)
- * assert that the command has a ``level`` field with a value matching the custom readConcern
- * assert that the command has an ``afterClusterTime`` field with a value of ``operationTime``
-
-10. When an unacknowledged write is executed in a causally consistent
- ``ClientSession`` the ``operationTime`` property of the ``ClientSession`` is
- not updated
- * ``session = client.startSession(causalConsistency = true)``
- * configure the collection to use ``{ w : 0 }`` unacknowledged writes
- * ``collection.anyWriteOperation(session, ...)``
- * assert ``session.operationTime`` does not have a value
-
-11. When connected to a deployment that does not support cluster times messages sent to
- the server should not include ``$clusterTime``
- * skip this test when connected to a deployment that does support cluster times
- * ``document = collection.findOne({})``
- * capture the command sent to the server
- * assert that the command does not include a ``$clusterTime`` field
-
-12. When connected to a deployment that does support cluster times messages sent to the server should
- include ``$clusterTime``
- * skip this test when connected to a deployment that does not support cluster times
- * ``document = collection.findOne({})``
- * capture the command sent to the server
- * assert that the command includes a ``$clusterTime`` field
-
-Motivation
-==========
-
-To support causal consistency. Only supported with server version 3.6 or newer.
-
-Design Rationale
-================
-
-The goal is to modify the driver API as little as possible so that existing
-programs that don't need causal consistency don't have to be changed.
-This goal is met by defining a ``SessionOptions`` field that applications use to
-start a ``ClientSession`` that can be used for causal consistency. Any
-operations performed with such a session are then causally consistent.
-
-The ``operationTime`` is tracked on a per ``ClientSession`` basis. This allows each
-``ClientSession`` to have an ``operationTime`` that is sufficiently new to guarantee
-causal consistency for that session, but no newer. Using an ``operationTime`` that
-is newer than necessary can cause reads to block longer than necessary when
-sent to a lagging secondary. The goal is to block for just long enough to
-guarantee causal consistency and no longer.
-
-Backwards Compatibility
-=======================
-
-The API changes to support sessions extend the existing API but do not
-introduce any backward breaking changes. Existing programs that don't use
-causal consistency continue to compile and run correctly.
-
-Reference Implementation
-========================
-
-A reference implementation must be completed before any spec is given status
-"Final", but it need not be completed before the spec is “Accepted”. While
-there is merit to the approach of reaching consensus on the specification and
-rationale before writing code, the principle of "rough consensus and running
-code" is still useful when it comes to resolving many discussions of spec
-details. A final reference implementation must include test code and
-documentation.
-
-Q&A
-===
-
-Changelog
-=========
-
-- 2017-09-13: Renamed "causally consistent reads" to "causal consistency"
-- 2017-09-13: If no value is supplied for ``causallyConsistent`` assume true
-- 2017-09-28: Remove remaining references to collections being associated with sessions
-- 2017-09-28: Update spec to reflect that replica sets use $clusterTime also now
-- 2017-10-04: Added advanceOperationTime
-- 2017-10-05: How to handle default read concern
-- 2017-10-06: advanceOperationTime MUST NOT validate operationTime
-- 2017-11-17 : Added link to ReadConcern spec which lists commands that support readConcern
+.. note::
+ This specification has been converted to Markdown and renamed to
+ `causal-consistency.md `_.
diff --git a/source/causal-consistency/tests/causal-consistency-clientBulkWrite.json b/source/causal-consistency/tests/causal-consistency-clientBulkWrite.json
new file mode 100644
index 0000000000..c2a04422dd
--- /dev/null
+++ b/source/causal-consistency/tests/causal-consistency-clientBulkWrite.json
@@ -0,0 +1,151 @@
+{
+ "description": "causal consistency bulkWrite include afterClusterTime",
+ "schemaVersion": "1.3",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "8.0",
+ "topologies": [
+ "replicaset",
+ "sharded",
+ "load-balanced"
+ ]
+ }
+ ],
+ "createEntities": [
+ {
+ "client": {
+ "id": "client0",
+ "useMultipleMongoses": false,
+ "uriOptions": {
+ "retryWrites": false
+ },
+ "observeEvents": [
+ "commandStartedEvent"
+ ]
+ }
+ },
+ {
+ "database": {
+ "id": "database0",
+ "client": "client0",
+ "databaseName": "causal-consistency-tests"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection0",
+ "database": "database0",
+ "collectionName": "test"
+ }
+ },
+ {
+ "session": {
+ "id": "session0",
+ "client": "client0",
+ "sessionOptions": {
+ "causalConsistency": true
+ }
+ }
+ }
+ ],
+ "initialData": [
+ {
+ "collectionName": "test",
+ "databaseName": "causal-consistency-tests",
+ "documents": [
+ {
+ "_id": 1,
+ "x": 11
+ },
+ {
+ "_id": 2,
+ "x": 22
+ },
+ {
+ "_id": 3,
+ "x": 33
+ }
+ ]
+ }
+ ],
+ "tests": [
+ {
+ "description": "clientBulkWrite includes afterClusterTime in causally consistent session",
+ "operations": [
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ }
+ },
+ "expectResult": [
+ {
+ "_id": 1,
+ "x": 11
+ }
+ ]
+ },
+ {
+ "name": "clientBulkWrite",
+ "object": "client0",
+ "arguments": {
+ "session": "session0",
+ "models": [
+ {
+ "insertOne": {
+ "namespace": "causal-consistency-tests.test",
+ "document": {
+ "_id": 4
+ }
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find",
+ "command": {
+ "find": "test",
+ "readConcern": {
+ "$$exists": false
+ },
+ "lsid": {
+ "$$sessionLsid": "session0"
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "bulkWrite",
+ "command": {
+ "bulkWrite": 1,
+ "lsid": {
+ "$$sessionLsid": "session0"
+ },
+ "readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ },
+ "level": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/source/causal-consistency/tests/causal-consistency-clientBulkWrite.yml b/source/causal-consistency/tests/causal-consistency-clientBulkWrite.yml
new file mode 100644
index 0000000000..d1902e188e
--- /dev/null
+++ b/source/causal-consistency/tests/causal-consistency-clientBulkWrite.yml
@@ -0,0 +1,75 @@
+description: "causal consistency bulkWrite include afterClusterTime"
+
+schemaVersion: "1.3"
+
+runOnRequirements:
+ - minServerVersion: "8.0"
+ topologies: [replicaset, sharded, load-balanced]
+
+createEntities:
+ - client:
+ id: &client0 client0
+ useMultipleMongoses: false
+ uriOptions:
+ retryWrites: false
+ observeEvents: [commandStartedEvent]
+ - database:
+ id: &database0 database0
+ client: *client0
+ databaseName: &databaseName causal-consistency-tests
+ - collection:
+ id: &collection0 collection0
+ database: *database0
+ collectionName: &collectionName test
+ - session:
+ id: &session0 session0
+ client: *client0
+ sessionOptions:
+ causalConsistency: true
+
+initialData:
+ - collectionName: *collectionName
+ databaseName: *databaseName
+ documents:
+ - { _id: 1, x: 11 }
+ - { _id: 2, x: 22 }
+ - { _id: 3, x: 33 }
+
+# In a causally consistent session, once an operationTime has been established by a prior
+# operation, subsequent write commands MUST include readConcern.afterClusterTime so the
+# server can apply the write causally after the previously-observed data.
+
+tests:
+ - description: "clientBulkWrite includes afterClusterTime in causally consistent session"
+ operations:
+ - name: find
+ object: *collection0
+ arguments:
+ session: *session0
+ filter: { _id: 1 }
+ expectResult: [{ _id: 1, x: 11 }]
+ - name: clientBulkWrite
+ object: *client0
+ arguments:
+ session: *session0
+ models:
+ - insertOne:
+ namespace: causal-consistency-tests.test
+ document: { _id: 4 }
+ expectEvents:
+ - client: *client0
+ events:
+ - commandStartedEvent:
+ commandName: find
+ command:
+ find: *collectionName
+ readConcern: { $$exists: false }
+ lsid: { $$sessionLsid: *session0 }
+ - commandStartedEvent:
+ commandName: bulkWrite
+ command:
+ bulkWrite: 1
+ lsid: { $$sessionLsid: *session0 }
+ readConcern:
+ afterClusterTime: { $$exists: true }
+ level: { $$exists: false }
diff --git a/source/causal-consistency/tests/causal-consistency-write-commands.json b/source/causal-consistency/tests/causal-consistency-write-commands.json
new file mode 100644
index 0000000000..a8140c2b3f
--- /dev/null
+++ b/source/causal-consistency/tests/causal-consistency-write-commands.json
@@ -0,0 +1,1396 @@
+{
+ "description": "causal consistency write commands include afterClusterTime",
+ "schemaVersion": "1.3",
+ "runOnRequirements": [
+ {
+ "topologies": [
+ "replicaset",
+ "sharded",
+ "load-balanced"
+ ]
+ }
+ ],
+ "createEntities": [
+ {
+ "client": {
+ "id": "client0",
+ "useMultipleMongoses": false,
+ "uriOptions": {
+ "retryWrites": false
+ },
+ "observeEvents": [
+ "commandStartedEvent"
+ ]
+ }
+ },
+ {
+ "database": {
+ "id": "database0",
+ "client": "client0",
+ "databaseName": "causal-consistency-tests"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection0",
+ "database": "database0",
+ "collectionName": "test"
+ }
+ },
+ {
+ "session": {
+ "id": "session0",
+ "client": "client0",
+ "sessionOptions": {
+ "causalConsistency": true
+ }
+ }
+ }
+ ],
+ "initialData": [
+ {
+ "collectionName": "test",
+ "databaseName": "causal-consistency-tests",
+ "documents": [
+ {
+ "_id": 1,
+ "x": 11
+ },
+ {
+ "_id": 2,
+ "x": 22
+ },
+ {
+ "_id": 3,
+ "x": 33
+ }
+ ]
+ }
+ ],
+ "tests": [
+ {
+ "description": "insertOne includes afterClusterTime in causally consistent session",
+ "operations": [
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ }
+ },
+ "expectResult": [
+ {
+ "_id": 1,
+ "x": 11
+ }
+ ]
+ },
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "document": {
+ "_id": 4
+ }
+ },
+ "expectResult": {
+ "$$unsetOrMatches": {
+ "insertedId": {
+ "$$unsetOrMatches": 4
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find",
+ "command": {
+ "find": "test",
+ "readConcern": {
+ "$$exists": false
+ },
+ "lsid": {
+ "$$sessionLsid": "session0"
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "insert",
+ "command": {
+ "insert": "test",
+ "lsid": {
+ "$$sessionLsid": "session0"
+ },
+ "readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ },
+ "level": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "insertMany includes afterClusterTime in causally consistent session",
+ "operations": [
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ }
+ },
+ "expectResult": [
+ {
+ "_id": 1,
+ "x": 11
+ }
+ ]
+ },
+ {
+ "name": "insertMany",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "documents": [
+ {
+ "_id": 4
+ },
+ {
+ "_id": 5
+ }
+ ]
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find",
+ "command": {
+ "find": "test",
+ "readConcern": {
+ "$$exists": false
+ },
+ "lsid": {
+ "$$sessionLsid": "session0"
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "insert",
+ "command": {
+ "insert": "test",
+ "lsid": {
+ "$$sessionLsid": "session0"
+ },
+ "readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ },
+ "level": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "updateOne includes afterClusterTime in causally consistent session",
+ "operations": [
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ }
+ },
+ "expectResult": [
+ {
+ "_id": 1,
+ "x": 11
+ }
+ ]
+ },
+ {
+ "name": "updateOne",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ },
+ "update": {
+ "$set": {
+ "x": 100
+ }
+ }
+ },
+ "expectResult": {
+ "matchedCount": 1,
+ "modifiedCount": 1,
+ "upsertedCount": 0
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find",
+ "command": {
+ "find": "test",
+ "readConcern": {
+ "$$exists": false
+ },
+ "lsid": {
+ "$$sessionLsid": "session0"
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "update",
+ "command": {
+ "update": "test",
+ "lsid": {
+ "$$sessionLsid": "session0"
+ },
+ "readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ },
+ "level": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "updateMany includes afterClusterTime in causally consistent session",
+ "operations": [
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ }
+ },
+ "expectResult": [
+ {
+ "_id": 1,
+ "x": 11
+ }
+ ]
+ },
+ {
+ "name": "updateMany",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": {
+ "$gt": 0
+ }
+ },
+ "update": {
+ "$set": {
+ "updated": true
+ }
+ }
+ },
+ "expectResult": {
+ "matchedCount": 3,
+ "modifiedCount": 3,
+ "upsertedCount": 0
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find",
+ "command": {
+ "find": "test",
+ "readConcern": {
+ "$$exists": false
+ },
+ "lsid": {
+ "$$sessionLsid": "session0"
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "update",
+ "command": {
+ "update": "test",
+ "lsid": {
+ "$$sessionLsid": "session0"
+ },
+ "readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ },
+ "level": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "replaceOne includes afterClusterTime in causally consistent session",
+ "operations": [
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ }
+ },
+ "expectResult": [
+ {
+ "_id": 1,
+ "x": 11
+ }
+ ]
+ },
+ {
+ "name": "replaceOne",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ },
+ "replacement": {
+ "x": 100
+ }
+ },
+ "expectResult": {
+ "matchedCount": 1,
+ "modifiedCount": 1,
+ "upsertedCount": 0
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find",
+ "command": {
+ "find": "test",
+ "readConcern": {
+ "$$exists": false
+ },
+ "lsid": {
+ "$$sessionLsid": "session0"
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "update",
+ "command": {
+ "update": "test",
+ "lsid": {
+ "$$sessionLsid": "session0"
+ },
+ "readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ },
+ "level": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "deleteOne includes afterClusterTime in causally consistent session",
+ "operations": [
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ }
+ },
+ "expectResult": [
+ {
+ "_id": 1,
+ "x": 11
+ }
+ ]
+ },
+ {
+ "name": "deleteOne",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ }
+ },
+ "expectResult": {
+ "deletedCount": 1
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find",
+ "command": {
+ "find": "test",
+ "readConcern": {
+ "$$exists": false
+ },
+ "lsid": {
+ "$$sessionLsid": "session0"
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "delete",
+ "command": {
+ "delete": "test",
+ "lsid": {
+ "$$sessionLsid": "session0"
+ },
+ "readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ },
+ "level": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "deleteMany includes afterClusterTime in causally consistent session",
+ "operations": [
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ }
+ },
+ "expectResult": [
+ {
+ "_id": 1,
+ "x": 11
+ }
+ ]
+ },
+ {
+ "name": "deleteMany",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": {
+ "$gt": 0
+ }
+ }
+ },
+ "expectResult": {
+ "deletedCount": 3
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find",
+ "command": {
+ "find": "test",
+ "readConcern": {
+ "$$exists": false
+ },
+ "lsid": {
+ "$$sessionLsid": "session0"
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "delete",
+ "command": {
+ "delete": "test",
+ "lsid": {
+ "$$sessionLsid": "session0"
+ },
+ "readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ },
+ "level": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "findOneAndUpdate includes afterClusterTime in causally consistent session",
+ "operations": [
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ }
+ },
+ "expectResult": [
+ {
+ "_id": 1,
+ "x": 11
+ }
+ ]
+ },
+ {
+ "name": "findOneAndUpdate",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ },
+ "update": {
+ "$set": {
+ "x": 100
+ }
+ }
+ },
+ "expectResult": {
+ "_id": 1,
+ "x": 11
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find",
+ "command": {
+ "find": "test",
+ "readConcern": {
+ "$$exists": false
+ },
+ "lsid": {
+ "$$sessionLsid": "session0"
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify",
+ "command": {
+ "findAndModify": "test",
+ "lsid": {
+ "$$sessionLsid": "session0"
+ },
+ "readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ },
+ "level": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "findOneAndDelete includes afterClusterTime in causally consistent session",
+ "operations": [
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ }
+ },
+ "expectResult": [
+ {
+ "_id": 1,
+ "x": 11
+ }
+ ]
+ },
+ {
+ "name": "findOneAndDelete",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ }
+ },
+ "expectResult": {
+ "_id": 1,
+ "x": 11
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find",
+ "command": {
+ "find": "test",
+ "readConcern": {
+ "$$exists": false
+ },
+ "lsid": {
+ "$$sessionLsid": "session0"
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify",
+ "command": {
+ "findAndModify": "test",
+ "lsid": {
+ "$$sessionLsid": "session0"
+ },
+ "readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ },
+ "level": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "findOneAndReplace includes afterClusterTime in causally consistent session",
+ "operations": [
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ }
+ },
+ "expectResult": [
+ {
+ "_id": 1,
+ "x": 11
+ }
+ ]
+ },
+ {
+ "name": "findOneAndReplace",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ },
+ "replacement": {
+ "x": 100
+ }
+ },
+ "expectResult": {
+ "_id": 1,
+ "x": 11
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find",
+ "command": {
+ "find": "test",
+ "readConcern": {
+ "$$exists": false
+ },
+ "lsid": {
+ "$$sessionLsid": "session0"
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify",
+ "command": {
+ "findAndModify": "test",
+ "lsid": {
+ "$$sessionLsid": "session0"
+ },
+ "readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ },
+ "level": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "bulkWrite includes afterClusterTime in causally consistent session",
+ "operations": [
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ }
+ },
+ "expectResult": [
+ {
+ "_id": 1,
+ "x": 11
+ }
+ ]
+ },
+ {
+ "name": "bulkWrite",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "requests": [
+ {
+ "insertOne": {
+ "document": {
+ "_id": 4
+ }
+ }
+ },
+ {
+ "updateOne": {
+ "filter": {
+ "_id": 2
+ },
+ "update": {
+ "$set": {
+ "x": 100
+ }
+ }
+ }
+ },
+ {
+ "deleteOne": {
+ "filter": {
+ "_id": 3
+ }
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find",
+ "command": {
+ "find": "test",
+ "readConcern": {
+ "$$exists": false
+ },
+ "lsid": {
+ "$$sessionLsid": "session0"
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "insert",
+ "command": {
+ "insert": "test",
+ "lsid": {
+ "$$sessionLsid": "session0"
+ },
+ "readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ },
+ "level": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "update",
+ "command": {
+ "update": "test",
+ "lsid": {
+ "$$sessionLsid": "session0"
+ },
+ "readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ },
+ "level": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "delete",
+ "command": {
+ "delete": "test",
+ "lsid": {
+ "$$sessionLsid": "session0"
+ },
+ "readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ },
+ "level": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "create includes afterClusterTime in causally consistent session",
+ "operations": [
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ }
+ },
+ "expectResult": [
+ {
+ "_id": 1,
+ "x": 11
+ }
+ ]
+ },
+ {
+ "name": "dropCollection",
+ "object": "database0",
+ "arguments": {
+ "session": "session0",
+ "collection": "causal-consistency-createCollection-test"
+ }
+ },
+ {
+ "name": "createCollection",
+ "object": "database0",
+ "arguments": {
+ "session": "session0",
+ "collection": "causal-consistency-createCollection-test"
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find",
+ "command": {
+ "find": "test",
+ "readConcern": {
+ "$$exists": false
+ },
+ "lsid": {
+ "$$sessionLsid": "session0"
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "drop",
+ "command": {
+ "drop": "causal-consistency-createCollection-test",
+ "lsid": {
+ "$$sessionLsid": "session0"
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "create",
+ "command": {
+ "create": "causal-consistency-createCollection-test",
+ "lsid": {
+ "$$sessionLsid": "session0"
+ },
+ "readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ },
+ "level": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "createIndexes includes afterClusterTime in causally consistent session",
+ "operations": [
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ }
+ },
+ "expectResult": [
+ {
+ "_id": 1,
+ "x": 11
+ }
+ ]
+ },
+ {
+ "name": "createIndex",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "keys": {
+ "x": 1
+ },
+ "name": "x_1"
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find",
+ "command": {
+ "find": "test",
+ "readConcern": {
+ "$$exists": false
+ },
+ "lsid": {
+ "$$sessionLsid": "session0"
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "createIndexes",
+ "command": {
+ "createIndexes": "test",
+ "lsid": {
+ "$$sessionLsid": "session0"
+ },
+ "readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ },
+ "level": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "drop includes afterClusterTime in causally consistent session",
+ "operations": [
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ }
+ },
+ "expectResult": [
+ {
+ "_id": 1,
+ "x": 11
+ }
+ ]
+ },
+ {
+ "name": "dropCollection",
+ "object": "database0",
+ "arguments": {
+ "session": "session0",
+ "collection": "test"
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find",
+ "command": {
+ "find": "test",
+ "readConcern": {
+ "$$exists": false
+ },
+ "lsid": {
+ "$$sessionLsid": "session0"
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "drop",
+ "command": {
+ "drop": "test",
+ "lsid": {
+ "$$sessionLsid": "session0"
+ },
+ "readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ },
+ "level": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "dropDatabase includes afterClusterTime in causally consistent session",
+ "operations": [
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ }
+ },
+ "expectResult": [
+ {
+ "_id": 1,
+ "x": 11
+ }
+ ]
+ },
+ {
+ "name": "dropDatabase",
+ "object": "client0",
+ "arguments": {
+ "session": "session0",
+ "database": "causal-consistency-dropDatabase-test"
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find",
+ "command": {
+ "find": "test",
+ "readConcern": {
+ "$$exists": false
+ },
+ "lsid": {
+ "$$sessionLsid": "session0"
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "dropDatabase",
+ "command": {
+ "dropDatabase": 1,
+ "$db": "causal-consistency-dropDatabase-test",
+ "lsid": {
+ "$$sessionLsid": "session0"
+ },
+ "readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ },
+ "level": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "dropIndexes includes afterClusterTime in causally consistent session",
+ "operations": [
+ {
+ "name": "find",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "filter": {
+ "_id": 1
+ }
+ },
+ "expectResult": [
+ {
+ "_id": 1,
+ "x": 11
+ }
+ ]
+ },
+ {
+ "name": "dropIndexes",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0"
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find",
+ "command": {
+ "find": "test",
+ "readConcern": {
+ "$$exists": false
+ },
+ "lsid": {
+ "$$sessionLsid": "session0"
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "dropIndexes",
+ "command": {
+ "dropIndexes": "test",
+ "lsid": {
+ "$$sessionLsid": "session0"
+ },
+ "readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ },
+ "level": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "first write command in a causally consistent session does not include afterClusterTime",
+ "operations": [
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "session": "session0",
+ "document": {
+ "_id": 4
+ }
+ },
+ "expectResult": {
+ "$$unsetOrMatches": {
+ "insertedId": {
+ "$$unsetOrMatches": 4
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "insert",
+ "command": {
+ "insert": "test",
+ "lsid": {
+ "$$sessionLsid": "session0"
+ },
+ "readConcern": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/source/causal-consistency/tests/causal-consistency-write-commands.yml b/source/causal-consistency/tests/causal-consistency-write-commands.yml
new file mode 100644
index 0000000000..a33ca17436
--- /dev/null
+++ b/source/causal-consistency/tests/causal-consistency-write-commands.yml
@@ -0,0 +1,472 @@
+description: "causal consistency write commands include afterClusterTime"
+
+schemaVersion: "1.3"
+
+runOnRequirements:
+ - topologies: [replicaset, sharded, load-balanced]
+
+createEntities:
+ - client:
+ id: &client0 client0
+ useMultipleMongoses: false
+ uriOptions:
+ retryWrites: false
+ observeEvents: [commandStartedEvent]
+ - database:
+ id: &database0 database0
+ client: *client0
+ databaseName: &databaseName causal-consistency-tests
+ - collection:
+ id: &collection0 collection0
+ database: *database0
+ collectionName: &collectionName test
+ - session:
+ id: &session0 session0
+ client: *client0
+ sessionOptions:
+ causalConsistency: true
+
+initialData:
+ - collectionName: *collectionName
+ databaseName: *databaseName
+ documents:
+ - { _id: 1, x: 11 }
+ - { _id: 2, x: 22 }
+ - { _id: 3, x: 33 }
+
+# In a causally consistent session, once an operationTime has been established by a prior
+# operation, subsequent write commands MUST include readConcern.afterClusterTime so the
+# server can apply the write causally after the previously-observed data.
+
+tests:
+ - description: "insertOne includes afterClusterTime in causally consistent session"
+ operations:
+ - &find
+ name: find
+ object: *collection0
+ arguments:
+ session: *session0
+ filter: { _id: 1 }
+ expectResult: [{ _id: 1, x: 11 }]
+ - name: insertOne
+ object: *collection0
+ arguments:
+ session: *session0
+ document: { _id: 4 }
+ expectResult:
+ $$unsetOrMatches: { insertedId: { $$unsetOrMatches: 4 } }
+ expectEvents:
+ - client: *client0
+ events:
+ - &findEvent
+ commandStartedEvent:
+ commandName: find
+ command:
+ find: *collectionName
+ readConcern: { $$exists: false }
+ lsid: { $$sessionLsid: *session0 }
+ - commandStartedEvent:
+ commandName: insert
+ command:
+ insert: *collectionName
+ lsid: { $$sessionLsid: *session0 }
+ readConcern:
+ afterClusterTime: { $$exists: true }
+ level: { $$exists: false }
+
+ - description: "insertMany includes afterClusterTime in causally consistent session"
+ operations:
+ - *find
+ - name: insertMany
+ object: *collection0
+ arguments:
+ session: *session0
+ documents:
+ - { _id: 4 }
+ - { _id: 5 }
+ expectEvents:
+ - client: *client0
+ events:
+ - *findEvent
+ - commandStartedEvent:
+ commandName: insert
+ command:
+ insert: *collectionName
+ lsid: { $$sessionLsid: *session0 }
+ readConcern:
+ afterClusterTime: { $$exists: true }
+ level: { $$exists: false }
+
+ - description: "updateOne includes afterClusterTime in causally consistent session"
+ operations:
+ - *find
+ - name: updateOne
+ object: *collection0
+ arguments:
+ session: *session0
+ filter: { _id: 1 }
+ update: { $set: { x: 100 } }
+ expectResult:
+ matchedCount: 1
+ modifiedCount: 1
+ upsertedCount: 0
+ expectEvents:
+ - client: *client0
+ events:
+ - *findEvent
+ - commandStartedEvent:
+ commandName: update
+ command:
+ update: *collectionName
+ lsid: { $$sessionLsid: *session0 }
+ readConcern:
+ afterClusterTime: { $$exists: true }
+ level: { $$exists: false }
+
+ - description: "updateMany includes afterClusterTime in causally consistent session"
+ operations:
+ - *find
+ - name: updateMany
+ object: *collection0
+ arguments:
+ session: *session0
+ filter: { _id: { $gt: 0 } }
+ update: { $set: { updated: true } }
+ expectResult:
+ matchedCount: 3
+ modifiedCount: 3
+ upsertedCount: 0
+ expectEvents:
+ - client: *client0
+ events:
+ - *findEvent
+ - commandStartedEvent:
+ commandName: update
+ command:
+ update: *collectionName
+ lsid: { $$sessionLsid: *session0 }
+ readConcern:
+ afterClusterTime: { $$exists: true }
+ level: { $$exists: false }
+
+ - description: "replaceOne includes afterClusterTime in causally consistent session"
+ operations:
+ - *find
+ - name: replaceOne
+ object: *collection0
+ arguments:
+ session: *session0
+ filter: { _id: 1 }
+ replacement: { x: 100 }
+ expectResult:
+ matchedCount: 1
+ modifiedCount: 1
+ upsertedCount: 0
+ expectEvents:
+ - client: *client0
+ events:
+ - *findEvent
+ - commandStartedEvent:
+ commandName: update
+ command:
+ update: *collectionName
+ lsid: { $$sessionLsid: *session0 }
+ readConcern:
+ afterClusterTime: { $$exists: true }
+ level: { $$exists: false }
+
+ - description: "deleteOne includes afterClusterTime in causally consistent session"
+ operations:
+ - *find
+ - name: deleteOne
+ object: *collection0
+ arguments:
+ session: *session0
+ filter: { _id: 1 }
+ expectResult:
+ deletedCount: 1
+ expectEvents:
+ - client: *client0
+ events:
+ - *findEvent
+ - commandStartedEvent:
+ commandName: delete
+ command:
+ delete: *collectionName
+ lsid: { $$sessionLsid: *session0 }
+ readConcern:
+ afterClusterTime: { $$exists: true }
+ level: { $$exists: false }
+
+ - description: "deleteMany includes afterClusterTime in causally consistent session"
+ operations:
+ - *find
+ - name: deleteMany
+ object: *collection0
+ arguments:
+ session: *session0
+ filter: { _id: { $gt: 0 } }
+ expectResult:
+ deletedCount: 3
+ expectEvents:
+ - client: *client0
+ events:
+ - *findEvent
+ - commandStartedEvent:
+ commandName: delete
+ command:
+ delete: *collectionName
+ lsid: { $$sessionLsid: *session0 }
+ readConcern:
+ afterClusterTime: { $$exists: true }
+ level: { $$exists: false }
+
+ - description: "findOneAndUpdate includes afterClusterTime in causally consistent session"
+ operations:
+ - *find
+ - name: findOneAndUpdate
+ object: *collection0
+ arguments:
+ session: *session0
+ filter: { _id: 1 }
+ update: { $set: { x: 100 } }
+ expectResult: { _id: 1, x: 11 }
+ expectEvents:
+ - client: *client0
+ events:
+ - *findEvent
+ - commandStartedEvent:
+ commandName: findAndModify
+ command:
+ findAndModify: *collectionName
+ lsid: { $$sessionLsid: *session0 }
+ readConcern:
+ afterClusterTime: { $$exists: true }
+ level: { $$exists: false }
+
+ - description: "findOneAndDelete includes afterClusterTime in causally consistent session"
+ operations:
+ - *find
+ - name: findOneAndDelete
+ object: *collection0
+ arguments:
+ session: *session0
+ filter: { _id: 1 }
+ expectResult: { _id: 1, x: 11 }
+ expectEvents:
+ - client: *client0
+ events:
+ - *findEvent
+ - commandStartedEvent:
+ commandName: findAndModify
+ command:
+ findAndModify: *collectionName
+ lsid: { $$sessionLsid: *session0 }
+ readConcern:
+ afterClusterTime: { $$exists: true }
+ level: { $$exists: false }
+
+ - description: "findOneAndReplace includes afterClusterTime in causally consistent session"
+ operations:
+ - *find
+ - name: findOneAndReplace
+ object: *collection0
+ arguments:
+ session: *session0
+ filter: { _id: 1 }
+ replacement: { x: 100 }
+ expectResult: { _id: 1, x: 11 }
+ expectEvents:
+ - client: *client0
+ events:
+ - *findEvent
+ - commandStartedEvent:
+ commandName: findAndModify
+ command:
+ findAndModify: *collectionName
+ lsid: { $$sessionLsid: *session0 }
+ readConcern:
+ afterClusterTime: { $$exists: true }
+ level: { $$exists: false }
+
+ - description: "bulkWrite includes afterClusterTime in causally consistent session"
+ operations:
+ - *find
+ - name: bulkWrite
+ object: *collection0
+ arguments:
+ session: *session0
+ requests:
+ - insertOne:
+ document: { _id: 4 }
+ - updateOne:
+ filter: { _id: 2 }
+ update: { $set: { x: 100 } }
+ - deleteOne:
+ filter: { _id: 3 }
+ expectEvents:
+ - client: *client0
+ events:
+ - *findEvent
+ - commandStartedEvent:
+ commandName: insert
+ command:
+ insert: *collectionName
+ lsid: { $$sessionLsid: *session0 }
+ readConcern:
+ afterClusterTime: { $$exists: true }
+ level: { $$exists: false }
+ - commandStartedEvent:
+ commandName: update
+ command:
+ update: *collectionName
+ lsid: { $$sessionLsid: *session0 }
+ readConcern:
+ afterClusterTime: { $$exists: true }
+ level: { $$exists: false }
+ - commandStartedEvent:
+ commandName: delete
+ command:
+ delete: *collectionName
+ lsid: { $$sessionLsid: *session0 }
+ readConcern:
+ afterClusterTime: { $$exists: true }
+ level: { $$exists: false }
+
+ - description: "create includes afterClusterTime in causally consistent session"
+ operations:
+ - *find
+ # Drop the collection first to make sure there's no name conflict during
+ # createCollection.
+ - name: dropCollection
+ object: *database0
+ arguments:
+ session: *session0
+ collection: &newCollectionName causal-consistency-createCollection-test
+ - name: createCollection
+ object: *database0
+ arguments:
+ session: *session0
+ collection: *newCollectionName
+ expectEvents:
+ - client: *client0
+ events:
+ - *findEvent
+ - commandStartedEvent:
+ commandName: drop
+ command:
+ drop: *newCollectionName
+ lsid: { $$sessionLsid: *session0 }
+ - commandStartedEvent:
+ commandName: create
+ command:
+ create: *newCollectionName
+ lsid: { $$sessionLsid: *session0 }
+ readConcern:
+ afterClusterTime: { $$exists: true }
+ level: { $$exists: false }
+
+ - description: "createIndexes includes afterClusterTime in causally consistent session"
+ operations:
+ - *find
+ - name: createIndex
+ object: *collection0
+ arguments:
+ session: *session0
+ keys: { x: 1 }
+ name: x_1
+ expectEvents:
+ - client: *client0
+ events:
+ - *findEvent
+ - commandStartedEvent:
+ commandName: createIndexes
+ command:
+ createIndexes: *collectionName
+ lsid: { $$sessionLsid: *session0 }
+ readConcern:
+ afterClusterTime: { $$exists: true }
+ level: { $$exists: false }
+
+ - description: "drop includes afterClusterTime in causally consistent session"
+ operations:
+ - *find
+ - name: dropCollection
+ object: *database0
+ arguments:
+ session: *session0
+ collection: *collectionName
+ expectEvents:
+ - client: *client0
+ events:
+ - *findEvent
+ - commandStartedEvent:
+ commandName: drop
+ command:
+ drop: *collectionName
+ lsid: { $$sessionLsid: *session0 }
+ readConcern:
+ afterClusterTime: { $$exists: true }
+ level: { $$exists: false }
+
+ - description: "dropDatabase includes afterClusterTime in causally consistent session"
+ operations:
+ - *find
+ - name: dropDatabase
+ object: *client0
+ arguments:
+ session: *session0
+ database: causal-consistency-dropDatabase-test
+ expectEvents:
+ - client: *client0
+ events:
+ - *findEvent
+ - commandStartedEvent:
+ commandName: dropDatabase
+ command:
+ dropDatabase: 1
+ $db: causal-consistency-dropDatabase-test
+ lsid: { $$sessionLsid: *session0 }
+ readConcern:
+ afterClusterTime: { $$exists: true }
+ level: { $$exists: false }
+
+ - description: "dropIndexes includes afterClusterTime in causally consistent session"
+ operations:
+ - *find
+ - name: dropIndexes
+ object: *collection0
+ arguments:
+ session: *session0
+ expectEvents:
+ - client: *client0
+ events:
+ - *findEvent
+ - commandStartedEvent:
+ commandName: dropIndexes
+ command:
+ dropIndexes: *collectionName
+ lsid: { $$sessionLsid: *session0 }
+ readConcern:
+ afterClusterTime: { $$exists: true }
+ level: { $$exists: false }
+
+ # Covers the same condition as Causal Consistency prose test #2, but for a write operation.
+ - description: "first write command in a causally consistent session does not include afterClusterTime"
+ operations:
+ - name: insertOne
+ object: *collection0
+ arguments:
+ session: *session0
+ document: { _id: 4 }
+ expectResult:
+ $$unsetOrMatches: { insertedId: { $$unsetOrMatches: 4 } }
+ expectEvents:
+ - client: *client0
+ events:
+ - commandStartedEvent:
+ commandName: insert
+ command:
+ insert: *collectionName
+ lsid: { $$sessionLsid: *session0 }
+ readConcern: { $$exists: false }
diff --git a/source/change-streams/change-streams.md b/source/change-streams/change-streams.md
new file mode 100644
index 0000000000..39e20dddd0
--- /dev/null
+++ b/source/change-streams/change-streams.md
@@ -0,0 +1,1141 @@
+# Change Streams
+
+- Status: Accepted
+- Minimum Server Version: 3.6
+
+______________________________________________________________________
+
+## Abstract
+
+As of version 3.6 of the MongoDB server a new `$changeStream` pipeline stage is supported in the aggregation framework.
+Specifying this stage first in an aggregation pipeline allows users to request that notifications are sent for all
+changes to a particular collection. This specification defines the means for creating change streams in drivers, as well
+as behavior during failure scenarios.
+
+## Specification
+
+### Definitions
+
+#### META
+
+The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
+"OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt).
+
+#### Terms
+
+##### Resumable Error
+
+An error is considered resumable if it meets any of the criteria listed below. For any criteria with wire version
+constraints, the driver MUST use the wire version of the connection used to do the initial `aggregate` or the `getMore`
+that resulted in the error. Drivers MUST NOT check the wire version of the server after the command has been executed
+when checking these constraints.
+
+- Any error encountered which is not a server error (e.g. a timeout error or network error)
+
+- A server error with code 43 (`CursorNotFound`)
+
+- For servers with wire version 9 or higher (server version 4.4 or higher), any server error with the
+ `ResumableChangeStreamError` error label.
+
+- For servers with wire version less than 9, a server error with one of the following codes:
+
+ | Error Name | Error Code |
+ | ------------------------------- | ---------- |
+ | HostUnreachable | 6 |
+ | HostNotFound | 7 |
+ | NetworkTimeout | 89 |
+ | ShutdownInProgress | 91 |
+ | PrimarySteppedDown | 189 |
+ | ExceededTimeLimit | 262 |
+ | SocketException | 9001 |
+ | NotWritablePrimary | 10107 |
+ | InterruptedAtShutdown | 11600 |
+ | InterruptedDueToReplStateChange | 11602 |
+ | NotPrimaryNoSecondaryOk | 13435 |
+ | NotPrimaryOrSecondary | 13436 |
+ | StaleShardVersion | 63 |
+ | StaleEpoch | 150 |
+ | StaleConfig | 13388 |
+ | RetryChangeStream | 234 |
+ | FailedToSatisfyReadPreference | 133 |
+
+An error on an aggregate command is not a resumable error. Only errors on a getMore command may be considered resumable
+errors.
+
+### Guidance
+
+For naming and deviation guidance, see the [CRUD specification](../crud/crud.md#naming).
+
+### Server Specification
+
+#### Response Format
+
+**NOTE:** The examples in this section are provided for illustrative purposes, and are subject to change without
+warning. Drivers that provide a static type to represent ChangeStreamDocument MAY include additional fields in their
+API.
+
+If an aggregate command with a `$changeStream` stage completes successfully, the response contains documents with the
+following structure:
+
+```typescript
+class ChangeStreamDocument {
+ /**
+ * The id functions as an opaque token for use when resuming an interrupted
+ * change stream.
+ */
+ _id: Document;
+
+ /**
+ * Describes the type of operation represented in this change notification.
+ *
+ * @note: Whether a change is reported as an event of the operation type
+ * `update` or `replace` is a server implementation detail.
+ *
+ * @note: The server will add new `operationType` values in the future and drivers
+ * MUST NOT err when they encounter a new `operationType`. Unknown `operationType`
+ * values may be represented by "unknown" or the literal string value.
+ */
+ operationType: "insert"
+ | "update"
+ | "replace"
+ | "delete"
+ | "invalidate"
+ | "drop"
+ | "dropDatabase"
+ | "rename"
+ | "createIndexes"
+ | "dropIndexes"
+ | "modify"
+ | "create"
+ | "shardCollection"
+ | "refineCollectionShardKey"
+ | "reshardCollection";
+
+ /**
+ * Contains two fields: "db" and "coll" containing the database and
+ * collection name in which the change happened.
+ *
+ * @note: Drivers MUST NOT err when extra fields are encountered in the `ns` document
+ * as the server may add new fields in the future such as `viewOn`.
+ */
+ ns: Document;
+
+ /**
+ * Only present for ops of type 'create'.
+ * Only present when the `showExpandedEvents` change stream option is enabled.
+ *
+ * The type of the newly created object.
+ *
+ * @since 8.1.0
+ */
+ nsType: Optional<"collection" | "timeseries" | "view">;
+
+ /**
+ * Only present for ops of type 'rename'.
+ *
+ * The namespace, in the same format as `ns`, that a collection has been renamed to.
+ */
+ to: Optional;
+
+ /**
+ * * Only present when the `showExpandedEvents` change stream option is enabled and for the following events:
+ * - 'rename'
+ * - 'create'
+ * - 'modify'
+ * - 'createIndexes'
+ * - 'dropIndexes'
+ * - 'shardCollection'
+ * - 'reshardCollection'
+ * - 'refineCollectionShardKey'
+ *
+ * A description of the operation.
+ *
+ * @since 6.0.0
+ */
+ operationDescription: Optional
+
+ /**
+ * Only present for ops of type 'insert', 'update', 'replace', and
+ * 'delete'.
+ *
+ * For unsharded collections this contains a single field, _id, with the
+ * value of the _id of the document updated. For sharded collections,
+ * this will contain all the components of the shard key in order,
+ * followed by the _id if the _id isn't part of the shard key.
+ */
+ documentKey: Optional;
+
+ /**
+ * Only present for ops of type 'update'.
+ */
+ updateDescription: Optional;
+
+ /**
+ * Always present for operations of type 'insert' and 'replace'. Also
+ * present for operations of type 'update' if the user has specified
+ * 'updateLookup' for the 'fullDocument' option when creating the change
+ * stream.
+ *
+ * For operations of type 'insert' and 'replace', this key will contain the
+ * document being inserted or the new version of the document that is
+ * replacing the existing document, respectively.
+ *
+ * For operations of type 'update', this key will contain a copy of the full
+ * version of the document from some point after the update occurred. If the
+ * document was deleted since the updated happened, it will be null.
+ *
+ * Contains the point-in-time post-image of the modified document if the
+ * post-image is available and either 'required' or 'whenAvailable' was
+ * specified for the 'fullDocument' option when creating the change stream.
+ * A post-image is always available for 'insert' and 'replace' events.
+ */
+ fullDocument: Document | null;
+
+ /**
+ * Contains the pre-image of the modified or deleted document if the
+ * pre-image is available for the change event and either 'required' or
+ * 'whenAvailable' was specified for the 'fullDocumentBeforeChange' option
+ * when creating the change stream. If 'whenAvailable' was specified but the
+ * pre-image is unavailable, this will be explicitly set to null.
+ */
+ fullDocumentBeforeChange: Document | null;
+
+ /**
+ * The wall time from the mongod that the change event originated from.
+ * Populated for server versions 6.0 and above.
+ */
+ wallTime: Optional;
+
+ /**
+ * The `ui` field from the oplog entry corresponding to the change event.
+ *
+ * Only present when the `showExpandedEvents` change stream option is enabled and for the following events:
+ * - 'insert'
+ * - 'update'
+ * - 'delete'
+ * - 'createIndexes'
+ * - 'dropIndexes'
+ * - 'modify'
+ * - 'drop'
+ * - 'create'
+ * - 'shardCollection'
+ * - 'reshardCollection'
+ * - 'refineCollectionShardKey'
+ *
+ * This field is a value of binary subtype 4 (UUID).
+ *
+ * @since 6.0.0
+ */
+ collectionUUID: Optional;
+
+ /**
+ * The cluster time at which the change occurred.
+ */
+ clusterTime: Timestamp;
+
+}
+
+class UpdateDescription {
+ /**
+ * A document containing key:value pairs of names of the fields that were
+ * changed (excluding the fields reported via `truncatedArrays`), and the new value for those fields.
+ *
+ * Despite array fields reported via `truncatedArrays` being excluded from this field,
+ * changes to fields of the elements of the array values may be reported via this field.
+ * Example:
+ * original field:
+ * "arrayField": ["foo", {"a": "bar"}, 1, 2, 3]
+ * updated field:
+ * "arrayField": ["foo", {"a": "bar", "b": 3}]
+ * a potential corresponding UpdateDescription:
+ * {
+ * "updatedFields": {
+ * "arrayField.1.b": 3
+ * },
+ * "removedFields": [],
+ * "truncatedArrays": [
+ * {
+ * "field": "arrayField",
+ * "newSize": 2
+ * }
+ * ]
+ * }
+ *
+ * Modifications to array elements are expressed via the dot notation (https://www.mongodb.com/docs/manual/core/document/#document-dot-notation).
+ * Example: an `update` which sets the element with index 0 in the array field named arrayField to 7 is reported as
+ * "updatedFields": {"arrayField.0": 7}
+ */
+ updatedFields: Document;
+
+ /**
+ * An array of field names that were removed from the document.
+ */
+ removedFields: Array;
+
+ /**
+ * Truncations of arrays may be reported using one of the following methods:
+ * either via this field or via the 'updatedFields' field. In the latter case the entire array is considered to be replaced.
+ *
+ * The structure of documents in this field is
+ * {
+ * "field": ,
+ * "newSize":
+ * }
+ * Example: an `update` which shrinks the array arrayField.0.nestedArrayField from size 8 to 5 may be reported as
+ * "truncatedArrays": [{"field": "arrayField.0.nestedArrayField", "newSize": 5}]
+ *
+ * @note The method used to report a truncation is a server implementation detail.
+ * @since 4.7.0
+ */
+ truncatedArrays: Array;
+
+ /**
+ * A document containing a map that associates an update path to an array containing the path components used in the update document. This data
+ * can be used in combination with the other fields in an `UpdateDescription` to determine the actual path in the document that was updated. This is
+ * necessary in cases where a key contains dot-separated strings (i.e., `{ "a.b": "c" }`) or a document contains a numeric literal string key
+ * (i.e., `{ "a": { "0": "a" } }`. Note that in this scenario, the numeric key can't be the top level key, because `{ "0": "a" }` is not ambiguous -
+ * update paths would simply be `'0'` which is unambiguous because BSON documents cannot have arrays at the top level.).
+ *
+ * Each entry in the document maps an update path to an array which contains the actual path used when the document was updated.
+ * For example, given a document with the following shape `{ "a": { "0": 0 } }` and an update of `{ $inc: { "a.0": 1 } }`, `disambiguatedPaths` would
+ * look like the following:
+ * {
+ * "a.0": ["a", "0"]
+ * }
+ *
+ * In each array, all elements will be returned as strings with the exception of array indices, which will be returned as 32 bit integers.
+ *
+ * Only present when the `showExpandedEvents` change stream option is enabled.
+ *
+ * @since 6.1.0
+ */
+ disambiguatedPaths: Optional
+}
+```
+
+The responses to a change stream aggregate or getMore have the following structures:
+
+```typescript
+/**
+ * Response to a successful aggregate.
+ */
+{
+ ok: 1,
+ cursor: {
+ ns: String,
+ id: Int64,
+ firstBatch: Array,
+ /**
+ * postBatchResumeToken is returned in MongoDB 4.0.7 and later.
+ */
+ postBatchResumeToken: Document
+ },
+ operationTime: Timestamp,
+ $clusterTime: Document,
+}
+
+/**
+ * Response to a successful getMore.
+ */
+{
+ ok: 1,
+ cursor: {
+ ns: String,
+ id: Int64,
+ nextBatch: Array
+ /**
+ * postBatchResumeToken is returned in MongoDB 4.0.7 and later.
+ */
+ postBatchResumeToken: Document
+ },
+ operationTime: Timestamp,
+ $clusterTime: Document,
+}
+```
+
+### Driver API
+
+```typescript
+interface ChangeStream extends Iterable {
+ /**
+ * The cached resume token
+ */
+ private resumeToken: Document;
+
+ /**
+ * The pipeline of stages to append to an initial ``$changeStream`` stage
+ */
+ private pipeline: Array;
+
+ /**
+ * The options provided to the initial ``$changeStream`` stage
+ */
+ private options: ChangeStreamOptions;
+
+ /**
+ * The read preference for the initial change stream aggregation, used
+ * for server selection during an automatic resume.
+ */
+ private readPreference: ReadPreference;
+}
+
+interface Collection {
+ /**
+ * @returns a change stream on a specific collection.
+ */
+ watch(pipeline: Document[], options: Optional): ChangeStream;
+}
+
+interface Database {
+ /**
+ * Allows a client to observe all changes in a database.
+ * Excludes system collections.
+ * @returns a change stream on all collections in a database
+ * @since 4.0
+ * @see https://www.mongodb.com/docs/manual/reference/system-collections/
+ */
+ watch(pipeline: Document[], options: Optional): ChangeStream;
+}
+
+interface MongoClient {
+ /**
+ * Allows a client to observe all changes in a cluster.
+ * Excludes system collections.
+ * Excludes the "config", "local", and "admin" databases.
+ * @since 4.0
+ * @returns a change stream on all collections in all databases in a cluster
+ * @see https://www.mongodb.com/docs/manual/reference/system-collections/
+ */
+ watch(pipeline: Document[], options: Optional): ChangeStream;
+}
+
+class ChangeStreamOptions {
+ /**
+ * Allowed values: 'default', 'updateLookup', 'whenAvailable', 'required'.
+ *
+ * The default is to not send a value, which is equivalent to 'default'. By
+ * default, the change notification for partial updates will include a delta
+ * describing the changes to the document.
+ *
+ * When set to 'updateLookup', the change notification for partial updates
+ * will include both a delta describing the changes to the document as well
+ * as a copy of the entire document that was changed from some time after
+ * the change occurred.
+ *
+ * When set to 'whenAvailable', configures the change stream to return the
+ * post-image of the modified document for replace and update change events
+ * if the post-image for this event is available.
+ *
+ * When set to 'required', the same behavior as 'whenAvailable' except that
+ * an error is raised if the post-image is not available.
+ *
+ * For forward compatibility, a driver MUST NOT raise an error when a user
+ * provides an unknown value. The driver relies on the server to validate
+ * this option.
+ *
+ * @note this is an option of the `$changeStream` pipeline stage.
+ */
+ fullDocument: Optional;
+
+ /**
+ * Allowed values: 'whenAvailable', 'required', 'off'.
+ *
+ * The default is to not send a value, which is equivalent to 'off'.
+ *
+ * When set to 'whenAvailable', configures the change stream to return the
+ * pre-image of the modified document for replace, update, and delete change
+ * events if it is available.
+ *
+ * When set to 'required', the same behavior as 'whenAvailable' except that
+ * an error is raised if the pre-image is not available.
+ *
+ * For forward compatibility, a driver MUST NOT raise an error when a user
+ * provides an unknown value. The driver relies on the server to validate
+ * this option.
+ *
+ * @note this is an option of the `$changeStream` pipeline stage.
+ */
+ fullDocumentBeforeChange: Optional;
+
+ /**
+ * Specifies the logical starting point for the new change stream.
+ *
+ * @note this is an option of the `$changeStream` pipeline stage.
+ */
+ resumeAfter: Optional;
+
+ /**
+ * The maximum amount of time for the server to wait on new documents to satisfy
+ * a change stream query.
+ *
+ * This is the same field described in FindOptions in the CRUD spec.
+ *
+ * @see https://github.com/mongodb/specifications/blob/master/source/crud/crud.md#read
+ * @note this option is an alias for `maxTimeMS`, used on `getMore` commands
+ * @note this option is not set on the `aggregate` command nor `$changeStream` pipeline stage
+ */
+ maxAwaitTimeMS: Optional;
+
+ /**
+ * The number of documents to return per batch.
+ *
+ * This option is sent only if the caller explicitly provides a value. The
+ * default is to not send a value.
+ *
+ * @see https://www.mongodb.com/docs/manual/reference/command/aggregate
+ * @note this is an aggregation command option
+ */
+ batchSize: Optional;
+
+ /**
+ * Specifies a collation.
+ *
+ * This option is sent only if the caller explicitly provides a value. The
+ * default is to not send a value.
+ *
+ * @see https://www.mongodb.com/docs/manual/reference/command/aggregate
+ * @note this is an aggregation command option
+ */
+ collation: Optional;
+
+ /**
+ * The change stream will only provide changes that occurred at or after the
+ * specified timestamp. Any command run against the server will return
+ * an operation time that can be used here.
+ *
+ * @since 4.0
+ * @see https://www.mongodb.com/docs/manual/reference/method/db.runCommand/
+ * @note this is an option of the `$changeStream` pipeline stage.
+ */
+ startAtOperationTime: Optional;
+
+ /**
+ * Similar to `resumeAfter`, this option takes a resume token and starts a
+ * new change stream returning the first notification after the token.
+ * This will allow users to watch collections that have been dropped and recreated
+ * or newly renamed collections without missing any notifications.
+ *
+ * The server will report an error if `startAfter` and `resumeAfter` are both specified.
+ *
+ * @since 4.1.1
+ * @see https://www.mongodb.com/docs/manual/changeStreams/#change-stream-start-after
+ * @note this is an option of the `$changeStream` pipeline stage.
+ */
+ startAfter: Optional;
+
+ /**
+ * Enables users to specify an arbitrary comment to help trace the operation through
+ * the database profiler, currentOp and logs. The default is to not send a value.
+ *
+ * The comment can be any valid BSON type for server versions 4.4 and above.
+ * Server versions prior to 4.4 only support string as comment,
+ * and providing a non-string type will result in a server-side error.
+ *
+ * If a comment is provided, drivers MUST attach this comment to all
+ * subsequent getMore commands run on the same cursor for server
+ * versions 4.4 and above. For server versions below 4.4 drivers MUST NOT
+ * attach a comment to getMore commands.
+ *
+ * @see https://www.mongodb.com/docs/manual/reference/command/aggregate
+ * @note this is an aggregation command option
+ */
+ comment: Optional
+
+ /**
+ * Enables the server to send the 'expanded' list of change stream events.
+ * The list of additional events included with this flag set are
+ * - createIndexes
+ * - dropIndexes
+ * - modify
+ * - create
+ * - shardCollection
+ * - reshardCollection
+ * - refineCollectionShardKey
+ *
+ * This flag is available in server versions greater than 6.0.0. `reshardCollection` and
+ * `refineCollectionShardKey` events are not available until server version 6.1.0.
+ *
+ * @note this is an option of the change stream pipeline stage
+ */
+ showExpandedEvents: Optional
+}
+```
+
+**NOTE:** The set of `ChangeStreamOptions` may grow over time.
+
+#### Helper Method
+
+The driver API consists of a `ChangeStream` type, as well as three helper methods. All helpers MUST return a
+`ChangeStream` instance. Implementers MUST document that helper methods are preferred to running a raw aggregation with
+a `$changeStream` stage, for the purpose of supporting resumability.
+
+The helper methods must construct an aggregation command with a REQUIRED initial `$changeStream` stage. A driver MUST
+NOT throw a custom exception if multiple `$changeStream` stages are present (e.g. if a user also passed `$changeStream`
+in the pipeline supplied to the helper), as the server will return an error.
+
+The helper methods MUST determine a read concern for the operation in accordance with the
+[Read and Write Concern specification](../read-write-concern/read-write-concern.md#via-code). The initial implementation
+of change streams on the server requires a 'majority' read concern or no read concern. Drivers MUST document this
+requirement. Drivers SHALL NOT throw an exception if any other read concern is specified, but instead should depend on
+the server to return an error.
+
+The stage has the following shape:
+
+```typescript
+{ $changeStream: ChangeStreamOptions }
+```
+
+The first parameter of the helpers specifies an array of aggregation pipeline stages which MUST be appended to the
+initial stage. Drivers MUST support an empty pipeline. Languages which support default parameters MAY specify an empty
+array as the default value for this parameter. Drivers SHOULD otherwise make specification of a pipeline as similar as
+possible to the [aggregate](../crud/crud.md#read) CRUD method.
+
+Additionally, implementers MAY provide a form of these methods which require no parameters, assuming no options and no
+additional stages beyond the initial `$changeStream` stage:
+
+```python
+for change in db.collection.watch():
+ print(change)
+```
+
+Presently change streams support only a subset of available aggregation stages:
+
+- `$match`
+- `$project`
+- `$addFields`
+- `$replaceRoot`
+- `$redact`
+
+A driver MUST NOT throw an exception if any unsupported stage is provided, but instead depend on the server to return an
+error.
+
+A driver MUST NOT throw an exception if a user adds, removes, or modifies fields using `$project`. The server will
+produce an error if `_id` is projected out, but a user should otherwise be able to modify the shape of the change stream
+event as desired. This may require the result to be deserialized to a `BsonDocument` or custom-defined type rather than
+a `ChangeStreamDocument`. It is the responsibility of the user to ensure that the deserialized type is compatible with
+the specified `$project` stage.
+
+The aggregate helper methods MUST have no new logic related to the `$changeStream` stage. Drivers MUST be capable of
+handling [TAILABLE_AWAIT](../crud/crud.md#read) cursors from the aggregate command in the same way they handle such
+cursors from find.
+
+##### `Collection.watch` helper
+
+Returns a `ChangeStream` on a specific collection
+
+Command syntax:
+
+```typescript
+{
+ aggregate: 'collectionName'
+ pipeline: [{$changeStream: {...}}, ...],
+ ...
+}
+```
+
+##### `Database.watch` helper
+
+- Since: 4.0
+
+Returns a `ChangeStream` on all collections in a database.
+
+Command syntax:
+
+```typescript
+{
+ aggregate: 1
+ pipeline: [{$changeStream: {...}}, ...],
+ ...
+}
+```
+
+Drivers MUST use the `ns` returned in the `aggregate` command to set the `collection` option in subsequent `getMore`
+commands.
+
+##### `MongoClient.watch` helper
+
+- Since: 4.0
+
+Returns a `ChangeStream` on all collections in all databases in a cluster
+
+Command syntax:
+
+```typescript
+{
+ aggregate: 1
+ pipeline: [{$changeStream: {allChangesForCluster: true, ...}}, ...],
+ ...
+}
+```
+
+The helper MUST run the command against the `admin` database
+
+Drivers MUST use the `ns` returned in the `aggregate` command to set the `collection` option in subsequent `getMore`
+commands.
+
+#### ChangeStream
+
+A `ChangeStream` is an abstraction of a [TAILABLE_AWAIT](../crud/crud.md#read) cursor, with support for resumability.
+Implementers MAY choose to implement a `ChangeStream` as an extension of an existing tailable cursor implementation. If
+the `ChangeStream` is implemented as a type which owns a tailable cursor, then the implementer MUST provide a manner of
+closing the change stream, as well as satisfy the requirements of extending `Iterable`. If your language has
+an idiomatic way of disposing of resources you MAY choose to implement that in addition to, or instead of, an explicit
+close method.
+
+A change stream MUST track the last resume token, per
+[Updating the Cached Resume Token](#updating-the-cached-resume-token).
+
+Drivers MUST raise an error on the first document received without a resume token (e.g. the user has removed `_id` with
+a pipeline stage), and close the change stream. The error message SHOULD resemble "Cannot provide resume functionality
+when the resume token is missing".
+
+A change stream MUST attempt to resume a single time if it encounters any resumable error per
+[Resumable Error](#resumable-error). A change stream MUST NOT attempt to resume on any other type of error.
+
+In addition to tracking a resume token, change streams MUST also track the read preference specified when the change
+stream was created. In the event of a resumable error, a change stream MUST perform server selection with the original
+read preference using the
+[rules for server selection](../server-selection/server-selection.md#rules-for-server-selection) before attempting to
+resume.
+
+##### Single Server Topologies
+
+Presently, change streams cannot be initiated on single server topologies as they do not have an oplog. Drivers MUST NOT
+throw an exception in this scenario, but instead rely on an error returned from the server. This allows for the server
+to seamlessly introduce support for this in the future, without need to make changes in driver code.
+
+##### startAtOperationTime
+
+- Since: 4.0
+
+`startAtOperationTime` specifies that a change stream will only return changes that occurred at or after the specified
+`Timestamp`.
+
+The server expects `startAtOperationTime` as a BSON Timestamp. Drivers MUST allow users to specify a
+`startAtOperationTime` option in the `watch` helpers. They MUST allow users to specify this value as a raw `Timestamp`.
+
+`startAtOperationTime`, `resumeAfter`, and `startAfter` are all mutually exclusive; if any two are set, the server will
+return an error. Drivers MUST NOT throw a custom error, and MUST defer to the server error.
+
+The `ChangeStream` MUST save the `operationTime` from the initial `aggregate` response when the following criteria are
+met:
+
+- None of `startAtOperationTime`, `resumeAfter`, `startAfter` were specified in the `ChangeStreamOptions`.
+- The max wire version is >= `7`.
+- The initial `aggregate` response had no results.
+- The initial `aggregate` response did not include a `postBatchResumeToken`.
+
+##### resumeAfter
+
+`resumeAfter` is used to resume a `ChangeStream` that has been stopped to ensure that only changes starting with the log
+entry immediately *after* the provided token will be returned. If the resume token specified does not exist, the server
+will return an error.
+
+##### Resume Process
+
+Once a `ChangeStream` has encountered a resumable error, it MUST attempt to resume one time. The process for resuming
+MUST follow these steps:
+
+- Perform server selection.
+- Connect to selected server.
+- If there is a cached `resumeToken`:
+ - If the `ChangeStream` was started with `startAfter` and has yet to return a result document:
+ - The driver MUST set `startAfter` to the cached `resumeToken`.
+ - The driver MUST NOT set `resumeAfter`.
+ - The driver MUST NOT set `startAtOperationTime`. If `startAtOperationTime` was in the original aggregation command,
+ the driver MUST remove it.
+ - Else:
+ - The driver MUST set `resumeAfter` to the cached `resumeToken`.
+ - The driver MUST NOT set `startAfter`. If `startAfter` was in the original aggregation command, the driver MUST
+ remove it.
+ - The driver MUST NOT set `startAtOperationTime`. If `startAtOperationTime` was in the original aggregation command,
+ the driver MUST remove it.
+- Else if there is no cached `resumeToken` and the `ChangeStream` has a saved operation time (either from an originally
+ specified `startAtOperationTime` or saved from the original aggregation) and the max wire version is >= `7`:
+ - The driver MUST NOT set `resumeAfter`.
+ - The driver MUST NOT set `startAfter`.
+ - The driver MUST set `startAtOperationTime` to the value of the originally used `startAtOperationTime` or the one
+ saved from the original aggregation.
+- Else:
+ - The driver MUST NOT set `resumeAfter`, `startAfter`, or `startAtOperationTime`.
+ - The driver MUST use the original aggregation command to resume.
+
+When `resumeAfter` is specified the `ChangeStream` will return notifications starting with the oplog entry immediately
+*after* the provided token.
+
+If the server supports sessions, the resume attempt MUST use the same session as the previous attempt's command.
+
+A driver MUST only attempt to resume once from a resumable error. However, if the `aggregate` for that resume succeeds,
+a driver MUST ensure that following resume attempts can succeed, even in the absence of any changes received by the
+cursor between resume attempts. For example:
+
+1. `aggregate` (succeeds)
+2. `getMore` (fails with resumable error)
+3. `aggregate` (succeeds)
+4. `getMore` (fails with resumable error)
+5. `aggregate` (succeeds)
+6. `getMore` (succeeds)
+7. change stream document received
+
+A driver SHOULD attempt to kill the cursor on the server on which the cursor is opened during the resume process, and
+MUST NOT attempt to kill the cursor on any other server. Any exceptions or errors that occur during the process of
+killing the cursor should be suppressed, including both errors returned by the `killCursor` command and exceptions
+thrown by opening, writing to, or reading from the socket.
+
+##### Exposing All Resume Tokens
+
+- Since: 4.0.7
+
+Users can inspect the \_id on each `ChangeDocument` to use as a resume token. But since MongoDB 4.0.7, aggregate and
+getMore responses also include a `postBatchResumeToken`. Drivers use one or the other when automatically resuming, as
+described in [Resume Process](#resume-process).
+
+Drivers MUST expose a mechanism to retrieve the same resume token that would be used to automatically resume. It MUST be
+possible to use this mechanism after iterating every document. It MUST be possible for users to use this mechanism
+periodically even when no documents are getting returned (i.e. `getMore` has returned empty batches). Drivers have two
+options to implement this.
+
+###### Option 1: ChangeStream::getResumeToken()
+
+```typescript
+interface ChangeStream extends Iterable {
+ /**
+ * Returns the cached resume token that will be used to resume
+ * after the most recently returned change.
+ */
+ public getResumeToken() Optional;
+}
+```
+
+This MUST be implemented in synchronous drivers. This MAY be implemented in asynchronous drivers.
+
+###### Option 2: Event Emitted for Resume Token
+
+Allow users to set a callback to listen for new resume tokens. The exact interface is up to the driver, but it MUST meet
+the following criteria:
+
+- The callback is set in the same manner as a callback used for receiving change documents.
+- The callback accepts a resume token as an argument.
+- The callback (or event) MAY include an optional ChangeDocument, which is unset when called with resume tokens sourced
+ from `postBatchResumeToken`.
+
+A possible interface for this callback MAY look like:
+
+```typescript
+interface ChangeStream extends Iterable {
+ /**
+ * Returns a resume token that should be used to resume after the most
+ * recently returned change.
+ */
+ public onResumeTokenChanged(ResumeTokenCallback:(Document resumeToken) => void);
+}
+```
+
+This MUST NOT be implemented in synchronous drivers. This MAY be implemented in asynchronous drivers.
+
+###### Updating the Cached Resume Token
+
+The following rules describe how to update the cached `resumeToken`:
+
+- When the `ChangeStream` is started:
+ - If `startAfter` is set, cache it.
+ - Else if `resumeAfter` is set, cache it.
+ - Else, `resumeToken` remains unset.
+- When `aggregate` or `getMore` returns:
+ - If an empty batch was returned and a `postBatchResumeToken` was included, cache it.
+- When returning a document to the user:
+ - If it's the last document in the batch and a `postBatchResumeToken` is included, cache it.
+ - Else, cache the `_id` of the document.
+
+###### Not Blocking on Iteration
+
+Synchronous drivers MUST provide a way to iterate a change stream without blocking until a change document is returned.
+This MUST give the user an opportunity to get the most up-to-date resume token, even when the change stream continues to
+receive empty batches in getMore responses. This allows users to call `ChangeStream::getResumeToken()` after iterating
+every document and periodically when no documents are getting returned.
+
+Although the implementation of tailable awaitData cursors is not specified, this MAY be implemented with a `tryNext`
+method on the change stream cursor.
+
+All drivers MUST document how users can iterate a change stream and receive *all* resume token updates.
+[Why do we allow access to the resume token to users](#why-do-we-allow-access-to-the-resume-token-to-users) shows an
+example. The documentation MUST state that users intending to store the resume token should use this method to get the
+most up to date resume token.
+
+##### Timeouts
+
+Drivers MUST apply timeouts to change stream establishment, iteration, and resume attempts per
+[Client Side Operations Timeout: Change Streams](../client-side-operations-timeout/client-side-operations-timeout.md#change-streams).
+
+##### Notes and Restrictions
+
+**1. `fullDocument: updateLookup` can result in change documents larger than 16 MiB**
+
+There is a risk that if there is a large change to a large document, the full document and delta might result in a
+document larger than the 16 MiB limitation on BSON documents. If that happens the cursor will be closed, and a server
+error will be returned.
+
+**2. Users can remove the resume token with aggregation stages**
+
+It is possible for a user to specify the following stage:
+
+```javascript
+{ $project: { _id: 0 } }
+```
+
+Similar removal of the resume token is possible with the `$redact` and `$replaceRoot` stages. While this is not
+technically illegal, it makes it impossible for drivers to support resumability. Users may explicitly opt out of
+resumability by issuing a raw aggregation with a `$changeStream` stage.
+
+## Rationale
+
+### Why Helper Methods?
+
+Change streams are a first class concept similar to CRUD or aggregation; the fact that they are initiated via an
+aggregation pipeline stage is merely an implementation detail. By requiring drivers to support top-level helper methods
+for this feature we not only signal this intent, but also solve a number of other potential problems:
+
+Disambiguation of the result type of this special-case aggregation pipeline (`ChangeStream`), and an ability to control
+the behaviors of the resultant cursor
+
+More accurate support for the concept of a maximum time the user is willing to wait for subsequent queries to complete
+on the resultant cursor (`maxAwaitTimeMs`)
+
+Finer control over the options pertaining specifically to this type of operation, without polluting the already
+well-defined `AggregateOptions`
+
+Flexibility for future potentially breaking changes for this feature on the server
+
+### Why are ChangeStreams required to retry once on a resumable error?
+
+User experience is of the utmost importance. Errors not originating from the server are generally network errors, and
+network errors can be transient. Attempting to resume an interrupted change stream after the initial error allows for a
+seamless experience for the user, while subsequent network errors are likely to be an outage which can then be exposed
+to the user with greater confidence.
+
+### Why do we allow access to the resume token to users
+
+Imagine a scenario in which a user wants to process each change to a collection **at least once**, but the application
+crashes during processing. In order to overcome this failure, a user might use the following approach:
+
+```python
+localChange = getChangeFromLocalStorage()
+resumeToken = getResumeTokenFromLocalStorage()
+
+if localChange:
+ processChange(localChange)
+
+try:
+ change_stream = db.collection.watch([...], resumeAfter=resumeToken)
+ while True:
+ change = change_stream.try_next()
+ persistResumeTokenToLocalStorage(change_stream.get_resume_token())
+ if change:
+ persistChangeToLocalStorage(change)
+ processChange(change)
+except Exception:
+ log.error("...")
+```
+
+In this case the current change is always persisted locally, including the resume token, such that on restart the
+application can still process the change while ensuring that the change stream continues from the right logical time in
+the oplog. It is the application's responsibility to ensure that `processChange` is idempotent, this design merely makes
+a reasonable effort to process each change **at least** once.
+
+### Why is there no example of the desired user experience?
+
+The specification used to include this overspecified example of the "desired user experience":
+
+```python
+try:
+ for change in db.collection.watch(...):
+ print(change)
+except Exception:
+ # We know for sure it's unrecoverable:
+ log.error("...")
+```
+
+It was decided to remove this example from the specification for the following reasons:
+
+- Tailable + awaitData cursors behave differently in existing supported drivers.
+- There are considerations to be made for languages that do not permit interruptible I/O (such as Java), where a change
+ stream which blocks forever in a separate thread would necessitate killing the thread.
+- There is something to be said for an API that allows cooperation by default. The model in which a call to next only
+ blocks until any response is returned (even an empty batch), allows for interruption and cooperation (e.g.
+ interaction with other event loops).
+
+### Why is an allow list of error codes preferable to a deny list?
+
+Change streams originally used a deny list of error codes to determine which errors were not resumable. However, this
+allowed for the possibility of infinite resume loops if an error was not correctly deny listed. Due to the fact that all
+errors aside from transient issues such as failovers are not resumable, the resume behavior was changed to use an allow
+list. Part of this change was to introduce the `ResumableChangeStreamError` label so the server can add new error codes
+to the allow list without requiring changes to drivers.
+
+### Why is `CursorNotFound` special-cased when determining resumability?
+
+With the exception of `CursorNotFound`, a server error on version 4.4 or higher is considered resumable if and only if
+it contains the `ResumableChangeStreamError` label. However, this label is only added by the server if the cursor being
+created or iterated is a change stream. `CursorNotFound` is returned when a `getMore` is done with a cursor ID that the
+server isn't aware of and therefore can't determine if the cursor is a change stream. Marking all `CursorNotFound`
+errors resumable in the server regardless of cursor type could be confusing as a user could see the
+`ResumableChangeStreamError` label when iterating a non-change stream cursor. To workaround this, drivers always treat
+this error as resumable despite it not having the proper error label.
+
+### Why do we need to send a default `startAtOperationTime` when resuming a `ChangeStream`?
+
+`startAtOperationTime` allows a user to create a resumable change stream even when a result (and corresponding
+resumeToken) is not available until a later point in time.
+
+For example:
+
+- A client creates a `ChangeStream`, and calls `watch`
+- The `ChangeStream` sends out the initial `aggregate` call, and receives a response with no initial values. Because
+ there are no initial values, there is no latest resumeToken.
+- The client's network is partitioned from the server, causing the client's `getMore` to time out
+- Changes occur on the server.
+- The network is unpartitioned
+- The client attempts to resume the `ChangeStream`
+
+In the above example, not sending `startAtOperationTime` will result in the change stream missing the changes that
+occurred while the server and client are partitioned. By sending `startAtOperationTime`, the server will know to include
+changes from that previous point in time.
+
+### Why do we need to expose the postBatchResumeToken?
+
+Resume tokens refer to an oplog entry. The resume token from the `_id` of a document corresponds the oplog entry of the
+change. The `postBatchResumeToken` represents the oplog entry the change stream has scanned up to on the server (not
+necessarily a matching change). This can be a much more recent oplog entry, and should be used to resume when possible.
+
+Attempting to resume with an old resume token may degrade server performance since the server needs to scan through more
+oplog entries. Worse, if the resume token is older than the last oplog entry stored on the server, then resuming is
+impossible.
+
+Imagine the change stream matches a very small percentage of events. On a `getMore` the server scans the oplog for the
+duration of `maxAwaitTimeMS` but finds no matching entries and returns an empty response (still containing a
+`postBatchResumeToken`). There may be a long sequence of empty responses. Then due to a network error, the change stream
+tries resuming. If we tried resuming with the most recent `_id`, this throws out the oplog scanning the server had done
+for the long sequence of getMores with empty responses. But resuming with the last `postBatchResumeToken` skips the
+unnecessary scanning of unmatched oplog entries.
+
+## Test Plan
+
+See [tests/README.md](tests/README.md)
+
+## Backwards Compatibility
+
+There should be no backwards compatibility concerns.
+
+## Reference Implementations
+
+- NODE (NODE-1055)
+- PYTHON (PYTHON-1338)
+- RUBY (RUBY-1228)
+
+## Changelog
+
+- 2026-04-28: Remove test for nsType when creating timeseries
+
+- 2026-03-18: Revert expanded field visibility change.
+
+- 2025-09-08: Clarify resume behavior.
+
+- 2025-03-31: Update for expanded field visibility in server 8.2+
+
+- 2025-02-24: Make `nsType` `Optional` to match other optional fields in the change stream spec.
+
+- 2025-01-29: Add `nsType` to `ChangeStreamDocument`.
+
+- 2024-02-09: Migrated from reStructuredText to Markdown.
+
+- 2023-08-11: Update server versions for `$changeStreamSplitLargeEvent` test.
+
+- 2023-05-22: Add spec test for `$changeStreamSplitLargeEvent`.
+
+- 2022-10-20: Reformat changelog.
+
+- 2022-10-05: Remove spec front matter.
+
+- 2022-08-22: Add `clusterTime` to `ChangeStreamDocument`.
+
+- 2022-08-17: Support `disambiguatedPaths` in `UpdateDescription`.
+
+- 2022-05-19: Support new change stream events with `showExpandedEvents`.
+
+- 2022-05-17: Add `wallTime` to `ChangeStreamDocument`.
+
+- 2022-04-13: Support returning point-in-time pre and post-images with `fullDocumentBeforeChange` and `fullDocument`.
+
+- 2022-03-25: Do not error when parsing change stream event documents.
+
+- 2022-02-28: Add `to` to `ChangeStreamDocument`.
+
+- 2022-02-10: Specify that `getMore` command must explicitly send inherited `comment`.
+
+- 2022-02-01: Add `comment` to `ChangeStreamOptions`.
+
+- 2022-01-19: Require that timeouts be applied per the client-side operations timeout specification.
+
+- 2021-09-01: Clarify that server selection during resumption should respect normal server selection rules.
+
+- 2021-04-29: Add `load-balanced` to test topology requirements.
+
+- 2021-04-23: Update to use modern terminology.
+
+- 2021-02-08: Add the `UpdateDescription.truncatedArrays` field.
+
+- 2020-02-10: Change error handling approach to use an allow list.
+
+- 2019-07-15: Clarify resume process for change streams started with the `startAfter` option.
+
+- 2019-07-09: Change `fullDocument` to be an optional string.
+
+- 2019-07-02: Fix server version for `startAfter`.
+
+- 2019-07-01: Clarify that close may be implemented with more idiomatic patterns instead of a method.
+
+- 2019-06-20: Fix server version for addition of `postBatchResumeToken`.
+
+- 2019-04-12: Clarify caching process for resume token.
+
+- 2019-04-03: Update the lowest server version that supports `postBatchResumeToken`.
+
+- 2019-01-10: Clarify error handling for killing the cursor.
+
+- 2018-11-06: Add handling of `postBatchResumeToken`.
+
+- 2018-12-14: Add `startAfter` to change stream options.
+
+- 2018-09-09: Add `dropDatabase` to change stream `operationType`.
+
+- 2018-07-30: Remove redundant error message checks for resumable errors.
+
+- 2018-07-27: Add drop to change stream `operationType`.
+
+- 2018-06-14: Clarify how to calculate `startAtOperationTime`.
+
+- 2018-05-24: Change `startAtClusterTime` to `startAtOperationTime`.
+
+- 2018-04-18: Add helpers for Database and MongoClient, and add `startAtClusterTime` option.
+
+- 2018-04-17: Clarify that the initial aggregate should not be retried.
+
+- 2017-12-13: Default read concern is also accepted, not just "majority".
+
+- 2017-11-06: Defer to Read and Write concern spec for determining a read concern for the helper method.
+
+- 2017-09-26: Clarify that change stream options may be added later.
+
+- 2017-09-21: Clarify that we need to close the cursor on missing token.
+
+- 2017-09-06: Remove `desired user experience` example.
+
+- 2017-08-22: Clarify killing cursors during resume process.
+
+- 2017-08-16: Fix formatting of resume process.
+
+- 2017-08-16: Add clarification regarding Resumable errors.
+
+- 2017-08-07: Fix typo in command format.
+
+- 2017-08-03: Initial commit.
diff --git a/source/change-streams/change-streams.rst b/source/change-streams/change-streams.rst
index 259ae13164..92a1bd4163 100644
--- a/source/change-streams/change-streams.rst
+++ b/source/change-streams/change-streams.rst
@@ -1,606 +1,3 @@
-==============
-Change Streams
-==============
-
-:Title: Change Streams
-:Author: Matt Broadstone
-:Advisory Group: David Golden, A. Jesse Jiryu Davis
-:Approvers: Jeff Yemin, A. Jesse Jiryu Davis, Bernie Hackett, David Golden, Eliot
-:Status: Accepted
-:Type: Standards
-:Minimum Server Version: 3.6
-:Last Modified: July 30, 2018
-:Version: 1.5.1
-
-.. contents::
-
---------
-
-Abstract
-========
-
-As of version 3.6 of the MongoDB server a new ``$changeStream`` pipeline stage is supported in the aggregation framework. Specifying this stage first in an aggregation pipeline allows users to request that notifications are sent for all changes to a particular collection. This specification defines the means for creating change streams in drivers, as well as behavior during failure scenarios.
-
-Specification
-=============
-
------------
-Definitions
------------
-
-META
-----
-
-The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”,
-“SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be
-interpreted as described in `RFC 2119 `_.
-
-Terms
------
-
-Resumable Error
-^^^^^^^^^^^^^^^
-
-An error is considered resumable if it meets any of the following criteria:
-
-- any error encountered which is not a server error (e.g. a timeout error or
- network error)
-
-- *any* server error response from a getMore command excluding those
- containing the following error codes
-
- .. list-table::
- :header-rows: 1
-
- * - Error Name
- - Error Code
- * - Interrupted
- - 11601
- * - CappedPositionLost
- - 136
- * - CursorKilled
- - 237
-
-An error on an aggregate command is not a resumable error. Only errors on a
-getMore command may be considered resumable errors.
-
-The criteria for resumable errors is similar to the discussion in the SDAM
-spec's section on `Error Handling`_, but includes additional error codes. See
-`What do the additional error codes mean?`_ for the reasoning behind these
-additional errors.
-
-.. _Error Handling: ../server-discovery-and-monitoring/server-discovery-and-monitoring.rst#error-handling
-
---------
-Guidance
---------
-
-For naming and deviation guidance, see the `CRUD specification `_.
-
---------------------
-Server Specification
---------------------
-
-Response Format
----------------
-
-If the aggregation with ``$changeStream`` specified completes successfully, the resultant stream will deliver documents in the following format:
-
-.. code:: typescript
-
- {
- /**
- * The id functions as an opaque token for use when resuming an interrupted
- * change stream.
- */
- _id: Document;
-
- /**
- * Describes the type of operation represented in this change notification.
- */
- operationType: "insert" | "update" | "replace" | "delete" | "invalidate" | "drop" | "dropDatabase";
-
- /**
- * Contains two fields: “db” and “coll” containing the database and
- * collection name in which the change happened.
- */
- ns: Document;
-
- /**
- * Only present for ops of type ‘insert’, ‘update’, ‘replace’, and
- * ‘delete’.
- *
- * For unsharded collections this contains a single field, _id, with the
- * value of the _id of the document updated. For sharded collections,
- * this will contain all the components of the shard key in order,
- * followed by the _id if the _id isn’t part of the shard key.
- */
- documentKey: Optional;
-
- /**
- * Only present for ops of type ‘update’.
- *
- * Contains a description of updated and removed fields in this
- * operation.
- */
- updateDescription: Optional;
-
- /**
- * Always present for operations of type ‘insert’ and ‘replace’. Also
- * present for operations of type ‘update’ if the user has specified ‘updateLookup’
- * in the ‘fullDocument’ arguments to the ‘$changeStream’ stage.
- *
- * For operations of type ‘insert’ and ‘replace’, this key will contain the
- * document being inserted, or the new version of the document that is replacing
- * the existing document, respectively.
- *
- * For operations of type ‘update’, this key will contain a copy of the full
- * version of the document from some point after the update occurred. If the
- * document was deleted since the updated happened, it will be null.
- */
- fullDocument: Document | null;
-
- }
-
- class UpdateDescription {
- /**
- * A document containing key:value pairs of names of the fields that were
- * changed, and the new value for those fields.
- */
- updatedFields: Document;
-
- /**
- * An array of field names that were removed from the document.
- */
- removedFields: Array;
- }
-
-**NOTE:** The above format is provided for illustrative purposes, and is subject to change without warning.
-
-----------
-Driver API
-----------
-
-.. code:: typescript
-
- interface ChangeStream extends Iterable {
- /**
- * The resume token (_id) of the document the iterator last returned
- */
- private resumeToken: Document;
-
- /**
- * The pipeline of stages to append to an initial ``$changeStream`` stage
- */
- private pipeline: Array;
-
- /**
- * The options provided to the initial ``$changeStream`` stage
- */
- private options: ChangeStreamOptions;
-
- /**
- * The read preference for the initial change stream aggregation, used
- * for server selection during an automatic resume.
- */
- private readPreference: ReadPreference;
- }
-
- interface Collection {
- /**
- * @returns a change stream on a specific collection.
- */
- watch(pipeline: Document[], options: Optional): ChangeStream;
- }
-
- interface Database {
- /**
- * Allows a client to observe all changes in a database.
- * Excludes system collections.
- * @returns a change stream on all collections in a database
- * @since 4.0
- * @see https://docs.mongodb.com/manual/reference/system-collections/
- */
- watch(pipeline: Document[], options: Optional): ChangeStream;
- }
-
- interface MongoClient {
- /**
- * Allows a client to observe all changes in a cluster.
- * Excludes system collections.
- * Excludes the "config", "local", and "admin" databases.
- * @since 4.0
- * @returns a change stream on all collections in all databases in a cluster
- * @see https://docs.mongodb.com/manual/reference/system-collections/
- */
- watch(pipeline: Document[], options: Optional): ChangeStream;
- }
-
- class ChangeStreamOptions {
- /**
- * Allowed values: ‘default’, ‘updateLookup’. Defaults to ‘default’. When set to
- * ‘updateLookup’, the change notification for partial updates will include both
- * a delta describing the changes to the document, as well as a copy of the entire
- * document that was changed from some time after the change occurred. For forward
- * compatibility, a driver MUST NOT raise an error when a user provides an unknown
- * value. The driver relies on the server to validate this option.
- *
- * @note this is an option of the `$changeStream` pipeline stage.
- */
- fullDocument: string = ‘default’;
-
- /**
- * Specifies the logical starting point for the new change stream.
- *
- * @note this is an option of the `$changeStream` pipeline stage.
- */
- resumeAfter: Optional;
-
- /**
- * The maximum amount of time for the server to wait on new documents to satisfy
- * a change stream query.
- *
- * This is the same field described in FindOptions in the CRUD spec.
- * @see https://github.com/mongodb/specifications/blob/master/source/crud/crud.rst#read
- * @note this option is an alias for `maxTimeMS`, used on `getMore` commands
- * @note this is an aggregation command option
- */
- maxAwaitTimeMS: Optional;
-
- /**
- * The number of documents to return per batch.
- *
- * This option is sent only if the caller explicitly provides a value. The
- * default is to not send a value.
- *
- * @see https://docs.mongodb.com/manual/reference/command/aggregate
- * @note this is an aggregation command option
- */
- batchSize: Optional;
-
- /**
- * Specifies a collation.
- *
- * This option is sent only if the caller explicitly provides a value. The
- * default is to not send a value.
- *
- * @see https://docs.mongodb.com/manual/reference/command/aggregate
- * @note this is an aggregation command option
- */
- collation: Optional;
-
- /**
- * The change stream will only provide changes that occurred at or after the
- * specified timestamp. Any command run against the server will return
- * an operation time that can be used here.
- * @since 4.0
- * @see https://docs.mongodb.com/manual/reference/method/db.runCommand/
- * @note this is an option of the `$changeStream` pipeline stage.
- */
- startAtOperationTime: Optional;
- }
-
-**NOTE:** The set of ``ChangeStreamOptions`` may grow over time.
-
-Helper Method
--------------
-
-The driver API consists of a ``ChangeStream`` type, as well as three helper methods. All helpers MUST return a ``ChangeStream`` instance. Implementers MUST document that helper methods are preferred to running a raw aggregation with a ``$changeStream`` stage, for the purpose of supporting resumability.
-
-The helper methods must construct an aggregation command with a REQUIRED initial ``$changeStream`` stage. A driver MUST NOT throw a custom exception if multiple ``$changeStream`` stages are present (e.g. if a user also passed ``$changeStream`` in the pipeline supplied to the helper), as the server will return an error.
-
-The helper methods MUST determine a read concern for the operation in accordance with the `Read and Write Concern specification `_. The initial implementation of change streams on the server requires a “majority” read concern or no read concern. Drivers MUST document this requirement. Drivers SHALL NOT throw an exception if any other read concern is specified, but instead should depend on the server to return an error.
-
-The stage has the following shape:
-
-.. code:: typescript
-
- { $changeStream: ChangeStreamOptions }
-
-The first parameter of the helpers specifies an array of aggregation pipeline stages which MUST be appended to the initial stage. Drivers MUST support an empty pipeline. Languages which support default parameters MAY specify an empty array as the default value for this parameter. Drivers SHOULD otherwise make specification of a pipeline as similar as possible to the `aggregate `_ CRUD method.
-
-Additionally, implementors MAY provide a form of these methods which require no parameters, assuming no options and no additional stages beyond the initial ``$changeStream`` stage:
-
-.. code:: python
-
- for change in db.collection.watch():
- print(change)
-
-Presently change streams support only a subset of available aggregation stages:
-
-- ``$match``
-- ``$project``
-- ``$addFields``
-- ``$replaceRoot``
-- ``$redact``
-
-A driver MUST NOT throw an exception if any unsupported stage is provided, but instead depend on the server to return an error.
-
-The aggregate helper methods MUST have no new logic related to the ``$changeStream`` stage. Drivers MUST be capable of handling `TAILABLE_AWAIT `_ cursors from the aggregate command in the same way they handle such cursors from find.
-
-``Collection.watch`` helper
-^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Returns a ``ChangeStream`` on a specific collection
-
-Command syntax:
-
-.. code:: typescript
-
- {
- aggregate: 'collectionName'
- pipeline: [{$changeStream: {...}}, ...],
- ...
- }
-
-``Database.watch`` helper
-^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:since: 4.0
-
-Returns a ``ChangeStream`` on all collections in a database.
-
-Command syntax:
-
-.. code:: typescript
-
- {
- aggregate: 1
- pipeline: [{$changeStream: {...}}, ...],
- ...
- }
-
-Drivers MUST use the ``ns`` returned in the ``aggregate`` command to set the ``collection`` option in subsequent ``getMore`` commands.
-
-``MongoClient.watch`` helper
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-:since: 4.0
-
-Returns a ``ChangeStream`` on all collections in all databases in a cluster
-
-Command syntax:
-
-.. code:: typescript
-
- {
- aggregate: 1
- pipeline: [{$changeStream: {allChangesForCluster: true, ...}}, ...],
- ...
- }
-
-The helper MUST run the command against the `admin` database
-
-Drivers MUST use the ``ns`` returned in the ``aggregate`` command to set the ``collection`` option in subsequent ``getMore`` commands.
-
-ChangeStream
-------------
-
-A ``ChangeStream`` is an abstraction of a `TAILABLE_AWAIT `_ cursor, with support for resumability. Implementors MAY choose to implement a ``ChangeStream`` as an extension of an existing tailable cursor implementation. If the ``ChangeStream`` is implemented as a type which owns a tailable cursor, then the implementor MUST provide a method to close the change stream, as well as satisfy the requirements of extending ``Iterable``.
-
-A change stream MUST track the last resume token returned by the iterator to the user, caching it locally for use in future attempts to resume. A driver MUST raise an error on the first response received without a resume token (e.g. the user has removed it with a pipeline stage), and close the change stream. The error message SHOULD resemble “Cannot provide resume functionality when the resume token is missing”.
-
-A change stream MUST attempt to resume a single time if it encounters any resumable error. A change stream MUST NOT attempt to resume on any other type of error, with the exception of a “not master” server error. If a driver receives a “not master” error (for instance, because the primary it was connected to is stepping down), it will treat the error as a resumable error and attempt to resume.
-
-In addition to tracking the most recently delivered resume token, change streams MUST also track the read preference specified when the change stream was created. In the event of a resumable error, a change stream MUST perform server selection with the original read preference before attempting to resume.
-
-Single Server Topologies
-^^^^^^^^^^^^^^^^^^^^^^^^
-
-Presently, change streams cannot be initiated on single server topologies as they do not have an oplog. Drivers MUST NOT throw an exception in this scenario, but instead rely on an error returned from the server. This allows for the server to seamlessly introduce support for this in the future, without need to make changes in driver code.
-
-startAtOperationTime
-^^^^^^^^^^^^^^^^^^^^
-
-:since: 4.0
-
-``startAtOperationTime`` specifies that a change stream will only return changes that occurred at or after the specified ``Timestamp``.
-
-The server expects ``startAtOperationTime`` as a BSON Timestamp. Drivers MUST allow users to specify a ``startAtOperationTime`` option in the ``watch`` helpers. They MUST allow users to specify this value as a raw ``Timestamp``.
-
-``startAtOperationTime`` and ``resumeAfter`` are mutually exclusive; if both ``startAtOperationTime`` and ``resumeAfter`` are set, the server will return an error. Drivers MUST NOT throw a custom error, and MUST defer to the server error.
-
-If neither ``startAtOperationTime`` nor ``resumeAfter`` are specified, and the max wire version is >= ``7``, and the initial ``aggregate`` command does not return a resumeToken (indicating no results), the ``ChangeStream`` MUST save the ``operationTime`` from the initial ``aggregate`` command when it returns.
-
-resumeAfter
-^^^^^^^^^^^
-
-``resumeAfter`` is used to resume a changeStream that has been stopped to ensure that only changes starting with the log entry immediately *after* the provided token will be returned. If the resume token specified does not exist, the server will return an error.
-
-Resume Process
-^^^^^^^^^^^^^^
-
-Once a ``ChangeStream`` has encountered a resumable error, it MUST attempt to resume one time. The process for resuming MUST follow these steps:
-
-- Perform server selection.
-- Connect to selected server.
-- If the ``ChangeStream`` has not received any changes, and ``resumeAfter`` is not specified, and the max wire version is >= ``7``:
-
- - The driver MUST execute the known aggregation command.
- - The driver MUST specify the ``startAtOperationTime`` key set to the ``startAtOperationTime`` provided by the user or saved from the original aggregation.
- - The driver MUST NOT set a ``resumeAfter`` key.
- - In this case, the ``ChangeStream`` will return all changes that occurred after the specified ``startAtOperationTime``.
-- Else:
-
- - The driver MUST execute the known aggregation command.
- - The driver MUST specify a ``resumeAfter`` with the last known ``resumeToken``.
- - The driver MUST NOT set a ``startAtOperationTime``.
- - If a ``startAtOperationTime`` key was part of the original aggregation command, the driver MUST remove it.
- - In this case, the ``ChangeStream`` will return notifications starting with the oplog entry immediately *after* the provided token.
-
-If the server supports sessions, the resume attempt MUST use the same session as the previous attempt's command.
-
-A driver SHOULD attempt to kill the cursor on the server on which the cursor is opened during the resume process, and MUST NOT attempt to kill the cursor on any other server.
-
-
-Notes and Restrictions
-^^^^^^^^^^^^^^^^^^^^^^
-
-**1. `fullDocument: updateLookup` can result in change documents larger than 16MB**
-
-There is a risk that if there is a large change to a large document, the full document and delta might result in a document larger than the 16MB limitation on BSON documents. If that happens the cursor will be closed, and a server error will be returned.
-
-**2. Users can remove the resume token with aggregation stages**
-
-It is possible for a user to specify the following stage:
-
-.. code:: javascript
-
- { $project: { _id: 0 } }
-
-Similar removal of the resume token is possible with the ``$redact`` and ``$replaceRoot`` stages. While this is not technically illegal, it makes it impossible for drivers to support resumability. Users may explicitly opt out of resumability by issuing a raw aggregation with a ``$changeStream`` stage.
-
-Rationale
-=========
-
--------------------
-Why Helper Methods?
--------------------
-
-Change streams are a first class concept similar to CRUD or aggregation; the fact that they are initiated via an aggregation pipeline stage is merely an implementation detail. By requiring drivers to support top-level helper methods for this feature we not only signal this intent, but also solve a number of other potential problems:
-
-Disambiguation of the result type of this special-case aggregation pipeline (``ChangeStream``), and an ability to control the behaviors of the resultant cursor
-
-More accurate support for the concept of a maximum time the user is willing to wait for subsequent queries to complete on the resultant cursor (``maxAwaitTimeMs``)
-
-Finer control over the options pertaining specifically to this type of operation, without polluting the already well-defined ``AggregateOptions``
-
-Flexibility for future potentially breaking changes for this feature on the server
-
-------------------------------------------------------------------
-Why are ChangeStreams required to retry once on a resumable error?
-------------------------------------------------------------------
-
-User experience is of the utmost importance. Errors not originating from the server are generally network errors, and network errors can be transient. Attempting to resume an interrupted change stream after the initial error allows for a seamless experience for the user, while subsequent network errors are likely to be an outage which can then be exposed to the user with greater confidence.
-
----------------------------------------------------
-Why do we allow access to the resume token to users
----------------------------------------------------
-
-Imagine a scenario in which a user wants to process each change to a collection **at least once**, but the application crashes during processing. In order to overcome this failure, a user might use the following approach:
-
-.. code:: python
-
- resumeToken = None
- localChange = getChangeFromLocalStorage()
- if localChange:
- processChange(localChange)
- resumeToken = localChange['_id']
-
- try:
- for change in db.collection.watch([...], resumeAfter=resumeToken):
- persistToLocalStorage(change)
- processChange(change)
- except Exception:
- log.error("...")
-
-In this case the current change is always persisted locally, including the resume token, such that on restart the application can still process the change while ensuring that the change stream continues from the right logical time in the oplog. It is the application's responsibility to ensure that ``processChange`` is idempotent, this design merely makes a reasonable effort to process each change **at least** once.
-
--------------------------------------------------------
-Why is there no example of the desired user experience?
--------------------------------------------------------
-
-The specification used to include this overspecified example of the "desired user experience":
-
-.. code:: python
-
- try:
- for change in db.collection.watch(...):
- print(change)
- except Exception:
- # We know for sure it's unrecoverable:
- log.error("...")
-
-It was decided to remove this example from the specification for the following reasons:
-
-- Tailable + awaitData cursors behave differently in existing supported drivers.
-- There are considerations to be made for languages that do not permit interruptible I/O (such as Java), where a change stream which blocks forever in a separate thread would necessitate killing the thread.
-- There is something to be said for an API that allows cooperation by default. The model in which a call to next only blocks until any response is returned (even an empty batch), allows for interruption and cooperation (e.g. interaction with other event loops).
-
-----------------------------------------
-What do the additional error codes mean?
-----------------------------------------
-
-The `CursorKilled` or `Interrupted` error implies implies some other actor killed the cursor.
-
-The `CappedPositionLost` error implies falling off of the back of the oplog,
-so resuming is impossible.
-
--------------------------------------------------------------------------------------------
-Why do we need to send a default ``startAtOperationTime`` when resuming a ``ChangeStream``?
--------------------------------------------------------------------------------------------
-
-``startAtOperationTime`` allows a user to create a resumable change stream even when a result
-(and corresponding resumeToken) is not available until a later point in time.
-
-For example:
-
-- A client creates a ``ChangeStream``, and calls ``watch``
-- The ``ChangeStream`` sends out the initial ``aggregate`` call, and receives a response
-with no initial values. Because there are no initial values, there is no latest resumeToken.
-- The client's network is partitioned from the server, causing the client's ``getMore`` to time out
-- Changes occur on the server.
-- The network is unpartitioned
-- The client attempts to resume the ``ChangeStream``
-
-In the above example, not sending ``startAtOperationTime`` will result in the change stream missing
-the changes that occurred while the server and client are partitioned. By sending ``startAtOperationTime``,
-the server will know to include changes from that previous point in time.
-
-Test Plan
-=========
-
-See `tests/README.rst `_
-
-Backwards Compatibility
-=======================
-
-There should be no backwards compatibility concerns.
-
-
-Reference Implementations
-=========================
-
- - NODE (NODE-1055)
- - PYTHON (PYTHON-1338)
- - RUBY (RUBY-1228)
-
-Changelog
-=========
-+------------+------------------------------------------------------------+
-| 2017-08-03 | Initial commit |
-+------------+------------------------------------------------------------+
-| 2017-08-07 | Fixed typo in command format |
-+------------+------------------------------------------------------------+
-| 2017-08-16 | Added clarification regarding Resumable errors |
-+------------+------------------------------------------------------------+
-| 2017-08-16 | Fixed formatting of resume process |
-+------------+------------------------------------------------------------+
-| 2017-08-22 | Clarified killing cursors during resume process |
-+------------+------------------------------------------------------------+
-| 2017-09-06 | Remove `desired user experience` example |
-+------------+------------------------------------------------------------+
-| 2017-09-21 | Clarified that we need to close the cursor on missing token|
-+------------+------------------------------------------------------------+
-| 2017-09-26 | Clarified that change stream options may be added later |
-+------------+------------------------------------------------------------+
-| 2017-11-06 | Defer to Read and Write concern spec for determining a read|
-| | concern for the helper method. |
-+------------+------------------------------------------------------------+
-| 2017-12-13 | Default read concern is also accepted, not just "majority".|
-+------------+------------------------------------------------------------+
-| 2018-04-17 | Clarified that the initial aggregate should not be retried.|
-+------------+------------------------------------------------------------+
-| 2018-04-18 | Added helpers for Database and MongoClient, |
-| | and added ``startAtClusterTime`` option. |
-+------------+------------------------------------------------------------+
-| 2018-05-24 | Changed ``startatClusterTime`` to ``startAtOperationTime`` |
-+------------+------------------------------------------------------------+
-| 2018-06-14 | Clarified how to calculate ``startAtOperationTime`` |
-+------------+------------------------------------------------------------+
-| 2018-07-27 | Added drop to change stream operationType |
-+------------+------------------------------------------------------------+
-| 2018-07-30 | Remove redundant error message checks for resumable errors |
-+------------+------------------------------------------------------------+
-| 2018-09-09 | Added dropDatabase to change stream operationType |
-+------------+------------------------------------------------------------+
+.. note::
+ This specification has been converted to Markdown and renamed to
+ `change-streams.md `_.
diff --git a/source/change-streams/tests/README.md b/source/change-streams/tests/README.md
new file mode 100644
index 0000000000..3804168576
--- /dev/null
+++ b/source/change-streams/tests/README.md
@@ -0,0 +1,252 @@
+# Change Streams
+
+______________________________________________________________________
+
+## Introduction
+
+The YAML and JSON files in this directory are platform-independent tests that drivers can use to prove their conformance
+to the Change Streams Spec.
+
+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.
+
+### Subdirectories for Test Formats
+
+This document describes the legacy format for change streams tests. Tests in this legacy format are located under
+`./legacy/`.
+
+New change streams tests should be written in the
+[unified test format](../../unified-test-format/unified-test-format.md) and placed under `./unified/`.
+
+## Spec Test Format
+
+Each YAML file has the following keys:
+
+- `database_name`: The default database
+- `collection_name`: The default collection
+- `database2_name`: Another database
+- `collection2_name`: Another collection
+- `tests`: An array of tests that are to be run independently of each other. Each test will have some of the following
+ fields:
+ - `description`: The name of the test.
+ - `minServerVersion`: The minimum server version to run this test against. If not present, assume there is no minimum
+ server version.
+ - `maxServerVersion`: Reserved for later use
+ - `failPoint`: Optional configureFailPoint command document to run to configure a fail point on the primary server.
+ - `target`: The entity on which to run the change stream. Valid values are:
+ - `collection`: Watch changes on collection `database_name.collection_name`
+ - `database`: Watch changes on database `database_name`
+ - `client`: Watch changes on entire clusters
+ - `topology`: An array of server topologies against which to run the test. Valid topologies are `single`,
+ `replicaset`, `sharded`, and `load-balanced`.
+ - `changeStreamPipeline`: An array of additional aggregation pipeline stages to add to the change stream
+ - `changeStreamOptions`: Additional options to add to the changeStream
+ - `operations`: Array of documents, each describing an operation. Each document has the following fields:
+ - `database`: Database against which to run the operation
+ - `collection`: Collection against which to run the operation
+ - `name`: Name of the command to run
+ - `arguments` (optional): Object of arguments for the command (ex: document to insert)
+ - `expectations`: Optional list of command-started events in Extended JSON format
+ - `result`: Document with ONE of the following fields:
+ - `error`: Describes an error received during the test
+ - `success`: An Extended JSON array of documents expected to be received from the changeStream
+
+## Spec Test Match Function
+
+The definition of MATCH or MATCHES in the Spec Test Runner is as follows:
+
+- MATCH takes two values, `expected` and `actual`
+- Notation is "Assert [actual] MATCHES [expected]
+- Assertion passes if `expected` is a subset of `actual`, with the value `42` acting as placeholders for "any value"
+
+Pseudocode implementation of `actual` MATCHES `expected`:
+
+```text
+If expected is "42" or 42:
+ Assert that actual exists (is not null or undefined)
+Else:
+ Assert that actual is of the same JSON type as expected
+ If expected is a JSON array:
+ For every idx/value in expected:
+ Assert that actual[idx] MATCHES value
+ Else if expected is a JSON object:
+ For every key/value in expected
+ Assert that actual[key] MATCHES value
+ Else:
+ Assert that expected equals actual
+```
+
+The expected values for `result.success` and `expectations` are written in Extended JSON. Drivers may adopt any of the
+following approaches to comparisons, as long as they are consistent:
+
+- Convert `actual` to Extended JSON and compare to `expected`
+- Convert `expected` and `actual` to BSON, and compare them
+- Convert `expected` and `actual` to native equivalents of JSON, and compare them
+
+## Spec Test Runner
+
+Before running the tests
+
+- Create a MongoClient `globalClient`, and connect to the server. When executing tests against a sharded cluster,
+ `globalClient` must only connect to one mongos. This is because tests that set failpoints will only work
+ consistently if both the `configureFailPoint` and failing commands are sent to the same mongos.
+
+For each YAML file, for each element in `tests`:
+
+- If `topology` does not include the topology of the server instance(s), skip this test.
+- Use `globalClient` to
+ - Drop the database `database_name`
+ - Drop the database `database2_name`
+ - Create the database `database_name` and the collection `database_name.collection_name`
+ - Create the database `database2_name` and the collection `database2_name.collection2_name`
+ - If the the `failPoint` field is present, configure the fail point on the primary server. See
+ [Server Fail Point](../../transactions/tests/legacy-test-format.md#server-fail-point) in the Transactions spec
+ test documentation for more information.
+- Create a new MongoClient `client`
+- Begin monitoring all APM events for `client`. (If the driver uses global listeners, filter out all events that do not
+ originate with `client`). Filter out any "internal" commands (e.g. `hello` or legacy hello)
+- Using `client`, create a changeStream `changeStream` against the specified `target`. Use `changeStreamPipeline` and
+ `changeStreamOptions` if they are non-empty. Capture any error.
+- If there was no error, use `globalClient` and run every operation in `operations` in serial against the server until
+ all operations have been executed or an error is thrown. Capture any error.
+- If there was no error and `result.error` is set, iterate `changeStream` once and capture any error.
+- If there was no error and `result.success` is non-empty, iterate `changeStream` until it returns as many changes as
+ there are elements in the `result.success` array or an error is thrown. Capture any error.
+- Close `changeStream`
+- If there was an error:
+ - Assert that an error was expected for the test.
+ - Assert that the error MATCHES `result.error`
+- Else:
+ - Assert that no error was expected for the test
+ - Assert that the changes received from `changeStream` MATCH the results in `result.success`
+- If there are any `expectations`
+ - For each (`expected`, `idx`) in `expectations`
+ - If `actual[idx]` is a `killCursors` event, skip it and move to `actual[idx+1]`.
+ - Else assert that `actual[idx]` MATCHES `expected`
+ - Note: the change stream test command event expectations cover a prefix subset of all command events published by the
+ driver. The test runner MUST verify that, if there are N expectations, that the first N events published by the
+ driver match the expectations, and MUST NOT inspect any subsequent events published by the driver.
+- Close the MongoClient `client`
+
+After running all tests
+
+- Close the MongoClient `globalClient`
+- Drop database `database_name`
+- Drop database `database2_name`
+
+### Iterating the Change Stream
+
+Although synchronous drivers must provide a
+[non-blocking mode of iteration](../change-streams.md#not-blocking-on-iteration), asynchronous drivers may not have such
+a mechanism. Those drivers with only a blocking mode of iteration should be careful not to iterate the change stream
+unnecessarily, as doing so could cause the test runner to block indefinitely. For this reason, the test runner procedure
+above advises drivers to take a conservative approach to iteration.
+
+If the test expects an error and one was not thrown by either creating the change stream or executing the test's
+operations, iterating the change stream once allows for an error to be thrown by a `getMore` command. If the test does
+not expect any error, the change stream should be iterated only until it returns as many result documents as are
+expected by the test.
+
+### Testing on Sharded Clusters
+
+When writing data on sharded clusters, majority-committed data does not always show up in the response of the first
+`getMore` command after the data is written. This is because in sharded clusters, no data from shard A may be returned
+until all other shard reports an entry that sorts after the change in shard A.
+
+To account for this, drivers MUST NOT rely on change stream documents in certain batches. For example, if expecting two
+documents in a change stream, these may not be part of the same `getMore` response, or even be produced in two
+subsequent `getMore` responses. Drivers MUST allow for a `getMore` to produce empty batches when testing on a sharded
+cluster. By default, this can take up to 10 seconds, but can be controlled by enabling the `writePeriodicNoops` server
+parameter and configuring the `periodNoopIntervalSecs` parameter. Choosing lower values allows for running change stream
+tests with smaller timeouts.
+
+## Prose Tests
+
+The following tests have not yet been automated, but MUST still be tested. All tests SHOULD be run on both replica sets
+and sharded clusters unless otherwise specified:
+
+1. `ChangeStream` must continuously track the last seen `resumeToken`
+
+2. `ChangeStream` will throw an exception if the server response is missing the resume token (if wire version is < 8,
+ this is a driver-side error; for 8+, this is a server-side error)
+
+3. After receiving a `resumeToken`, `ChangeStream` will automatically resume one time on a resumable error with the
+ initial pipeline and options, except for the addition/update of a `resumeToken`.
+
+4. `ChangeStream` will not attempt to resume on any error encountered while executing an `aggregate` command. Note that
+ retryable reads may retry `aggregate` commands. Drivers should be careful to distinguish retries from resume
+ attempts. Alternatively, drivers may specify `retryReads=false` or avoid using a
+ [retryable error](../../retryable-reads/retryable-reads.md#retryable-error) for this test.
+
+5. **Removed**
+
+6. `ChangeStream` will perform server selection before attempting to resume, using initial `readPreference`
+
+7. Ensure that a cursor returned from an aggregate command with a cursor id and an initial empty batch is not closed on
+ the driver side.
+
+8. The `killCursors` command sent during the "Resume Process" must not be allowed to throw an exception.
+
+9. `$changeStream` stage for `ChangeStream` against a server `>=4.0` and `<4.0.7` that has not received any results yet
+ MUST include a `startAtOperationTime` option when resuming a change stream.
+
+10. **Removed**
+
+11. For a `ChangeStream` under these conditions:
+
+ - The batch is empty or has been iterated to the last document.
+
+ Expected result:
+
+ - `getResumeToken` must return the `postBatchResumeToken` from the current command response.
+
+12. **Removed**
+
+13. For a `ChangeStream` under these conditions:
+
+ - The batch is not empty.
+ - The batch has been iterated up to but not including the last element.
+
+ Expected result:
+
+ - `getResumeToken` must return the `_id` of the previous document returned.
+
+14. For a `ChangeStream` under these conditions:
+
+ - The batch is not empty.
+ - The batch hasn’t been iterated at all.
+ - Only the initial `aggregate` command has been executed.
+
+ Expected result:
+
+ - `getResumeToken` must return `startAfter` from the initial aggregate if the option was specified.
+ - `getResumeToken` must return `resumeAfter` from the initial aggregate if the option was specified.
+ - If neither the `startAfter` nor `resumeAfter` options were specified, the `getResumeToken` result must be empty.
+
+ Note that this test cannot be run against sharded topologies because in that case the initial `aggregate` command
+ only establishes cursors on the shards and always returns an empty `firstBatch`.
+
+15. **Removed**
+
+16. **Removed**
+
+17. `$changeStream` stage for `ChangeStream` started with `startAfter` against a server `>=4.1.1` that has not received
+ any results yet MUST include a `startAfter` option and MUST NOT include a `resumeAfter` option when resuming a
+ change stream.
+
+18. `$changeStream` stage for `ChangeStream` started with `startAfter` against a server `>=4.1.1` that has received at
+ least one result MUST include a `resumeAfter` option and MUST NOT include a `startAfter` option when resuming a
+ change stream.
+
+19. Validate that large `ChangeStream` events are split when using `$changeStreamSplitLargeEvent`:
+
+ 1. Run only against servers `>=6.0.9 && <6.1` or `>=7.0`.
+ 2. Create a new collection `_[C]()` with `changeStreamPreAndPostImages` enabled.
+ 3. Insert into `_[C]()` a document at least 10mb in size, e.g. `{ "value": "q"*10*1024*1024 }`
+ 4. Create a change stream `_[S]()` by calling `watch` on `_[C]()` with pipeline
+ `[{ "$changeStreamSplitLargeEvent": {} }]` and `fullDocumentBeforeChange=required`.
+ 5. Call `updateOne` on `_[C]()` with an empty `query` and an update setting the field to a new large value, e.g.
+ `{ "$set": { "value": "z"*10*1024*1024 } }`.
+ 6. Collect two events from `_[S]()`.
+ 7. Assert that the events collected have `splitEvent` fields `{ "fragment": 1, "of": 2 }` and
+ `{ "fragment": 2, "of": 2 }`, in that order.
diff --git a/source/change-streams/tests/README.rst b/source/change-streams/tests/README.rst
deleted file mode 100644
index 98fdab0b72..0000000000
--- a/source/change-streams/tests/README.rst
+++ /dev/null
@@ -1,157 +0,0 @@
-.. role:: javascript(code)
- :language: javascript
-
-==============
-Change Streams
-==============
-
-.. contents::
-
---------
-
-Introduction
-============
-
-The YAML and JSON files in this directory are platform-independent tests that
-drivers can use to prove their conformance to the Change Streams Spec.
-
-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.
-
-Spec Test Format
-================
-
-Each YAML file has the following keys:
-
-- ``database_name``: The default database
-- ``collection_name``: The default collection
-- ``database2_name``: Another database
-- ``collection2_name``: Another collection
-- ``tests``: An array of tests that are to be run independently of each other.
- Each test will have some of the following fields:
-
- - ``description``: The name of the test.
- - ``minServerVersion``: The minimum server version to run this test against. If not present, assume there is no minimum server version.
- - ``maxServerVersion``: Reserved for later use
- - ``failPoint``: Reserved for later use
- - ``target``: The entity on which to run the change stream. Valid values are:
-
- - ``collection``: Watch changes on collection ``database_name.collection_name``
- - ``database``: Watch changes on database ``database_name``
- - ``client``: Watch changes on entire clusters
- - ``topology``: An array of server topologies against which to run the test.
- Valid topologies are ``single``, ``replicaset``, and ``sharded``.
- - ``changeStreamPipeline``: An array of additional aggregation pipeline stages to add to the change stream
- - ``changeStreamOptions``: Additional options to add to the changeStream
- - ``operations``: Array of documents, each describing an operation. Each document has the following fields:
-
- - ``database``: Database against which to run the operation
- - ``collection``: Collection against which to run the operation
- - ``name``: Name of the command to run
- - ``arguments``: Object of arguments for the command (ex: document to insert)
-
- - ``expectations``: Optional list of command-started events in Extended JSON format
- - ``result``: Document with ONE of the following fields:
-
- - ``error``: Describes an error received during the test
- - ``success``: An Extended JSON array of documents expected to be received from the changeStream
-
-Spec Test Match Function
-========================
-
-The definition of MATCH or MATCHES in the Spec Test Runner is as follows:
-
-- MATCH takes two values, ``expected`` and ``actual``
-- Notation is "Assert [actual] MATCHES [expected]
-- Assertion passes if ``expected`` is a subset of ``actual``, with the values ``42`` and ``"42"`` acting as placeholders for "any value"
-
-Pseudocode implementation of ``actual`` MATCHES ``expected``:
-
-::
-
- If expected is "42" or 42:
- Assert that actual exists (is not null or undefined)
- Else:
- Assert that actual is of the same JSON type as expected
- If expected is a JSON array:
- For every idx/value in expected:
- Assert that actual[idx] MATCHES value
- Else if expected is a JSON object:
- For every key/value in expected
- Assert that actual[key] MATCHES value
- Else:
- Assert that expected equals actual
-
-The expected values for ``result.success`` and ``expectations`` are written in Extended JSON. Drivers may adopt any of the following approaches to comparisons, as long as they are consistent:
-
-- Convert ``actual`` to Extended JSON and compare to ``expected``
-- Convert ``expected`` and ``actual`` to BSON, and compare them
-- Convert ``expected`` and ``actual`` to native equivalents of JSON, and compare them
-
-Spec Test Runner
-================
-
-Before running the tests
-
-- Create a MongoClient ``globalClient``, and connect to the server
-
-For each YAML file, for each element in ``tests``:
-
-- If ``topology`` does not include the topology of the server instance(s), skip this test.
-- Use ``globalClient`` to
-
- - Drop the database ``database_name``
- - Drop the database ``database2_name``
- - Create the database ``database_name`` and the collection ``database_name.collection_name``
- - Create the database ``database2_name`` and the collection ``database2_name.collection2_name``
-
-- Create a new MongoClient ``client``
-- Begin monitoring all APM events for ``client``. (If the driver uses global listeners, filter out all events that do not originate with ``client``). Filter out any "internal" commands (e.g. ``isMaster``)
-- Using ``client``, create a changeStream ``changeStream`` against the specified ``target``. Use ``changeStreamPipeline`` and ``changeStreamOptions`` if they are non-empty
-- Using ``globalClient``, run every operation in ``operations`` in serial against the server
-- Wait until either:
-
- - An error occurs
- - All operations have been successful AND the changeStream has received as many changes as there are in ``result.success``
-
-- Close ``changeStream``
-- If there was an error:
-
- - Assert that an error was expected for the test.
- - Assert that the error MATCHES ``results.error``
-
-- Else:
-
- - Assert that no error was expected for the test
- - Assert that the changes received from ``changeStream`` MATCH the results in ``results.success``
-
-- If there are any ``expectations``
-
- - For each (``expected``, ``idx``) in ``expectations``
-
- - Assert that ``actual[idx]`` MATCHES ``expected``
-
-- Close the MongoClient ``client``
-
-After running all tests
-
-- Close the MongoClient ``globalClient``
-- Drop database ``database_name``
-- Drop database ``database2_name``
-
-
-Prose Tests
-===========
-
-The following tests have not yet been automated, but MUST still be tested
-
-#. ``ChangeStream`` must continuously track the last seen ``resumeToken``
-#. ``ChangeStream`` will throw an exception if the server response is missing the resume token
-#. ``ChangeStream`` will automatically resume one time on a resumable error (including `not master`) with the initial pipeline and options, except for the addition/update of a ``resumeToken``.
-#. ``ChangeStream`` will not attempt to resume on any error encountered while executing an ``aggregate`` command.
-#. ``ChangeStream`` will not attempt to resume after encountering error code 11601 (Interrupted), 136 (CappedPositionLost), or 237 (CursorKilled) while executing a ``getMore`` command.
-#. ``ChangeStream`` will perform server selection before attempting to resume, using initial ``readPreference``
-#. Ensure that a cursor returned from an aggregate command with a cursor id and an initial empty batch is not closed on the driver side.
-#. The ``killCursors`` command sent during the "Resume Process" must not be allowed to throw an exception.
-#. ``$changeStream`` stage for ``ChangeStream`` against a server ``>=4.0`` that has not received any results yet MUST include a ``startAtOperationTime`` option when resuming a changestream.
-#. ``ChangeStream`` will resume after a ``killCursors`` command is issued for its child cursor.
diff --git a/source/change-streams/tests/change-streams-errors.json b/source/change-streams/tests/change-streams-errors.json
deleted file mode 100644
index 053ac43e70..0000000000
--- a/source/change-streams/tests/change-streams-errors.json
+++ /dev/null
@@ -1,78 +0,0 @@
-{
- "collection_name": "test",
- "database_name": "change-stream-tests",
- "collection2_name": "test2",
- "database2_name": "change-stream-tests-2",
- "tests": [
- {
- "description": "The watch helper must not throw a custom exception when executed against a single server topology, but instead depend on a server error",
- "minServerVersion": "3.6.0",
- "target": "collection",
- "topology": [
- "single"
- ],
- "changeStreamPipeline": [],
- "changeStreamOptions": {},
- "operations": [],
- "expectations": [],
- "result": {
- "error": {
- "code": 40573
- }
- }
- },
- {
- "description": "Change Stream should error when an invalid aggregation stage is passed in",
- "minServerVersion": "3.6.0",
- "target": "collection",
- "topology": [
- "replicaset"
- ],
- "changeStreamPipeline": [
- {
- "$unsupported": "foo"
- }
- ],
- "changeStreamOptions": {},
- "operations": [
- {
- "database": "change-stream-tests",
- "collection": "test",
- "name": "insertOne",
- "arguments": {
- "document": {
- "z": 3
- }
- }
- }
- ],
- "expectations": [
- {
- "command_started_event": {
- "command": {
- "aggregate": "test",
- "cursor": {},
- "pipeline": [
- {
- "$changeStream": {
- "fullDocument": "default"
- }
- },
- {
- "$unsupported": "foo"
- }
- ]
- },
- "command_name": "aggregate",
- "database_name": "change-stream-tests"
- }
- }
- ],
- "result": {
- "error": {
- "code": 40324
- }
- }
- }
- ]
-}
diff --git a/source/change-streams/tests/change-streams-errors.yml b/source/change-streams/tests/change-streams-errors.yml
deleted file mode 100644
index 1286e86588..0000000000
--- a/source/change-streams/tests/change-streams-errors.yml
+++ /dev/null
@@ -1,53 +0,0 @@
-collection_name: &collection_name "test"
-database_name: &database_name "change-stream-tests"
-collection2_name: &collection2_name "test2"
-database2_name: &database2_name "change-stream-tests-2"
-tests:
- -
- description: The watch helper must not throw a custom exception when executed against a single server topology, but instead depend on a server error
- minServerVersion: "3.6.0"
- target: collection
- topology:
- - single
- changeStreamPipeline: []
- changeStreamOptions: {}
- operations: []
- expectations: []
- result:
- error:
- code: 40573
- -
- description: Change Stream should error when an invalid aggregation stage is passed in
- minServerVersion: "3.6.0"
- target: collection
- topology:
- - replicaset
- changeStreamPipeline:
- -
- $unsupported: foo
- changeStreamOptions: {}
- operations:
- -
- database: *database_name
- collection: *collection_name
- name: insertOne
- arguments:
- document:
- z: 3
- expectations:
- -
- command_started_event:
- command:
- aggregate: *collection_name
- cursor: {}
- pipeline:
- -
- $changeStream:
- fullDocument: default
- -
- $unsupported: foo
- command_name: aggregate
- database_name: *database_name
- result:
- error:
- code: 40324
\ No newline at end of file
diff --git a/source/change-streams/tests/change-streams.json b/source/change-streams/tests/change-streams.json
deleted file mode 100644
index 5c28b6276f..0000000000
--- a/source/change-streams/tests/change-streams.json
+++ /dev/null
@@ -1,445 +0,0 @@
-{
- "collection_name": "test",
- "database_name": "change-stream-tests",
- "collection2_name": "test2",
- "database2_name": "change-stream-tests-2",
- "tests": [
- {
- "description": "$changeStream must be the first stage in a change stream pipeline sent to the server",
- "minServerVersion": "3.6.0",
- "target": "collection",
- "topology": [
- "replicaset"
- ],
- "changeStreamPipeline": [],
- "changeStreamOptions": {},
- "operations": [
- {
- "database": "change-stream-tests",
- "collection": "test",
- "name": "insertOne",
- "arguments": {
- "document": {
- "x": 1
- }
- }
- }
- ],
- "expectations": [
- {
- "command_started_event": {
- "command": {
- "aggregate": "test",
- "cursor": {},
- "pipeline": [
- {
- "$changeStream": {
- "fullDocument": "default"
- }
- }
- ]
- },
- "command_name": "aggregate",
- "database_name": "change-stream-tests"
- }
- }
- ],
- "result": {
- "success": []
- }
- },
- {
- "description": "The server returns change stream responses in the specified server response format",
- "minServerVersion": "3.6.0",
- "target": "collection",
- "topology": [
- "replicaset"
- ],
- "changeStreamPipeline": [],
- "changeStreamOptions": {},
- "operations": [
- {
- "database": "change-stream-tests",
- "collection": "test",
- "name": "insertOne",
- "arguments": {
- "document": {
- "x": 1
- }
- }
- }
- ],
- "expectations": [],
- "result": {
- "success": [
- {
- "_id": "42",
- "documentKey": "42",
- "operationType": "insert",
- "ns": {
- "db": "change-stream-tests",
- "coll": "test"
- },
- "fullDocument": {
- "x": {
- "$numberInt": "1"
- }
- }
- }
- ]
- }
- },
- {
- "description": "Executing a watch helper on a Collection results in notifications for changes to the specified collection",
- "minServerVersion": "3.6.0",
- "target": "collection",
- "topology": [
- "replicaset"
- ],
- "changeStreamPipeline": [],
- "changeStreamOptions": {},
- "operations": [
- {
- "database": "change-stream-tests",
- "collection": "test2",
- "name": "insertOne",
- "arguments": {
- "document": {
- "x": 1
- }
- }
- },
- {
- "database": "change-stream-tests-2",
- "collection": "test",
- "name": "insertOne",
- "arguments": {
- "document": {
- "y": 2
- }
- }
- },
- {
- "database": "change-stream-tests",
- "collection": "test",
- "name": "insertOne",
- "arguments": {
- "document": {
- "z": 3
- }
- }
- }
- ],
- "expectations": [
- {
- "command_started_event": {
- "command": {
- "aggregate": "test",
- "cursor": {},
- "pipeline": [
- {
- "$changeStream": {
- "fullDocument": "default"
- }
- }
- ]
- },
- "command_name": "aggregate",
- "database_name": "change-stream-tests"
- }
- }
- ],
- "result": {
- "success": [
- {
- "operationType": "insert",
- "ns": {
- "db": "change-stream-tests",
- "coll": "test"
- },
- "fullDocument": {
- "z": {
- "$numberInt": "3"
- }
- }
- }
- ]
- }
- },
- {
- "description": "Change Stream should allow valid aggregate pipeline stages",
- "minServerVersion": "3.6.0",
- "target": "collection",
- "topology": [
- "replicaset"
- ],
- "changeStreamPipeline": [
- {
- "$match": {
- "fullDocument.z": 3
- }
- }
- ],
- "changeStreamOptions": {},
- "operations": [
- {
- "database": "change-stream-tests",
- "collection": "test",
- "name": "insertOne",
- "arguments": {
- "document": {
- "y": 2
- }
- }
- },
- {
- "database": "change-stream-tests",
- "collection": "test",
- "name": "insertOne",
- "arguments": {
- "document": {
- "z": 3
- }
- }
- }
- ],
- "expectations": [
- {
- "command_started_event": {
- "command": {
- "aggregate": "test",
- "cursor": {},
- "pipeline": [
- {
- "$changeStream": {
- "fullDocument": "default"
- }
- },
- {
- "$match": {
- "fullDocument.z": {
- "$numberInt": "3"
- }
- }
- }
- ]
- },
- "command_name": "aggregate",
- "database_name": "change-stream-tests"
- }
- }
- ],
- "result": {
- "success": [
- {
- "operationType": "insert",
- "ns": {
- "db": "change-stream-tests",
- "coll": "test"
- },
- "fullDocument": {
- "z": {
- "$numberInt": "3"
- }
- }
- }
- ]
- }
- },
- {
- "description": "Executing a watch helper on a Database results in notifications for changes to all collections in the specified database.",
- "minServerVersion": "3.8.0",
- "target": "database",
- "topology": [
- "replicaset"
- ],
- "changeStreamPipeline": [],
- "changeStreamOptions": {},
- "operations": [
- {
- "database": "change-stream-tests",
- "collection": "test2",
- "name": "insertOne",
- "arguments": {
- "document": {
- "x": 1
- }
- }
- },
- {
- "database": "change-stream-tests-2",
- "collection": "test",
- "name": "insertOne",
- "arguments": {
- "document": {
- "y": 2
- }
- }
- },
- {
- "database": "change-stream-tests",
- "collection": "test",
- "name": "insertOne",
- "arguments": {
- "document": {
- "z": 3
- }
- }
- }
- ],
- "expectations": [
- {
- "command_started_event": {
- "command": {
- "aggregate": {
- "$numberInt": "1"
- },
- "cursor": {},
- "pipeline": [
- {
- "$changeStream": {
- "fullDocument": "default"
- }
- }
- ]
- },
- "command_name": "aggregate",
- "database_name": "change-stream-tests"
- }
- }
- ],
- "result": {
- "success": [
- {
- "operationType": "insert",
- "ns": {
- "db": "change-stream-tests",
- "coll": "test2"
- },
- "fullDocument": {
- "x": {
- "$numberInt": "1"
- }
- }
- },
- {
- "operationType": "insert",
- "ns": {
- "db": "change-stream-tests",
- "coll": "test"
- },
- "fullDocument": {
- "z": {
- "$numberInt": "3"
- }
- }
- }
- ]
- }
- },
- {
- "description": "Executing a watch helper on a MongoClient results in notifications for changes to all collections in all databases in the cluster.",
- "minServerVersion": "3.8.0",
- "target": "client",
- "topology": [
- "replicaset"
- ],
- "changeStreamPipeline": [],
- "changeStreamOptions": {},
- "operations": [
- {
- "database": "change-stream-tests",
- "collection": "test2",
- "name": "insertOne",
- "arguments": {
- "document": {
- "x": 1
- }
- }
- },
- {
- "database": "change-stream-tests-2",
- "collection": "test",
- "name": "insertOne",
- "arguments": {
- "document": {
- "y": 2
- }
- }
- },
- {
- "database": "change-stream-tests",
- "collection": "test",
- "name": "insertOne",
- "arguments": {
- "document": {
- "z": 3
- }
- }
- }
- ],
- "expectations": [
- {
- "command_started_event": {
- "command": {
- "aggregate": {
- "$numberInt": "1"
- },
- "cursor": {},
- "pipeline": [
- {
- "$changeStream": {
- "fullDocument": "default",
- "allChangesForCluster": true
- }
- }
- ]
- },
- "command_name": "aggregate",
- "database_name": "admin"
- }
- }
- ],
- "result": {
- "success": [
- {
- "operationType": "insert",
- "ns": {
- "db": "change-stream-tests",
- "coll": "test2"
- },
- "fullDocument": {
- "x": {
- "$numberInt": "1"
- }
- }
- },
- {
- "operationType": "insert",
- "ns": {
- "db": "change-stream-tests-2",
- "coll": "test"
- },
- "fullDocument": {
- "y": {
- "$numberInt": "2"
- }
- }
- },
- {
- "operationType": "insert",
- "ns": {
- "db": "change-stream-tests",
- "coll": "test"
- },
- "fullDocument": {
- "z": {
- "$numberInt": "3"
- }
- }
- }
- ]
- }
- }
- ]
-}
diff --git a/source/change-streams/tests/change-streams.yml b/source/change-streams/tests/change-streams.yml
deleted file mode 100644
index 720a22f973..0000000000
--- a/source/change-streams/tests/change-streams.yml
+++ /dev/null
@@ -1,299 +0,0 @@
-collection_name: &collection_name "test"
-database_name: &database_name "change-stream-tests"
-collection2_name: &collection2_name "test2"
-database2_name: &database2_name "change-stream-tests-2"
-tests:
- -
- description: "$changeStream must be the first stage in a change stream pipeline sent to the server"
- minServerVersion: "3.6.0"
- target: collection
- topology:
- - replicaset
- changeStreamPipeline: []
- changeStreamOptions: {}
- operations:
- -
- database: *database_name
- collection: *collection_name
- name: insertOne
- arguments:
- document:
- x: 1
- expectations:
- -
- command_started_event:
- command:
- aggregate: *collection_name
- cursor: {}
- pipeline:
- -
- $changeStream:
- fullDocument: default
- command_name: aggregate
- database_name: *database_name
- result:
- success: []
- -
- description: The server returns change stream responses in the specified server response format
- minServerVersion: "3.6.0"
- target: collection
- topology:
- - replicaset
- changeStreamPipeline: []
- changeStreamOptions: {}
- operations:
- -
- database: *database_name
- collection: *collection_name
- name: insertOne
- arguments:
- document:
- x: 1
- expectations: []
- result:
- success:
- -
- _id: "42"
- documentKey: "42"
- operationType: insert
- ns:
- db: *database_name
- coll: *collection_name
- fullDocument:
- x:
- $numberInt: "1"
- -
- description: Executing a watch helper on a Collection results in notifications for changes to the specified collection
- minServerVersion: "3.6.0"
- target: collection
- topology:
- - replicaset
- changeStreamPipeline: []
- changeStreamOptions: {}
- operations:
- -
- database: *database_name
- collection: *collection2_name
- name: insertOne
- arguments:
- document:
- x: 1
- -
- database: *database2_name
- collection: *collection_name
- name: insertOne
- arguments:
- document:
- y: 2
- -
- database: *database_name
- collection: *collection_name
- name: insertOne
- arguments:
- document:
- z: 3
- expectations:
- -
- command_started_event:
- command:
- aggregate: *collection_name
- cursor: {}
- pipeline:
- -
- $changeStream:
- fullDocument: default
- command_name: aggregate
- database_name: *database_name
- result:
- success:
- -
- operationType: insert
- ns:
- db: *database_name
- coll: *collection_name
- fullDocument:
- z:
- $numberInt: "3"
- -
- description: Change Stream should allow valid aggregate pipeline stages
- minServerVersion: "3.6.0"
- target: collection
- topology:
- - replicaset
- changeStreamPipeline:
- -
- $match:
- "fullDocument.z": 3
- changeStreamOptions: {}
- operations:
- -
- database: *database_name
- collection: *collection_name
- name: insertOne
- arguments:
- document:
- y: 2
- -
- database: *database_name
- collection: *collection_name
- name: insertOne
- arguments:
- document:
- z: 3
- expectations:
- -
- command_started_event:
- command:
- aggregate: *collection_name
- cursor: {}
- pipeline:
- -
- $changeStream:
- fullDocument: default
- -
- $match:
- "fullDocument.z":
- $numberInt: "3"
- command_name: aggregate
- database_name: *database_name
- result:
- success:
- -
- operationType: insert
- ns:
- db: *database_name
- coll: *collection_name
- fullDocument:
- z:
- $numberInt: "3"
- -
- description: Executing a watch helper on a Database results in notifications for changes to all collections in the specified database.
- minServerVersion: "3.8.0"
- target: database
- topology:
- - replicaset
- changeStreamPipeline: []
- changeStreamOptions: {}
- operations:
- -
- database: *database_name
- collection: *collection2_name
- name: insertOne
- arguments:
- document:
- x: 1
- -
- database: *database2_name
- collection: *collection_name
- name: insertOne
- arguments:
- document:
- y: 2
- -
- database: *database_name
- collection: *collection_name
- name: insertOne
- arguments:
- document:
- z: 3
- expectations:
- -
- command_started_event:
- command:
- aggregate:
- $numberInt: "1"
- cursor: {}
- pipeline:
- -
- $changeStream:
- fullDocument: default
- command_name: aggregate
- database_name: *database_name
- result:
- success:
- -
- operationType: insert
- ns:
- db: *database_name
- coll: *collection2_name
- fullDocument:
- x:
- $numberInt: "1"
- -
- operationType: insert
- ns:
- db: *database_name
- coll: *collection_name
- fullDocument:
- z:
- $numberInt: "3"
- -
- description: Executing a watch helper on a MongoClient results in notifications for changes to all collections in all databases in the cluster.
- minServerVersion: "3.8.0"
- target: client
- topology:
- - replicaset
- changeStreamPipeline: []
- changeStreamOptions: {}
- operations:
- -
- database: *database_name
- collection: *collection2_name
- name: insertOne
- arguments:
- document:
- x: 1
- -
- database: *database2_name
- collection: *collection_name
- name: insertOne
- arguments:
- document:
- y: 2
- -
- database: *database_name
- collection: *collection_name
- name: insertOne
- arguments:
- document:
- z: 3
- expectations:
- -
- command_started_event:
- command:
- aggregate:
- $numberInt: "1"
- cursor: {}
- pipeline:
- -
- $changeStream:
- fullDocument: default
- allChangesForCluster: true
- command_name: aggregate
- database_name: admin
- result:
- success:
- -
- operationType: insert
- ns:
- db: *database_name
- coll: *collection2_name
- fullDocument:
- x:
- $numberInt: "1"
- -
- operationType: insert
- ns:
- db: *database2_name
- coll: *collection_name
- fullDocument:
- y:
- $numberInt: "2"
- -
- operationType: insert
- ns:
- db: *database_name
- coll: *collection_name
- fullDocument:
- z:
- $numberInt: "3"
diff --git a/source/change-streams/tests/unified/change-streams-clusterTime.json b/source/change-streams/tests/unified/change-streams-clusterTime.json
new file mode 100644
index 0000000000..2b09e548f1
--- /dev/null
+++ b/source/change-streams/tests/unified/change-streams-clusterTime.json
@@ -0,0 +1,81 @@
+{
+ "description": "change-streams-clusterTime",
+ "schemaVersion": "1.4",
+ "createEntities": [
+ {
+ "client": {
+ "id": "client0",
+ "useMultipleMongoses": false
+ }
+ },
+ {
+ "database": {
+ "id": "database0",
+ "client": "client0",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection0",
+ "database": "database0",
+ "collectionName": "collection0"
+ }
+ }
+ ],
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.0.0",
+ "topologies": [
+ "replicaset",
+ "load-balanced",
+ "sharded"
+ ],
+ "serverless": "forbid"
+ }
+ ],
+ "initialData": [
+ {
+ "collectionName": "collection0",
+ "databaseName": "database0",
+ "documents": []
+ }
+ ],
+ "tests": [
+ {
+ "description": "clusterTime is present",
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "clusterTime": {
+ "$$exists": true
+ }
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/source/change-streams/tests/unified/change-streams-clusterTime.yml b/source/change-streams/tests/unified/change-streams-clusterTime.yml
new file mode 100644
index 0000000000..b1d9f20e01
--- /dev/null
+++ b/source/change-streams/tests/unified/change-streams-clusterTime.yml
@@ -0,0 +1,41 @@
+description: "change-streams-clusterTime"
+schemaVersion: "1.4"
+createEntities:
+ - client:
+ id: &client0 client0
+ useMultipleMongoses: false
+ - database:
+ id: &database0 database0
+ client: *client0
+ databaseName: *database0
+ - collection:
+ id: &collection0 collection0
+ database: *database0
+ collectionName: *collection0
+
+runOnRequirements:
+ - minServerVersion: "4.0.0"
+ topologies: [ replicaset, load-balanced, sharded ]
+ serverless: forbid
+
+initialData:
+ - collectionName: *collection0
+ databaseName: *database0
+ documents: []
+
+tests:
+ - description: "clusterTime is present"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *collection0
+ arguments:
+ document: { _id: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ ns: { db: *database0, coll: *collection0 }
+ clusterTime: { $$exists: true }
diff --git a/source/change-streams/tests/unified/change-streams-disambiguatedPaths.json b/source/change-streams/tests/unified/change-streams-disambiguatedPaths.json
new file mode 100644
index 0000000000..df7422295a
--- /dev/null
+++ b/source/change-streams/tests/unified/change-streams-disambiguatedPaths.json
@@ -0,0 +1,272 @@
+{
+ "description": "disambiguatedPaths",
+ "schemaVersion": "1.4",
+ "createEntities": [
+ {
+ "client": {
+ "id": "client0",
+ "useMultipleMongoses": false
+ }
+ },
+ {
+ "database": {
+ "id": "database0",
+ "client": "client0",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection0",
+ "database": "database0",
+ "collectionName": "collection0"
+ }
+ }
+ ],
+ "runOnRequirements": [
+ {
+ "minServerVersion": "6.1.0",
+ "topologies": [
+ "replicaset",
+ "load-balanced",
+ "sharded"
+ ],
+ "serverless": "forbid"
+ }
+ ],
+ "initialData": [
+ {
+ "collectionName": "collection0",
+ "databaseName": "database0",
+ "documents": []
+ }
+ ],
+ "tests": [
+ {
+ "description": "disambiguatedPaths is not present when showExpandedEvents is false/unset",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "6.1.0",
+ "maxServerVersion": "8.1.99",
+ "topologies": [
+ "replicaset",
+ "load-balanced",
+ "sharded"
+ ],
+ "serverless": "forbid"
+ },
+ {
+ "minServerVersion": "8.2.1",
+ "topologies": [
+ "replicaset",
+ "load-balanced",
+ "sharded"
+ ],
+ "serverless": "forbid"
+ }
+ ],
+ "operations": [
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "a": {
+ "1": 1
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "updateOne",
+ "object": "collection0",
+ "arguments": {
+ "filter": {
+ "_id": 1
+ },
+ "update": {
+ "$set": {
+ "a.1": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "update",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "updateDescription": {
+ "updatedFields": {
+ "$$exists": true
+ },
+ "removedFields": {
+ "$$exists": true
+ },
+ "truncatedArrays": {
+ "$$exists": true
+ },
+ "disambiguatedPaths": {
+ "$$exists": false
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "description": "disambiguatedPaths is present on updateDescription when an ambiguous path is present",
+ "operations": [
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "a": {
+ "1": 1
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "showExpandedEvents": true
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "updateOne",
+ "object": "collection0",
+ "arguments": {
+ "filter": {
+ "_id": 1
+ },
+ "update": {
+ "$set": {
+ "a.1": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "update",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "updateDescription": {
+ "updatedFields": {
+ "$$exists": true
+ },
+ "removedFields": {
+ "$$exists": true
+ },
+ "truncatedArrays": {
+ "$$exists": true
+ },
+ "disambiguatedPaths": {
+ "a.1": [
+ "a",
+ "1"
+ ]
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "description": "disambiguatedPaths returns array indices as integers",
+ "operations": [
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "a": [
+ {
+ "1": 1
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "showExpandedEvents": true
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "updateOne",
+ "object": "collection0",
+ "arguments": {
+ "filter": {
+ "_id": 1
+ },
+ "update": {
+ "$set": {
+ "a.0.1": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "update",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "updateDescription": {
+ "updatedFields": {
+ "$$exists": true
+ },
+ "removedFields": {
+ "$$exists": true
+ },
+ "truncatedArrays": {
+ "$$exists": true
+ },
+ "disambiguatedPaths": {
+ "a.0.1": [
+ "a",
+ {
+ "$$type": "int"
+ },
+ "1"
+ ]
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/source/change-streams/tests/unified/change-streams-disambiguatedPaths.yml b/source/change-streams/tests/unified/change-streams-disambiguatedPaths.yml
new file mode 100644
index 0000000000..761193eda2
--- /dev/null
+++ b/source/change-streams/tests/unified/change-streams-disambiguatedPaths.yml
@@ -0,0 +1,112 @@
+description: "disambiguatedPaths"
+schemaVersion: "1.4"
+createEntities:
+ - client:
+ id: &client0 client0
+ useMultipleMongoses: false
+ - database:
+ id: &database0 database0
+ client: *client0
+ databaseName: *database0
+ - collection:
+ id: &collection0 collection0
+ database: *database0
+ collectionName: *collection0
+
+runOnRequirements:
+ - minServerVersion: "6.1.0"
+ topologies: [ replicaset, load-balanced, sharded ]
+ serverless: forbid
+
+initialData:
+ - collectionName: *collection0
+ databaseName: *database0
+ documents: []
+
+tests:
+ - description: "disambiguatedPaths is not present when showExpandedEvents is false/unset"
+ # skip server version 8.2.0, which emits disambiguatedPaths unconditionally
+ runOnRequirements:
+ - minServerVersion: "6.1.0"
+ maxServerVersion: "8.1.99"
+ topologies: [ replicaset, load-balanced, sharded ]
+ serverless: forbid
+ - minServerVersion: "8.2.1"
+ topologies: [ replicaset, load-balanced, sharded ]
+ serverless: forbid
+ operations:
+ - name: insertOne
+ object: *collection0
+ arguments:
+ document: { _id: 1, 'a': { '1': 1 } }
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: updateOne
+ object: *collection0
+ arguments:
+ filter: { _id: 1 }
+ update: { $set: { 'a.1': 2 } }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: "update"
+ ns: { db: *database0, coll: *collection0 }
+ updateDescription:
+ updatedFields: { $$exists: true }
+ removedFields: { $$exists: true }
+ truncatedArrays: { $$exists: true }
+ disambiguatedPaths: { $$exists: false }
+
+ - description: "disambiguatedPaths is present on updateDescription when an ambiguous path is present"
+ operations:
+ - name: insertOne
+ object: *collection0
+ arguments:
+ document: { _id: 1, 'a': { '1': 1 } }
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [], showExpandedEvents: true }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: updateOne
+ object: *collection0
+ arguments:
+ filter: { _id: 1 }
+ update: { $set: { 'a.1': 2 } }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: "update"
+ ns: { db: *database0, coll: *collection0 }
+ updateDescription:
+ updatedFields: { $$exists: true }
+ removedFields: { $$exists: true }
+ truncatedArrays: { $$exists: true }
+ disambiguatedPaths: { 'a.1': ['a', '1'] }
+
+ - description: "disambiguatedPaths returns array indices as integers"
+ operations:
+ - name: insertOne
+ object: *collection0
+ arguments:
+ document: { _id: 1, 'a': [{'1': 1 }] }
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [], showExpandedEvents: true }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: updateOne
+ object: *collection0
+ arguments:
+ filter: { _id: 1 }
+ update: { $set: { 'a.0.1': 2 } }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: "update"
+ ns: { db: *database0, coll: *collection0 }
+ updateDescription:
+ updatedFields: { $$exists: true }
+ removedFields: { $$exists: true }
+ truncatedArrays: { $$exists: true }
+ disambiguatedPaths: { 'a.0.1': ['a', { $$type: 'int' }, '1'] }
diff --git a/source/change-streams/tests/unified/change-streams-errors.json b/source/change-streams/tests/unified/change-streams-errors.json
new file mode 100644
index 0000000000..65e99e541e
--- /dev/null
+++ b/source/change-streams/tests/unified/change-streams-errors.json
@@ -0,0 +1,246 @@
+{
+ "description": "change-streams-errors",
+ "schemaVersion": "1.7",
+ "runOnRequirements": [
+ {
+ "serverless": "forbid"
+ }
+ ],
+ "createEntities": [
+ {
+ "client": {
+ "id": "client0",
+ "observeEvents": [
+ "commandStartedEvent"
+ ],
+ "ignoreCommandMonitoringEvents": [
+ "killCursors"
+ ],
+ "useMultipleMongoses": false
+ }
+ },
+ {
+ "client": {
+ "id": "globalClient",
+ "useMultipleMongoses": false
+ }
+ },
+ {
+ "database": {
+ "id": "database0",
+ "client": "client0",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection0",
+ "database": "database0",
+ "collectionName": "collection0"
+ }
+ },
+ {
+ "database": {
+ "id": "globalDatabase0",
+ "client": "globalClient",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "collection": {
+ "id": "globalCollection0",
+ "database": "globalDatabase0",
+ "collectionName": "collection0"
+ }
+ }
+ ],
+ "initialData": [
+ {
+ "collectionName": "collection0",
+ "databaseName": "database0",
+ "documents": []
+ }
+ ],
+ "tests": [
+ {
+ "description": "The watch helper must not throw a custom exception when executed against a single server topology, but instead depend on a server error",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "3.6.0",
+ "topologies": [
+ "single"
+ ]
+ }
+ ],
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "expectError": {
+ "errorCode": 40573
+ }
+ }
+ ]
+ },
+ {
+ "description": "Change Stream should error when an invalid aggregation stage is passed in",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "3.6.0",
+ "topologies": [
+ "replicaset"
+ ]
+ }
+ ],
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [
+ {
+ "$unsupported": "foo"
+ }
+ ]
+ },
+ "expectError": {
+ "errorCode": 40324
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ },
+ {
+ "$unsupported": "foo"
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Change Stream should error when _id is projected out",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.1.11",
+ "topologies": [
+ "replicaset",
+ "sharded",
+ "load-balanced"
+ ]
+ }
+ ],
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [
+ {
+ "$project": {
+ "_id": 0
+ }
+ }
+ ]
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "z": 3
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectError": {
+ "errorCode": 280
+ }
+ }
+ ]
+ },
+ {
+ "description": "change stream errors on ElectionInProgress",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.2",
+ "topologies": [
+ "replicaset",
+ "sharded",
+ "load-balanced"
+ ]
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorCode": 216,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "z": 3
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectError": {
+ "errorCode": 216
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/source/change-streams/tests/unified/change-streams-errors.yml b/source/change-streams/tests/unified/change-streams-errors.yml
new file mode 100644
index 0000000000..85133dae0a
--- /dev/null
+++ b/source/change-streams/tests/unified/change-streams-errors.yml
@@ -0,0 +1,120 @@
+description: "change-streams-errors"
+
+schemaVersion: "1.7"
+
+runOnRequirements:
+ - serverless: forbid
+
+createEntities:
+ - client:
+ id: &client0 client0
+ observeEvents: [ commandStartedEvent ]
+ ignoreCommandMonitoringEvents: [ killCursors ]
+ useMultipleMongoses: false
+ - client:
+ id: &globalClient globalClient
+ useMultipleMongoses: false
+ - database:
+ id: &database0 database0
+ client: *client0
+ databaseName: *database0
+ - collection:
+ id: &collection0 collection0
+ database: *database0
+ collectionName: *collection0
+ - database:
+ id: &globalDatabase0 globalDatabase0
+ client: *globalClient
+ databaseName: *database0
+ - collection:
+ id: &globalCollection0 globalCollection0
+ database: *globalDatabase0
+ collectionName: *collection0
+
+initialData:
+ - collectionName: *collection0
+ databaseName: *database0
+ documents: []
+
+tests:
+ - description: "The watch helper must not throw a custom exception when executed against a single server topology, but instead depend on a server error"
+ runOnRequirements:
+ - minServerVersion: "3.6.0"
+ topologies: [ single ]
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ expectError: { errorCode: 40573 }
+
+ - description: Change Stream should error when an invalid aggregation stage is passed in
+ runOnRequirements:
+ - minServerVersion: "3.6.0"
+ topologies: [ replicaset ]
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: [ { $unsupported: foo } ]
+ expectError: { errorCode: 40324 }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream: {}
+ - $unsupported: foo
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: Change Stream should error when _id is projected out
+ runOnRequirements:
+ - minServerVersion: "4.1.11"
+ topologies: [ replicaset, sharded, load-balanced ]
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline:
+ - $project: { _id: 0 }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { z: 3 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectError: { errorCode: 280 }
+
+ - description: change stream errors on ElectionInProgress
+ runOnRequirements:
+ - minServerVersion: "4.2"
+ topologies: [ replicaset, sharded, load-balanced ]
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ errorCode: 216
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { z: 3 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectError: { errorCode: 216 }
diff --git a/source/change-streams/tests/unified/change-streams-nsType.json b/source/change-streams/tests/unified/change-streams-nsType.json
new file mode 100644
index 0000000000..877ca9c007
--- /dev/null
+++ b/source/change-streams/tests/unified/change-streams-nsType.json
@@ -0,0 +1,104 @@
+{
+ "description": "change-streams-nsType",
+ "schemaVersion": "1.7",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "8.1.0",
+ "topologies": [
+ "replicaset",
+ "sharded"
+ ],
+ "serverless": "forbid"
+ }
+ ],
+ "createEntities": [
+ {
+ "client": {
+ "id": "client0",
+ "useMultipleMongoses": false
+ }
+ },
+ {
+ "database": {
+ "id": "database0",
+ "client": "client0",
+ "databaseName": "database0"
+ }
+ }
+ ],
+ "tests": [
+ {
+ "description": "nsType is present when creating collections",
+ "operations": [
+ {
+ "name": "dropCollection",
+ "object": "database0",
+ "arguments": {
+ "collection": "foo"
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "database0",
+ "arguments": {
+ "pipeline": [],
+ "showExpandedEvents": true
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "createCollection",
+ "object": "database0",
+ "arguments": {
+ "collection": "foo"
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "create",
+ "nsType": "collection"
+ }
+ }
+ ]
+ },
+ {
+ "description": "nsType is present when creating views",
+ "operations": [
+ {
+ "name": "dropCollection",
+ "object": "database0",
+ "arguments": {
+ "collection": "foo"
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "database0",
+ "arguments": {
+ "pipeline": [],
+ "showExpandedEvents": true
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "createCollection",
+ "object": "database0",
+ "arguments": {
+ "collection": "foo",
+ "viewOn": "testName"
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "create",
+ "nsType": "view"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/source/change-streams/tests/unified/change-streams-nsType.yml b/source/change-streams/tests/unified/change-streams-nsType.yml
new file mode 100644
index 0000000000..3471767c52
--- /dev/null
+++ b/source/change-streams/tests/unified/change-streams-nsType.yml
@@ -0,0 +1,60 @@
+description: "change-streams-nsType"
+schemaVersion: "1.7"
+runOnRequirements:
+ - minServerVersion: "8.1.0"
+ topologies: [ replicaset, sharded ]
+ serverless: forbid
+createEntities:
+ - client:
+ id: &client0 client0
+ useMultipleMongoses: false
+ - database:
+ id: &database0 database0
+ client: *client0
+ databaseName: *database0
+
+tests:
+ - description: "nsType is present when creating collections"
+ operations:
+ - name: dropCollection
+ object: *database0
+ arguments:
+ collection: &collection0 foo
+ - name: createChangeStream
+ object: *database0
+ arguments:
+ pipeline: []
+ showExpandedEvents: true
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: createCollection
+ object: *database0
+ arguments:
+ collection: *collection0
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: create
+ nsType: collection
+
+ - description: "nsType is present when creating views"
+ operations:
+ - name: dropCollection
+ object: *database0
+ arguments:
+ collection: &collection0 foo
+ - name: createChangeStream
+ object: *database0
+ arguments:
+ pipeline: []
+ showExpandedEvents: true
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: createCollection
+ object: *database0
+ arguments:
+ collection: *collection0
+ viewOn: testName
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: create
+ nsType: view
\ No newline at end of file
diff --git a/source/change-streams/tests/unified/change-streams-pre_and_post_images.json b/source/change-streams/tests/unified/change-streams-pre_and_post_images.json
new file mode 100644
index 0000000000..e62fc03459
--- /dev/null
+++ b/source/change-streams/tests/unified/change-streams-pre_and_post_images.json
@@ -0,0 +1,827 @@
+{
+ "description": "change-streams-pre_and_post_images",
+ "schemaVersion": "1.4",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "6.0.0",
+ "topologies": [
+ "replicaset",
+ "sharded",
+ "load-balanced"
+ ],
+ "serverless": "forbid"
+ }
+ ],
+ "createEntities": [
+ {
+ "client": {
+ "id": "client0",
+ "observeEvents": [
+ "commandStartedEvent"
+ ],
+ "ignoreCommandMonitoringEvents": [
+ "collMod",
+ "insert",
+ "update",
+ "getMore",
+ "killCursors"
+ ]
+ }
+ },
+ {
+ "database": {
+ "id": "database0",
+ "client": "client0",
+ "databaseName": "change-stream-tests"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection0",
+ "database": "database0",
+ "collectionName": "test"
+ }
+ }
+ ],
+ "initialData": [
+ {
+ "collectionName": "test",
+ "databaseName": "change-stream-tests",
+ "documents": [
+ {
+ "_id": 1
+ }
+ ]
+ }
+ ],
+ "tests": [
+ {
+ "description": "fullDocument:whenAvailable with changeStreamPreAndPostImages enabled",
+ "operations": [
+ {
+ "name": "runCommand",
+ "object": "database0",
+ "arguments": {
+ "commandName": "collMod",
+ "command": {
+ "collMod": "test",
+ "changeStreamPreAndPostImages": {
+ "enabled": true
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "fullDocument": "whenAvailable"
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "updateOne",
+ "object": "collection0",
+ "arguments": {
+ "filter": {
+ "_id": 1
+ },
+ "update": {
+ "$set": {
+ "x": 1
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "update",
+ "ns": {
+ "db": "change-stream-tests",
+ "coll": "test"
+ },
+ "updateDescription": {
+ "$$type": "object"
+ },
+ "fullDocument": {
+ "_id": 1,
+ "x": 1
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "test",
+ "pipeline": [
+ {
+ "$changeStream": {
+ "fullDocument": "whenAvailable"
+ }
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "fullDocument:whenAvailable with changeStreamPreAndPostImages disabled",
+ "operations": [
+ {
+ "name": "runCommand",
+ "object": "database0",
+ "arguments": {
+ "commandName": "collMod",
+ "command": {
+ "collMod": "test",
+ "changeStreamPreAndPostImages": {
+ "enabled": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "fullDocument": "whenAvailable"
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "updateOne",
+ "object": "collection0",
+ "arguments": {
+ "filter": {
+ "_id": 1
+ },
+ "update": {
+ "$set": {
+ "x": 1
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "update",
+ "ns": {
+ "db": "change-stream-tests",
+ "coll": "test"
+ },
+ "updateDescription": {
+ "$$type": "object"
+ },
+ "fullDocument": null
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "test",
+ "pipeline": [
+ {
+ "$changeStream": {
+ "fullDocument": "whenAvailable"
+ }
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "fullDocument:required with changeStreamPreAndPostImages enabled",
+ "operations": [
+ {
+ "name": "runCommand",
+ "object": "database0",
+ "arguments": {
+ "commandName": "collMod",
+ "command": {
+ "collMod": "test",
+ "changeStreamPreAndPostImages": {
+ "enabled": true
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "fullDocument": "required"
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "updateOne",
+ "object": "collection0",
+ "arguments": {
+ "filter": {
+ "_id": 1
+ },
+ "update": {
+ "$set": {
+ "x": 1
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "update",
+ "ns": {
+ "db": "change-stream-tests",
+ "coll": "test"
+ },
+ "updateDescription": {
+ "$$type": "object"
+ },
+ "fullDocument": {
+ "_id": 1,
+ "x": 1
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "test",
+ "pipeline": [
+ {
+ "$changeStream": {
+ "fullDocument": "required"
+ }
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "fullDocument:required with changeStreamPreAndPostImages disabled",
+ "operations": [
+ {
+ "name": "runCommand",
+ "object": "database0",
+ "arguments": {
+ "commandName": "collMod",
+ "command": {
+ "collMod": "test",
+ "changeStreamPreAndPostImages": {
+ "enabled": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "fullDocument": "required"
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "updateOne",
+ "object": "collection0",
+ "arguments": {
+ "filter": {
+ "_id": 1
+ },
+ "update": {
+ "$set": {
+ "x": 1
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectError": {
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "test",
+ "pipeline": [
+ {
+ "$changeStream": {
+ "fullDocument": "required"
+ }
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "fullDocumentBeforeChange:whenAvailable with changeStreamPreAndPostImages enabled",
+ "operations": [
+ {
+ "name": "runCommand",
+ "object": "database0",
+ "arguments": {
+ "commandName": "collMod",
+ "command": {
+ "collMod": "test",
+ "changeStreamPreAndPostImages": {
+ "enabled": true
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "fullDocumentBeforeChange": "whenAvailable"
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "updateOne",
+ "object": "collection0",
+ "arguments": {
+ "filter": {
+ "_id": 1
+ },
+ "update": {
+ "$set": {
+ "x": 1
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "update",
+ "ns": {
+ "db": "change-stream-tests",
+ "coll": "test"
+ },
+ "updateDescription": {
+ "$$type": "object"
+ },
+ "fullDocumentBeforeChange": {
+ "_id": 1
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "test",
+ "pipeline": [
+ {
+ "$changeStream": {
+ "fullDocumentBeforeChange": "whenAvailable"
+ }
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "fullDocumentBeforeChange:whenAvailable with changeStreamPreAndPostImages disabled",
+ "operations": [
+ {
+ "name": "runCommand",
+ "object": "database0",
+ "arguments": {
+ "commandName": "collMod",
+ "command": {
+ "collMod": "test",
+ "changeStreamPreAndPostImages": {
+ "enabled": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "fullDocumentBeforeChange": "whenAvailable"
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "updateOne",
+ "object": "collection0",
+ "arguments": {
+ "filter": {
+ "_id": 1
+ },
+ "update": {
+ "$set": {
+ "x": 1
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "update",
+ "ns": {
+ "db": "change-stream-tests",
+ "coll": "test"
+ },
+ "updateDescription": {
+ "$$type": "object"
+ },
+ "fullDocumentBeforeChange": null
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "test",
+ "pipeline": [
+ {
+ "$changeStream": {
+ "fullDocumentBeforeChange": "whenAvailable"
+ }
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "fullDocumentBeforeChange:required with changeStreamPreAndPostImages enabled",
+ "operations": [
+ {
+ "name": "runCommand",
+ "object": "database0",
+ "arguments": {
+ "commandName": "collMod",
+ "command": {
+ "collMod": "test",
+ "changeStreamPreAndPostImages": {
+ "enabled": true
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "fullDocumentBeforeChange": "required"
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "updateOne",
+ "object": "collection0",
+ "arguments": {
+ "filter": {
+ "_id": 1
+ },
+ "update": {
+ "$set": {
+ "x": 1
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "update",
+ "ns": {
+ "db": "change-stream-tests",
+ "coll": "test"
+ },
+ "updateDescription": {
+ "$$type": "object"
+ },
+ "fullDocumentBeforeChange": {
+ "_id": 1
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "test",
+ "pipeline": [
+ {
+ "$changeStream": {
+ "fullDocumentBeforeChange": "required"
+ }
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "fullDocumentBeforeChange:required with changeStreamPreAndPostImages disabled",
+ "operations": [
+ {
+ "name": "runCommand",
+ "object": "database0",
+ "arguments": {
+ "commandName": "collMod",
+ "command": {
+ "collMod": "test",
+ "changeStreamPreAndPostImages": {
+ "enabled": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "fullDocumentBeforeChange": "required"
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "updateOne",
+ "object": "collection0",
+ "arguments": {
+ "filter": {
+ "_id": 1
+ },
+ "update": {
+ "$set": {
+ "x": 1
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectError": {
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "test",
+ "pipeline": [
+ {
+ "$changeStream": {
+ "fullDocumentBeforeChange": "required"
+ }
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "fullDocumentBeforeChange:off with changeStreamPreAndPostImages enabled",
+ "operations": [
+ {
+ "name": "runCommand",
+ "object": "database0",
+ "arguments": {
+ "commandName": "collMod",
+ "command": {
+ "collMod": "test",
+ "changeStreamPreAndPostImages": {
+ "enabled": true
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "fullDocumentBeforeChange": "off"
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "updateOne",
+ "object": "collection0",
+ "arguments": {
+ "filter": {
+ "_id": 1
+ },
+ "update": {
+ "$set": {
+ "x": 1
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "update",
+ "ns": {
+ "db": "change-stream-tests",
+ "coll": "test"
+ },
+ "updateDescription": {
+ "$$type": "object"
+ },
+ "fullDocumentBeforeChange": {
+ "$$exists": false
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "test",
+ "pipeline": [
+ {
+ "$changeStream": {
+ "fullDocumentBeforeChange": "off"
+ }
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "fullDocumentBeforeChange:off with changeStreamPreAndPostImages disabled",
+ "operations": [
+ {
+ "name": "runCommand",
+ "object": "database0",
+ "arguments": {
+ "commandName": "collMod",
+ "command": {
+ "collMod": "test",
+ "changeStreamPreAndPostImages": {
+ "enabled": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "fullDocumentBeforeChange": "off"
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "updateOne",
+ "object": "collection0",
+ "arguments": {
+ "filter": {
+ "_id": 1
+ },
+ "update": {
+ "$set": {
+ "x": 1
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "update",
+ "ns": {
+ "db": "change-stream-tests",
+ "coll": "test"
+ },
+ "updateDescription": {
+ "$$type": "object"
+ },
+ "fullDocumentBeforeChange": {
+ "$$exists": false
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "test",
+ "pipeline": [
+ {
+ "$changeStream": {
+ "fullDocumentBeforeChange": "off"
+ }
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/source/change-streams/tests/unified/change-streams-pre_and_post_images.yml b/source/change-streams/tests/unified/change-streams-pre_and_post_images.yml
new file mode 100644
index 0000000000..6bc58eaf2d
--- /dev/null
+++ b/source/change-streams/tests/unified/change-streams-pre_and_post_images.yml
@@ -0,0 +1,351 @@
+description: "change-streams-pre_and_post_images"
+
+schemaVersion: "1.4"
+
+runOnRequirements:
+ - minServerVersion: "6.0.0"
+ topologies: [ replicaset, sharded, load-balanced ]
+ serverless: forbid
+
+createEntities:
+ - client:
+ id: &client0 client0
+ observeEvents: [ commandStartedEvent ]
+ ignoreCommandMonitoringEvents: [ collMod, insert, update, getMore, killCursors ]
+ - database:
+ id: &database0 database0
+ client: *client0
+ databaseName: &database0Name change-stream-tests
+ - collection:
+ id: &collection0 collection0
+ database: *database0
+ collectionName: &collection0Name test
+
+initialData:
+ - collectionName: *collection0Name
+ databaseName: *database0Name
+ documents:
+ - { _id: 1 }
+
+tests:
+ - description: "fullDocument:whenAvailable with changeStreamPreAndPostImages enabled"
+ operations:
+ - name: runCommand
+ object: *database0
+ arguments: &enablePreAndPostImages
+ commandName: collMod
+ command:
+ collMod: *collection0Name
+ changeStreamPreAndPostImages: { enabled: true }
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ fullDocument: "whenAvailable"
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: updateOne
+ object: *collection0
+ arguments:
+ filter: { _id: 1 }
+ update: { $set: { x: 1 }}
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: "update"
+ ns: { db: *database0Name, coll: *collection0Name }
+ updateDescription: { $$type: "object" }
+ fullDocument: { _id: 1, x: 1 }
+ expectEvents:
+ - client: *client0
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0Name
+ pipeline:
+ - $changeStream: { fullDocument: "whenAvailable" }
+
+ - description: "fullDocument:whenAvailable with changeStreamPreAndPostImages disabled"
+ operations:
+ - name: runCommand
+ object: *database0
+ arguments: &disablePreAndPostImages
+ commandName: collMod
+ command:
+ collMod: *collection0Name
+ changeStreamPreAndPostImages: { enabled: false }
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ fullDocument: "whenAvailable"
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: updateOne
+ object: *collection0
+ arguments:
+ filter: { _id: 1 }
+ update: { $set: { x: 1 }}
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: "update"
+ ns: { db: *database0Name, coll: *collection0Name }
+ updateDescription: { $$type: "object" }
+ fullDocument: null
+ expectEvents:
+ - client: *client0
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0Name
+ pipeline:
+ - $changeStream: { fullDocument: "whenAvailable" }
+
+ - description: "fullDocument:required with changeStreamPreAndPostImages enabled"
+ operations:
+ - name: runCommand
+ object: *database0
+ arguments: *enablePreAndPostImages
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ fullDocument: "required"
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: updateOne
+ object: *collection0
+ arguments:
+ filter: { _id: 1 }
+ update: { $set: { x: 1 }}
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: "update"
+ ns: { db: *database0Name, coll: *collection0Name }
+ updateDescription: { $$type: "object" }
+ fullDocument: { _id: 1, x: 1 }
+ expectEvents:
+ - client: *client0
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0Name
+ pipeline:
+ - $changeStream: { fullDocument: "required" }
+
+ - description: "fullDocument:required with changeStreamPreAndPostImages disabled"
+ operations:
+ - name: runCommand
+ object: *database0
+ arguments: *disablePreAndPostImages
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ fullDocument: "required"
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: updateOne
+ object: *collection0
+ arguments:
+ filter: { _id: 1 }
+ update: { $set: { x: 1 }}
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectError:
+ isClientError: false
+ expectEvents:
+ - client: *client0
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0Name
+ pipeline:
+ - $changeStream: { fullDocument: "required" }
+
+ - description: "fullDocumentBeforeChange:whenAvailable with changeStreamPreAndPostImages enabled"
+ operations:
+ - name: runCommand
+ object: *database0
+ arguments: *enablePreAndPostImages
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ fullDocumentBeforeChange: "whenAvailable"
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: updateOne
+ object: *collection0
+ arguments:
+ filter: { _id: 1 }
+ update: { $set: { x: 1 }}
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: "update"
+ ns: { db: *database0Name, coll: *collection0Name }
+ updateDescription: { $$type: "object" }
+ fullDocumentBeforeChange: { _id: 1 }
+ expectEvents:
+ - client: *client0
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0Name
+ pipeline:
+ - $changeStream: { fullDocumentBeforeChange: "whenAvailable" }
+
+ - description: "fullDocumentBeforeChange:whenAvailable with changeStreamPreAndPostImages disabled"
+ operations:
+ - name: runCommand
+ object: *database0
+ arguments: *disablePreAndPostImages
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ fullDocumentBeforeChange: "whenAvailable"
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: updateOne
+ object: *collection0
+ arguments:
+ filter: { _id: 1 }
+ update: { $set: { x: 1 }}
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: "update"
+ ns: { db: *database0Name, coll: *collection0Name }
+ updateDescription: { $$type: "object" }
+ fullDocumentBeforeChange: null
+ expectEvents:
+ - client: *client0
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0Name
+ pipeline:
+ - $changeStream: { fullDocumentBeforeChange: "whenAvailable" }
+
+ - description: "fullDocumentBeforeChange:required with changeStreamPreAndPostImages enabled"
+ operations:
+ - name: runCommand
+ object: *database0
+ arguments: *enablePreAndPostImages
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ fullDocumentBeforeChange: "required"
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: updateOne
+ object: *collection0
+ arguments:
+ filter: { _id: 1 }
+ update: { $set: { x: 1 }}
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: "update"
+ ns: { db: *database0Name, coll: *collection0Name }
+ updateDescription: { $$type: "object" }
+ fullDocumentBeforeChange: { _id: 1 }
+ expectEvents:
+ - client: *client0
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0Name
+ pipeline:
+ - $changeStream: { fullDocumentBeforeChange: "required" }
+
+ - description: "fullDocumentBeforeChange:required with changeStreamPreAndPostImages disabled"
+ operations:
+ - name: runCommand
+ object: *database0
+ arguments: *disablePreAndPostImages
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ fullDocumentBeforeChange: "required"
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: updateOne
+ object: *collection0
+ arguments:
+ filter: { _id: 1 }
+ update: { $set: { x: 1 }}
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectError:
+ isClientError: false
+ expectEvents:
+ - client: *client0
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0Name
+ pipeline:
+ - $changeStream: { fullDocumentBeforeChange: "required" }
+
+ - description: "fullDocumentBeforeChange:off with changeStreamPreAndPostImages enabled"
+ operations:
+ - name: runCommand
+ object: *database0
+ arguments: *enablePreAndPostImages
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ fullDocumentBeforeChange: "off"
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: updateOne
+ object: *collection0
+ arguments:
+ filter: { _id: 1 }
+ update: { $set: { x: 1 }}
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: "update"
+ ns: { db: *database0Name, coll: *collection0Name }
+ updateDescription: { $$type: "object" }
+ fullDocumentBeforeChange: { $$exists: false }
+ expectEvents:
+ - client: *client0
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0Name
+ pipeline:
+ - $changeStream: { fullDocumentBeforeChange: "off" }
+
+ - description: "fullDocumentBeforeChange:off with changeStreamPreAndPostImages disabled"
+ operations:
+ - name: runCommand
+ object: *database0
+ arguments: *disablePreAndPostImages
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ fullDocumentBeforeChange: "off"
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: updateOne
+ object: *collection0
+ arguments:
+ filter: { _id: 1 }
+ update: { $set: { x: 1 }}
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: "update"
+ ns: { db: *database0Name, coll: *collection0Name }
+ updateDescription: { $$type: "object" }
+ fullDocumentBeforeChange: { $$exists: false }
+ expectEvents:
+ - client: *client0
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0Name
+ pipeline:
+ - $changeStream: { fullDocumentBeforeChange: "off" }
diff --git a/source/change-streams/tests/unified/change-streams-resume-allowlist.json b/source/change-streams/tests/unified/change-streams-resume-allowlist.json
new file mode 100644
index 0000000000..1ec72b432b
--- /dev/null
+++ b/source/change-streams/tests/unified/change-streams-resume-allowlist.json
@@ -0,0 +1,2348 @@
+{
+ "description": "change-streams-resume-allowlist",
+ "schemaVersion": "1.7",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "3.6",
+ "topologies": [
+ "replicaset",
+ "sharded",
+ "load-balanced"
+ ],
+ "serverless": "forbid"
+ }
+ ],
+ "createEntities": [
+ {
+ "client": {
+ "id": "client0",
+ "observeEvents": [
+ "commandStartedEvent"
+ ],
+ "ignoreCommandMonitoringEvents": [
+ "killCursors"
+ ],
+ "useMultipleMongoses": false
+ }
+ },
+ {
+ "client": {
+ "id": "globalClient",
+ "useMultipleMongoses": false
+ }
+ },
+ {
+ "database": {
+ "id": "database0",
+ "client": "client0",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection0",
+ "database": "database0",
+ "collectionName": "collection0"
+ }
+ },
+ {
+ "database": {
+ "id": "globalDatabase0",
+ "client": "globalClient",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "collection": {
+ "id": "globalCollection0",
+ "database": "globalDatabase0",
+ "collectionName": "collection0"
+ }
+ }
+ ],
+ "tests": [
+ {
+ "description": "change stream resumes after a network error",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.2"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "closeConnection": true
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after HostUnreachable",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.2",
+ "maxServerVersion": "4.2.99"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorCode": 6,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after HostNotFound",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.2",
+ "maxServerVersion": "4.2.99"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorCode": 7,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after NetworkTimeout",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.2",
+ "maxServerVersion": "4.2.99"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorCode": 89,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after ShutdownInProgress",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.2",
+ "maxServerVersion": "4.2.99"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorCode": 91,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after PrimarySteppedDown",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.2",
+ "maxServerVersion": "4.2.99"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorCode": 189,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after ExceededTimeLimit",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.2",
+ "maxServerVersion": "4.2.99"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorCode": 262,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after SocketException",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.2",
+ "maxServerVersion": "4.2.99"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorCode": 9001,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after NotWritablePrimary",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.2",
+ "maxServerVersion": "4.2.99"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorCode": 10107,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after InterruptedAtShutdown",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.2",
+ "maxServerVersion": "4.2.99"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorCode": 11600,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after InterruptedDueToReplStateChange",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.2",
+ "maxServerVersion": "4.2.99"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorCode": 11602,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after NotPrimaryNoSecondaryOk",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.2",
+ "maxServerVersion": "4.2.99"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorCode": 13435,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after NotPrimaryOrSecondary",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.2",
+ "maxServerVersion": "4.2.99"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorCode": 13436,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after StaleShardVersion",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.2",
+ "maxServerVersion": "4.2.99"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorCode": 63,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after StaleEpoch",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.2",
+ "maxServerVersion": "4.2.99"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorCode": 150,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after RetryChangeStream",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.2",
+ "maxServerVersion": "4.2.99"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorCode": 234,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after FailedToSatisfyReadPreference",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.2",
+ "maxServerVersion": "4.2.99"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorCode": 133,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after CursorNotFound",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.2"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorCode": 43,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/source/change-streams/tests/unified/change-streams-resume-allowlist.yml b/source/change-streams/tests/unified/change-streams-resume-allowlist.yml
new file mode 100644
index 0000000000..c5b7a874d3
--- /dev/null
+++ b/source/change-streams/tests/unified/change-streams-resume-allowlist.yml
@@ -0,0 +1,1169 @@
+# Tests for resume behavior on server versions that do not support the ResumableChangeStreamError label
+description: "change-streams-resume-allowlist"
+
+schemaVersion: "1.7"
+
+runOnRequirements:
+ - minServerVersion: "3.6"
+ topologies: [ replicaset, sharded, load-balanced ]
+ serverless: forbid
+
+createEntities:
+ - client:
+ id: &client0 client0
+ observeEvents: [ commandStartedEvent ]
+ ignoreCommandMonitoringEvents: [ killCursors ]
+ useMultipleMongoses: false
+ - client:
+ id: &globalClient globalClient
+ useMultipleMongoses: false
+ - database:
+ id: &database0 database0
+ client: *client0
+ databaseName: *database0
+ - collection:
+ id: &collection0 collection0
+ database: *database0
+ collectionName: *collection0
+ - database:
+ id: &globalDatabase0 globalDatabase0
+ client: *globalClient
+ databaseName: *database0
+ - collection:
+ id: &globalCollection0 globalCollection0
+ database: *globalDatabase0
+ collectionName: *collection0
+
+tests:
+ - description: change stream resumes after a network error
+ runOnRequirements:
+ - minServerVersion: "4.2"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ closeConnection: true
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after HostUnreachable
+ runOnRequirements:
+ - minServerVersion: "4.2"
+ maxServerVersion: "4.2.99"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ errorCode: 6
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after HostNotFound
+ runOnRequirements:
+ - minServerVersion: "4.2"
+ maxServerVersion: "4.2.99"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ errorCode: 7
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after NetworkTimeout
+ runOnRequirements:
+ - minServerVersion: "4.2"
+ maxServerVersion: "4.2.99"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ errorCode: 89
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after ShutdownInProgress
+ runOnRequirements:
+ - minServerVersion: "4.2"
+ maxServerVersion: "4.2.99"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ errorCode: 91
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after PrimarySteppedDown
+ runOnRequirements:
+ - minServerVersion: "4.2"
+ maxServerVersion: "4.2.99"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ errorCode: 189
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after ExceededTimeLimit
+ runOnRequirements:
+ - minServerVersion: "4.2"
+ maxServerVersion: "4.2.99"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ errorCode: 262
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after SocketException
+ runOnRequirements:
+ - minServerVersion: "4.2"
+ maxServerVersion: "4.2.99"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ errorCode: 9001
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after NotWritablePrimary
+ runOnRequirements:
+ - minServerVersion: "4.2"
+ maxServerVersion: "4.2.99"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ errorCode: 10107
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after InterruptedAtShutdown
+ runOnRequirements:
+ - minServerVersion: "4.2"
+ maxServerVersion: "4.2.99"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ errorCode: 11600
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after InterruptedDueToReplStateChange
+ runOnRequirements:
+ - minServerVersion: "4.2"
+ maxServerVersion: "4.2.99"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ errorCode: 11602
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after NotPrimaryNoSecondaryOk
+ runOnRequirements:
+ - minServerVersion: "4.2"
+ maxServerVersion: "4.2.99"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ errorCode: 13435
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after NotPrimaryOrSecondary
+ runOnRequirements:
+ - minServerVersion: "4.2"
+ maxServerVersion: "4.2.99"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ errorCode: 13436
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after StaleShardVersion
+ runOnRequirements:
+ - minServerVersion: "4.2"
+ maxServerVersion: "4.2.99"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ errorCode: 63
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after StaleEpoch
+ runOnRequirements:
+ - minServerVersion: "4.2"
+ maxServerVersion: "4.2.99"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ errorCode: 150
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after RetryChangeStream
+ runOnRequirements:
+ - minServerVersion: "4.2"
+ maxServerVersion: "4.2.99"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ errorCode: 234
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after FailedToSatisfyReadPreference
+ runOnRequirements:
+ - minServerVersion: "4.2"
+ maxServerVersion: "4.2.99"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ errorCode: 133
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ # CursorNotFound is special-cased to be resumable regardless of server versions or error labels, so this test has
+ # no maxWireVersion.
+ - description: change stream resumes after CursorNotFound
+ runOnRequirements:
+ - minServerVersion: "4.2"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ errorCode: 43
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
diff --git a/source/change-streams/tests/unified/change-streams-resume-errorLabels.json b/source/change-streams/tests/unified/change-streams-resume-errorLabels.json
new file mode 100644
index 0000000000..7fd70108f0
--- /dev/null
+++ b/source/change-streams/tests/unified/change-streams-resume-errorLabels.json
@@ -0,0 +1,2130 @@
+{
+ "description": "change-streams-resume-errorlabels",
+ "schemaVersion": "1.7",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.3.1",
+ "topologies": [
+ "replicaset",
+ "sharded",
+ "load-balanced"
+ ],
+ "serverless": "forbid"
+ }
+ ],
+ "createEntities": [
+ {
+ "client": {
+ "id": "client0",
+ "observeEvents": [
+ "commandStartedEvent"
+ ],
+ "ignoreCommandMonitoringEvents": [
+ "killCursors"
+ ],
+ "useMultipleMongoses": false
+ }
+ },
+ {
+ "client": {
+ "id": "globalClient",
+ "useMultipleMongoses": false
+ }
+ },
+ {
+ "database": {
+ "id": "database0",
+ "client": "client0",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection0",
+ "database": "database0",
+ "collectionName": "collection0"
+ }
+ },
+ {
+ "database": {
+ "id": "globalDatabase0",
+ "client": "globalClient",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "collection": {
+ "id": "globalCollection0",
+ "database": "globalDatabase0",
+ "collectionName": "collection0"
+ }
+ }
+ ],
+ "tests": [
+ {
+ "description": "change stream resumes after HostUnreachable",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failGetMoreAfterCursorCheckout",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "errorCode": 6,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after HostNotFound",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failGetMoreAfterCursorCheckout",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "errorCode": 7,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after NetworkTimeout",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failGetMoreAfterCursorCheckout",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "errorCode": 89,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after ShutdownInProgress",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failGetMoreAfterCursorCheckout",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "errorCode": 91,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after PrimarySteppedDown",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failGetMoreAfterCursorCheckout",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "errorCode": 189,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after ExceededTimeLimit",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failGetMoreAfterCursorCheckout",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "errorCode": 262,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after SocketException",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failGetMoreAfterCursorCheckout",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "errorCode": 9001,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after NotWritablePrimary",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failGetMoreAfterCursorCheckout",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "errorCode": 10107,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after InterruptedAtShutdown",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failGetMoreAfterCursorCheckout",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "errorCode": 11600,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after InterruptedDueToReplStateChange",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failGetMoreAfterCursorCheckout",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "errorCode": 11602,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after NotPrimaryNoSecondaryOk",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failGetMoreAfterCursorCheckout",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "errorCode": 13435,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after NotPrimaryOrSecondary",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failGetMoreAfterCursorCheckout",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "errorCode": 13436,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after StaleShardVersion",
+ "runOnRequirements": [
+ {
+ "maxServerVersion": "6.0.99"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failGetMoreAfterCursorCheckout",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "errorCode": 63,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after StaleEpoch",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failGetMoreAfterCursorCheckout",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "errorCode": 150,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after RetryChangeStream",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failGetMoreAfterCursorCheckout",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "errorCode": 234,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes after FailedToSatisfyReadPreference",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failGetMoreAfterCursorCheckout",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "errorCode": 133,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream resumes if error contains ResumableChangeStreamError",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorCode": 50,
+ "closeConnection": false,
+ "errorLabels": [
+ "ResumableChangeStreamError"
+ ]
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$exists": true
+ },
+ "collection": "collection0"
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "resumeAfter": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "change stream does not resume if error does not contain ResumableChangeStreamError",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorCode": 6,
+ "closeConnection": false
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectError": {
+ "errorCode": 6
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/source/change-streams/tests/unified/change-streams-resume-errorLabels.yml b/source/change-streams/tests/unified/change-streams-resume-errorLabels.yml
new file mode 100644
index 0000000000..5cc6d423a4
--- /dev/null
+++ b/source/change-streams/tests/unified/change-streams-resume-errorLabels.yml
@@ -0,0 +1,1069 @@
+# Tests for resume behavior on server versions that support the ResumableChangeStreamError label
+description: "change-streams-resume-errorlabels"
+
+schemaVersion: "1.7"
+
+runOnRequirements:
+ - minServerVersion: "4.3.1"
+ topologies: [ replicaset, sharded, load-balanced ]
+ serverless: forbid
+
+createEntities:
+ - client:
+ id: &client0 client0
+ observeEvents: [ commandStartedEvent ]
+ ignoreCommandMonitoringEvents: [ killCursors ]
+ useMultipleMongoses: false
+ - client:
+ id: &globalClient globalClient
+ useMultipleMongoses: false
+ - database:
+ id: &database0 database0
+ client: *client0
+ databaseName: *database0
+ - collection:
+ id: &collection0 collection0
+ database: *database0
+ collectionName: *collection0
+ - database:
+ id: &globalDatabase0 globalDatabase0
+ client: *globalClient
+ databaseName: *database0
+ - collection:
+ id: &globalCollection0 globalCollection0
+ database: *globalDatabase0
+ collectionName: *collection0
+
+tests:
+ - description: change stream resumes after HostUnreachable
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failGetMoreAfterCursorCheckout # SERVER-46091 explains why a new failpoint was needed
+ mode: { times: 1 }
+ data:
+ errorCode: 6
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after HostNotFound
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failGetMoreAfterCursorCheckout
+ mode: { times: 1 }
+ data:
+ errorCode: 7
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after NetworkTimeout
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failGetMoreAfterCursorCheckout
+ mode: { times: 1 }
+ data:
+ errorCode: 89
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+
+ - description: change stream resumes after ShutdownInProgress
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failGetMoreAfterCursorCheckout
+ mode: { times: 1 }
+ data:
+ errorCode: 91
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after PrimarySteppedDown
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failGetMoreAfterCursorCheckout
+ mode: { times: 1 }
+ data:
+ errorCode: 189
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after ExceededTimeLimit
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failGetMoreAfterCursorCheckout
+ mode: { times: 1 }
+ data:
+ errorCode: 262
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after SocketException
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failGetMoreAfterCursorCheckout
+ mode: { times: 1 }
+ data:
+ errorCode: 9001
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after NotWritablePrimary
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failGetMoreAfterCursorCheckout
+ mode: { times: 1 }
+ data:
+ errorCode: 10107
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after InterruptedAtShutdown
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failGetMoreAfterCursorCheckout
+ mode: { times: 1 }
+ data:
+ errorCode: 11600
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after InterruptedDueToReplStateChange
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failGetMoreAfterCursorCheckout
+ mode: { times: 1 }
+ data:
+ errorCode: 11602
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after NotPrimaryNoSecondaryOk
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failGetMoreAfterCursorCheckout
+ mode: { times: 1 }
+ data:
+ errorCode: 13435
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after NotPrimaryOrSecondary
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failGetMoreAfterCursorCheckout
+ mode: { times: 1 }
+ data:
+ errorCode: 13436
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after StaleShardVersion
+ runOnRequirements:
+ # StaleShardVersion is obsolete as of 6.1 and is no longer marked as resumable.
+ - maxServerVersion: "6.0.99"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failGetMoreAfterCursorCheckout
+ mode: { times: 1 }
+ data:
+ errorCode: 63
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after StaleEpoch
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failGetMoreAfterCursorCheckout
+ mode: { times: 1 }
+ data:
+ errorCode: 150
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after RetryChangeStream
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failGetMoreAfterCursorCheckout
+ mode: { times: 1 }
+ data:
+ errorCode: 234
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream resumes after FailedToSatisfyReadPreference
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failGetMoreAfterCursorCheckout
+ mode: { times: 1 }
+ data:
+ errorCode: 133
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ # The next two tests ensure that the driver only uses the error label, not the allow list.
+ - description: change stream resumes if error contains ResumableChangeStreamError
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ errorCode: 50 # Use an error code that does not have the allow list label by default
+ closeConnection: false
+ errorLabels: [ ResumableChangeStreamError ]
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ getMore: { $$exists: true }
+ collection: *collection0
+ commandName: getMore
+ databaseName: *database0
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ resumeAfter: { $$unsetOrMatches: { $$exists: true } }
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: change stream does not resume if error does not contain ResumableChangeStreamError
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand # failCommand will not add the allow list error label
+ mode: { times: 1 }
+ data:
+ failCommands: [ getMore ]
+ errorCode: 6 # Use an error code that is on the allow list
+ closeConnection: false
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectError: { errorCode: 6 }
diff --git a/source/change-streams/tests/unified/change-streams-showExpandedEvents.json b/source/change-streams/tests/unified/change-streams-showExpandedEvents.json
new file mode 100644
index 0000000000..b9594e0c1e
--- /dev/null
+++ b/source/change-streams/tests/unified/change-streams-showExpandedEvents.json
@@ -0,0 +1,516 @@
+{
+ "description": "change-streams-showExpandedEvents",
+ "schemaVersion": "1.7",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "6.0.0",
+ "topologies": [
+ "replicaset",
+ "sharded"
+ ],
+ "serverless": "forbid"
+ }
+ ],
+ "createEntities": [
+ {
+ "client": {
+ "id": "client0",
+ "observeEvents": [
+ "commandStartedEvent"
+ ],
+ "ignoreCommandMonitoringEvents": [
+ "killCursors"
+ ],
+ "useMultipleMongoses": false
+ }
+ },
+ {
+ "database": {
+ "id": "database0",
+ "client": "client0",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection0",
+ "database": "database0",
+ "collectionName": "collection0"
+ }
+ },
+ {
+ "database": {
+ "id": "database1",
+ "client": "client0",
+ "databaseName": "database1"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection1",
+ "database": "database1",
+ "collectionName": "collection1"
+ }
+ },
+ {
+ "database": {
+ "id": "shardedDb",
+ "client": "client0",
+ "databaseName": "shardedDb"
+ }
+ },
+ {
+ "database": {
+ "id": "adminDb",
+ "client": "client0",
+ "databaseName": "admin"
+ }
+ },
+ {
+ "collection": {
+ "id": "shardedCollection",
+ "database": "shardedDb",
+ "collectionName": "shardedCollection"
+ }
+ }
+ ],
+ "initialData": [
+ {
+ "collectionName": "collection0",
+ "databaseName": "database0",
+ "documents": []
+ }
+ ],
+ "tests": [
+ {
+ "description": "when provided, showExpandedEvents is sent as a part of the aggregate command",
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "showExpandedEvents": true
+ },
+ "saveResultAsEntity": "changeStream0"
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "showExpandedEvents": true
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "when omitted, showExpandedEvents is not sent as a part of the aggregate command",
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "showExpandedEvents": {
+ "$$exists": false
+ }
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "when showExpandedEvents is true, new fields on change stream events are handled appropriately",
+ "operations": [
+ {
+ "name": "dropCollection",
+ "object": "database0",
+ "arguments": {
+ "collection": "foo"
+ }
+ },
+ {
+ "name": "createCollection",
+ "object": "database0",
+ "arguments": {
+ "collection": "foo"
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "showExpandedEvents": true
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "a": 1
+ }
+ }
+ },
+ {
+ "name": "createIndex",
+ "object": "collection0",
+ "arguments": {
+ "keys": {
+ "x": 1
+ },
+ "name": "x_1"
+ }
+ },
+ {
+ "name": "rename",
+ "object": "collection0",
+ "arguments": {
+ "to": "foo",
+ "dropTarget": true
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "collectionUUID": {
+ "$$exists": true
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "createIndexes",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "operationDescription": {
+ "$$exists": true
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "rename",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "to": {
+ "db": "database0",
+ "coll": "foo"
+ },
+ "operationDescription": {
+ "dropTarget": {
+ "$$exists": true
+ },
+ "to": {
+ "db": "database0",
+ "coll": "foo"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "description": "when showExpandedEvents is true, createIndex events are reported",
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [
+ {
+ "$match": {
+ "operationType": {
+ "$ne": "create"
+ }
+ }
+ }
+ ],
+ "showExpandedEvents": true
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "createIndex",
+ "object": "collection0",
+ "arguments": {
+ "keys": {
+ "x": 1
+ },
+ "name": "x_1"
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "createIndexes"
+ }
+ }
+ ]
+ },
+ {
+ "description": "when showExpandedEvents is true, dropIndexes events are reported",
+ "operations": [
+ {
+ "name": "createIndex",
+ "object": "collection0",
+ "arguments": {
+ "keys": {
+ "x": 1
+ },
+ "name": "x_1"
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "showExpandedEvents": true
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "dropIndex",
+ "object": "collection0",
+ "arguments": {
+ "name": "x_1"
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "dropIndexes"
+ }
+ }
+ ]
+ },
+ {
+ "description": "when showExpandedEvents is true, create events are reported",
+ "operations": [
+ {
+ "name": "dropCollection",
+ "object": "database0",
+ "arguments": {
+ "collection": "foo"
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "database0",
+ "arguments": {
+ "pipeline": [],
+ "showExpandedEvents": true
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "createCollection",
+ "object": "database0",
+ "arguments": {
+ "collection": "foo"
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "create"
+ }
+ }
+ ]
+ },
+ {
+ "description": "when showExpandedEvents is true, create events on views are reported",
+ "operations": [
+ {
+ "name": "dropCollection",
+ "object": "database0",
+ "arguments": {
+ "collection": "foo"
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "database0",
+ "arguments": {
+ "pipeline": [],
+ "showExpandedEvents": true
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "createCollection",
+ "object": "database0",
+ "arguments": {
+ "collection": "foo",
+ "viewOn": "testName"
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "create"
+ }
+ }
+ ]
+ },
+ {
+ "description": "when showExpandedEvents is true, modify events are reported",
+ "operations": [
+ {
+ "name": "createIndex",
+ "object": "collection0",
+ "arguments": {
+ "keys": {
+ "x": 1
+ },
+ "name": "x_2"
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "showExpandedEvents": true
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "runCommand",
+ "object": "database0",
+ "arguments": {
+ "command": {
+ "collMod": "collection0"
+ },
+ "commandName": "collMod"
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "modify"
+ }
+ }
+ ]
+ },
+ {
+ "description": "when showExpandedEvents is true, shardCollection events are reported",
+ "runOnRequirements": [
+ {
+ "topologies": [
+ "sharded"
+ ]
+ }
+ ],
+ "operations": [
+ {
+ "name": "dropCollection",
+ "object": "shardedDb",
+ "arguments": {
+ "collection": "shardedCollection"
+ }
+ },
+ {
+ "name": "createCollection",
+ "object": "shardedDb",
+ "arguments": {
+ "collection": "shardedCollection"
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "shardedCollection",
+ "arguments": {
+ "pipeline": [],
+ "showExpandedEvents": true
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "runCommand",
+ "object": "adminDb",
+ "arguments": {
+ "command": {
+ "shardCollection": "shardedDb.shardedCollection",
+ "key": {
+ "_id": 1
+ }
+ },
+ "commandName": "shardCollection"
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "shardCollection"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/source/change-streams/tests/unified/change-streams-showExpandedEvents.yml b/source/change-streams/tests/unified/change-streams-showExpandedEvents.yml
new file mode 100644
index 0000000000..e6289047bf
--- /dev/null
+++ b/source/change-streams/tests/unified/change-streams-showExpandedEvents.yml
@@ -0,0 +1,307 @@
+description: "change-streams-showExpandedEvents"
+schemaVersion: "1.7"
+runOnRequirements:
+ - minServerVersion: "6.0.0"
+ topologies: [ replicaset, sharded ]
+ serverless: forbid
+createEntities:
+ - client:
+ id: &client0 client0
+ observeEvents: [ commandStartedEvent ]
+ ignoreCommandMonitoringEvents: [ killCursors ]
+ useMultipleMongoses: false
+ - database:
+ id: &database0 database0
+ client: *client0
+ databaseName: *database0
+ - collection:
+ id: &collection0 collection0
+ database: *database0
+ collectionName: *collection0
+ - database:
+ id: &database1 database1
+ client: *client0
+ databaseName: *database1
+ - collection:
+ id: &collection1 collection1
+ database: *database1
+ collectionName: *collection1
+ - database:
+ id: &shardedDb shardedDb
+ client: *client0
+ databaseName: *shardedDb
+ - database:
+ id: &adminDb adminDb
+ client: *client0
+ databaseName: admin
+ - collection:
+ id: &shardedCollection shardedCollection
+ database: *shardedDb
+ collectionName: *shardedCollection
+
+initialData:
+ - collectionName: *collection0
+ databaseName: *database0
+ documents: []
+
+tests:
+ - description: "when provided, showExpandedEvents is sent as a part of the aggregate command"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ showExpandedEvents: true
+ saveResultAsEntity: &changeStream0 changeStream0
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ showExpandedEvents: true
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: "when omitted, showExpandedEvents is not sent as a part of the aggregate command"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ saveResultAsEntity: &changeStream0 changeStream0
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream:
+ showExpandedEvents:
+ $$exists: false
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: "when showExpandedEvents is true, new fields on change stream events are handled appropriately"
+ operations:
+ - name: dropCollection
+ object: *database0
+ arguments:
+ collection: &existing-collection foo
+ - name: createCollection
+ object: *database0
+ arguments:
+ collection: *existing-collection
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ showExpandedEvents: true
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *collection0
+ arguments:
+ document:
+ a: 1
+ - name: createIndex
+ object: *collection0
+ arguments:
+ keys:
+ x: 1
+ name: x_1
+ - name: rename
+ object: *collection0
+ arguments:
+ to: *existing-collection
+ dropTarget: true
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ collectionUUID:
+ $$exists: true
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: createIndexes
+ ns:
+ db: *database0
+ coll: *collection0
+ operationDescription:
+ $$exists: true
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: rename
+ ns:
+ db: *database0
+ coll: *collection0
+ to:
+ db: *database0
+ coll: *existing-collection
+ operationDescription:
+ dropTarget:
+ $$exists: true
+ to:
+ db: *database0
+ coll: *existing-collection
+
+ - description: "when showExpandedEvents is true, createIndex events are reported"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline:
+ # On sharded clusters, the create command run when loading initial
+ # data sometimes is still reported in the change stream. To avoid
+ # this, we exclude the create command when creating the change
+ # stream, but specifically don't exclude other events to still catch
+ # driver errors.
+ - $match:
+ operationType:
+ $ne: create
+ showExpandedEvents: true
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: createIndex
+ object: *collection0
+ arguments:
+ keys:
+ x: 1
+ name: x_1
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: createIndexes
+
+ - description: "when showExpandedEvents is true, dropIndexes events are reported"
+ operations:
+ - name: createIndex
+ object: *collection0
+ arguments:
+ keys:
+ x: 1
+ name: &index1 x_1
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ showExpandedEvents: true
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: dropIndex
+ object: *collection0
+ arguments:
+ name: *index1
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: dropIndexes
+
+ - description: "when showExpandedEvents is true, create events are reported"
+ operations:
+ - name: dropCollection
+ object: *database0
+ arguments:
+ collection: &collection1 foo
+ - name: createChangeStream
+ object: *database0
+ arguments:
+ pipeline: []
+ showExpandedEvents: true
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: createCollection
+ object: *database0
+ arguments:
+ collection: *collection1
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: create
+
+ - description: "when showExpandedEvents is true, create events on views are reported"
+ operations:
+ - name: dropCollection
+ object: *database0
+ arguments:
+ collection: &collection1 foo
+ - name: createChangeStream
+ object: *database0
+ arguments:
+ pipeline: []
+ showExpandedEvents: true
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: createCollection
+ object: *database0
+ arguments:
+ collection: *collection1
+ viewOn: testName
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: create
+
+ - description: "when showExpandedEvents is true, modify events are reported"
+ operations:
+ - name: createIndex
+ object: *collection0
+ arguments:
+ keys:
+ x: 1
+ name: &index2 x_2
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ showExpandedEvents: true
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: runCommand
+ object: *database0
+ arguments:
+ command:
+ collMod: *collection0
+ commandName: collMod
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: modify
+
+ - description: "when showExpandedEvents is true, shardCollection events are reported"
+ runOnRequirements:
+ # Note: minServerVersion is specified in top-level runOnRequirements
+ - topologies: [ sharded ]
+ operations:
+ - name: dropCollection
+ object: *shardedDb
+ arguments:
+ collection: *shardedCollection
+ - name: createCollection
+ object: *shardedDb
+ arguments:
+ collection: *shardedCollection
+ - name: createChangeStream
+ object: *shardedCollection
+ arguments:
+ pipeline: []
+ showExpandedEvents: true
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: runCommand
+ object: *adminDb
+ arguments:
+ command:
+ shardCollection: shardedDb.shardedCollection
+ key:
+ _id: 1
+ commandName: shardCollection
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: shardCollection
diff --git a/source/change-streams/tests/unified/change-streams.json b/source/change-streams/tests/unified/change-streams.json
new file mode 100644
index 0000000000..a155d85b6e
--- /dev/null
+++ b/source/change-streams/tests/unified/change-streams.json
@@ -0,0 +1,1805 @@
+{
+ "description": "change-streams",
+ "schemaVersion": "1.7",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "3.6",
+ "topologies": [
+ "replicaset"
+ ],
+ "serverless": "forbid"
+ }
+ ],
+ "createEntities": [
+ {
+ "client": {
+ "id": "client0",
+ "observeEvents": [
+ "commandStartedEvent"
+ ],
+ "ignoreCommandMonitoringEvents": [
+ "killCursors"
+ ],
+ "useMultipleMongoses": false
+ }
+ },
+ {
+ "client": {
+ "id": "globalClient",
+ "useMultipleMongoses": false
+ }
+ },
+ {
+ "database": {
+ "id": "database0",
+ "client": "client0",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection0",
+ "database": "database0",
+ "collectionName": "collection0"
+ }
+ },
+ {
+ "database": {
+ "id": "database1",
+ "client": "client0",
+ "databaseName": "database1"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection1",
+ "database": "database1",
+ "collectionName": "collection1"
+ }
+ },
+ {
+ "database": {
+ "id": "globalDatabase0",
+ "client": "globalClient",
+ "databaseName": "database0"
+ }
+ },
+ {
+ "collection": {
+ "id": "globalCollection0",
+ "database": "globalDatabase0",
+ "collectionName": "collection0"
+ }
+ },
+ {
+ "database": {
+ "id": "globalDatabase1",
+ "client": "globalClient",
+ "databaseName": "database1"
+ }
+ },
+ {
+ "collection": {
+ "id": "globalCollection1",
+ "database": "globalDatabase1",
+ "collectionName": "collection1"
+ }
+ },
+ {
+ "collection": {
+ "id": "globalDb1Collection0",
+ "database": "globalDatabase1",
+ "collectionName": "collection0"
+ }
+ },
+ {
+ "collection": {
+ "id": "globalDb0Collection1",
+ "database": "globalDatabase0",
+ "collectionName": "collection1"
+ }
+ }
+ ],
+ "initialData": [
+ {
+ "collectionName": "collection0",
+ "databaseName": "database0",
+ "documents": []
+ }
+ ],
+ "tests": [
+ {
+ "description": "Test array truncation",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.7"
+ }
+ ],
+ "operations": [
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "a": 1,
+ "array": [
+ "foo",
+ {
+ "a": "bar"
+ },
+ 1,
+ 2,
+ 3
+ ]
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "updateOne",
+ "object": "collection0",
+ "arguments": {
+ "filter": {
+ "_id": 1
+ },
+ "update": [
+ {
+ "$set": {
+ "array": [
+ "foo",
+ {
+ "a": "bar"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "update",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "updateDescription": {
+ "updatedFields": {},
+ "removedFields": [],
+ "truncatedArrays": [
+ {
+ "field": "array",
+ "newSize": 2
+ }
+ ],
+ "disambiguatedPaths": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "description": "Test with document comment",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.4"
+ }
+ ],
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "comment": {
+ "name": "test1"
+ }
+ },
+ "saveResultAsEntity": "changeStream0"
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ],
+ "comment": {
+ "name": "test1"
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Test with document comment - pre 4.4",
+ "runOnRequirements": [
+ {
+ "maxServerVersion": "4.2.99"
+ }
+ ],
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "comment": {
+ "name": "test1"
+ }
+ },
+ "expectError": {
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ],
+ "comment": {
+ "name": "test1"
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Test with string comment",
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "comment": "comment"
+ },
+ "saveResultAsEntity": "changeStream0"
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ],
+ "comment": "comment"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Test that comment is set on getMore",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.4.0"
+ }
+ ],
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "comment": {
+ "key": "value"
+ }
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "a": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0"
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ],
+ "comment": {
+ "key": "value"
+ }
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "insert": "collection0",
+ "documents": [
+ {
+ "_id": 1,
+ "a": 1
+ }
+ ]
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$type": [
+ "int",
+ "long"
+ ]
+ },
+ "collection": "collection0",
+ "comment": {
+ "key": "value"
+ }
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Test that comment is not set on getMore - pre 4.4",
+ "runOnRequirements": [
+ {
+ "maxServerVersion": "4.3.99"
+ }
+ ],
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "comment": "comment"
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "a": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0"
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ],
+ "comment": "comment"
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "insert": "collection0",
+ "documents": [
+ {
+ "_id": 1,
+ "a": 1
+ }
+ ]
+ }
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "command": {
+ "getMore": {
+ "$$type": [
+ "int",
+ "long"
+ ]
+ },
+ "collection": "collection0",
+ "comment": {
+ "$$exists": false
+ }
+ },
+ "commandName": "getMore",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "to field is set in a rename change event",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.0.1"
+ }
+ ],
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "dropCollection",
+ "object": "database0",
+ "arguments": {
+ "collection": "collection1"
+ }
+ },
+ {
+ "name": "rename",
+ "object": "collection0",
+ "arguments": {
+ "to": "collection1"
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "rename",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "to": {
+ "db": "database0",
+ "coll": "collection1"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "description": "Test unknown operationType MUST NOT err",
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [
+ {
+ "$project": {
+ "operationType": "addedInFutureMongoDBVersion",
+ "ns": 1
+ }
+ }
+ ]
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "a": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "addedInFutureMongoDBVersion",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "description": "Test newField added in response MUST NOT err",
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [
+ {
+ "$project": {
+ "operationType": 1,
+ "ns": 1,
+ "newField": "newFieldValue"
+ }
+ }
+ ]
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "a": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "newField": "newFieldValue"
+ }
+ }
+ ]
+ },
+ {
+ "description": "Test new structure in ns document MUST NOT err",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "3.6",
+ "maxServerVersion": "5.2.99"
+ },
+ {
+ "minServerVersion": "6.0"
+ }
+ ],
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [
+ {
+ "$project": {
+ "operationType": "insert",
+ "ns.viewOn": "db.coll"
+ }
+ }
+ ]
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "a": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "insert",
+ "ns": {
+ "viewOn": "db.coll"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "description": "Test modified structure in ns document MUST NOT err",
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [
+ {
+ "$project": {
+ "operationType": "insert",
+ "ns": {
+ "db": "$ns.db",
+ "coll": "$ns.coll",
+ "viewOn": "db.coll"
+ }
+ }
+ }
+ ]
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "a": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0",
+ "viewOn": "db.coll"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "description": "Test server error on projecting out _id",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.2"
+ }
+ ],
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [
+ {
+ "$project": {
+ "_id": 0
+ }
+ }
+ ]
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "a": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectError": {
+ "errorCode": 280,
+ "errorCodeName": "ChangeStreamFatalError",
+ "errorLabelsContain": [
+ "NonResumableChangeStreamError"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "description": "Test projection in change stream returns expected fields",
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [
+ {
+ "$project": {
+ "optype": "$operationType",
+ "ns": 1,
+ "newField": "value"
+ }
+ }
+ ]
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "a": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "optype": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "newField": "value"
+ }
+ }
+ ]
+ },
+ {
+ "description": "$changeStream must be the first stage in a change stream pipeline sent to the server",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "3.6.0"
+ }
+ ],
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "The server returns change stream responses in the specified server response format",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "3.6.0"
+ }
+ ],
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "_id": {
+ "$$exists": true
+ },
+ "documentKey": {
+ "$$exists": true
+ },
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "description": "Executing a watch helper on a Collection results in notifications for changes to the specified collection",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "3.6.0"
+ }
+ ],
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalDb0Collection1",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "insertOne",
+ "object": "globalDb1Collection0",
+ "arguments": {
+ "document": {
+ "y": 2
+ }
+ }
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "z": 3
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "z": 3,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Change Stream should allow valid aggregate pipeline stages",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "3.6.0"
+ }
+ ],
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [
+ {
+ "$match": {
+ "fullDocument.z": 3
+ }
+ }
+ ]
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "y": 2
+ }
+ }
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "z": 3
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "z": 3,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ },
+ {
+ "$match": {
+ "fullDocument.z": 3
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Executing a watch helper on a Database results in notifications for changes to all collections in the specified database.",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "3.8.0"
+ }
+ ],
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "database0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalDb0Collection1",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "insertOne",
+ "object": "globalDb1Collection0",
+ "arguments": {
+ "document": {
+ "y": 2
+ }
+ }
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "z": 3
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection1"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "z": 3,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": 1,
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Executing a watch helper on a MongoClient results in notifications for changes to all collections in all databases in the cluster.",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "3.8.0"
+ }
+ ],
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "client0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalDb0Collection1",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "insertOne",
+ "object": "globalDb1Collection0",
+ "arguments": {
+ "document": {
+ "y": 2
+ }
+ }
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "z": 3
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection1"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "insert",
+ "ns": {
+ "db": "database1",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "y": 2,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "z": 3,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": 1,
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {
+ "allChangesForCluster": true
+ }
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "admin"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Test insert, update, replace, and delete event types",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "3.6.0"
+ }
+ ],
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "updateOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "filter": {
+ "x": 1
+ },
+ "update": {
+ "$set": {
+ "x": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "replaceOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "filter": {
+ "x": 2
+ },
+ "replacement": {
+ "x": 3
+ }
+ }
+ },
+ {
+ "name": "deleteOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "filter": {
+ "x": 3
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "update",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "updateDescription": {
+ "updatedFields": {
+ "x": 2
+ },
+ "removedFields": [],
+ "truncatedArrays": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ },
+ "disambiguatedPaths": {
+ "$$unsetOrMatches": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "replace",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 3,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "delete",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Test rename and invalidate event types",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.0.1"
+ }
+ ],
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "dropCollection",
+ "object": "database0",
+ "arguments": {
+ "collection": "collection1"
+ }
+ },
+ {
+ "name": "rename",
+ "object": "globalCollection0",
+ "arguments": {
+ "to": "collection1"
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "rename",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "to": {
+ "db": "database0",
+ "coll": "collection1"
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "invalidate"
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Test drop and invalidate event types",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.0.1"
+ }
+ ],
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "dropCollection",
+ "object": "database0",
+ "arguments": {
+ "collection": "collection0"
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "drop",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "invalidate"
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {},
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Test consecutive resume",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.1.7"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "globalClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "closeConnection": true
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": [],
+ "batchSize": 1
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 1
+ }
+ }
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 2
+ }
+ }
+ },
+ {
+ "name": "insertOne",
+ "object": "globalCollection0",
+ "arguments": {
+ "document": {
+ "x": 3
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 1,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 2,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "fullDocument": {
+ "x": 3,
+ "_id": {
+ "$$exists": true
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "ignoreExtraEvents": true,
+ "events": [
+ {
+ "commandStartedEvent": {
+ "command": {
+ "aggregate": "collection0",
+ "cursor": {
+ "batchSize": 1
+ },
+ "pipeline": [
+ {
+ "$changeStream": {}
+ }
+ ]
+ },
+ "commandName": "aggregate",
+ "databaseName": "database0"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "Test wallTime field is set in a change event",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "6.0.0"
+ }
+ ],
+ "operations": [
+ {
+ "name": "createChangeStream",
+ "object": "collection0",
+ "arguments": {
+ "pipeline": []
+ },
+ "saveResultAsEntity": "changeStream0"
+ },
+ {
+ "name": "insertOne",
+ "object": "collection0",
+ "arguments": {
+ "document": {
+ "_id": 1,
+ "a": 1
+ }
+ }
+ },
+ {
+ "name": "iterateUntilDocumentOrError",
+ "object": "changeStream0",
+ "expectResult": {
+ "operationType": "insert",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ "wallTime": {
+ "$$exists": true
+ }
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/source/change-streams/tests/unified/change-streams.yml b/source/change-streams/tests/unified/change-streams.yml
new file mode 100644
index 0000000000..6e9f8d6a75
--- /dev/null
+++ b/source/change-streams/tests/unified/change-streams.yml
@@ -0,0 +1,929 @@
+description: "change-streams"
+
+schemaVersion: "1.7"
+
+runOnRequirements:
+ - minServerVersion: "3.6"
+ # TODO(DRIVERS-2323): Run all possible tests against sharded clusters once we know the
+ # cause of unexpected command monitoring events.
+ topologies: [ replicaset ]
+ serverless: forbid
+
+createEntities:
+ - client:
+ id: &client0 client0
+ observeEvents: [ commandStartedEvent ]
+ ignoreCommandMonitoringEvents: [ killCursors ]
+ useMultipleMongoses: false
+ - client:
+ id: &globalClient globalClient
+ useMultipleMongoses: false
+ - database:
+ id: &database0 database0
+ client: *client0
+ databaseName: *database0
+ - collection:
+ id: &collection0 collection0
+ database: *database0
+ collectionName: *collection0
+ - database:
+ id: &database1 database1
+ client: *client0
+ databaseName: *database1
+ - collection:
+ id: &collection1 collection1
+ database: *database1
+ collectionName: *collection1
+ - database:
+ id: &globalDatabase0 globalDatabase0
+ client: *globalClient
+ databaseName: *database0
+ - collection:
+ id: &globalCollection0 globalCollection0
+ database: *globalDatabase0
+ collectionName: *collection0
+ - database:
+ id: &globalDatabase1 globalDatabase1
+ client: *globalClient
+ databaseName: *database1
+ - collection:
+ id: &globalCollection1 globalCollection1
+ database: *globalDatabase1
+ collectionName: *collection1
+ # Some tests run operations against db1.coll0 or db0.coll1
+ - collection:
+ id: &globalDb1Collection0 globalDb1Collection0
+ database: *globalDatabase1
+ collectionName: *collection0
+ - collection:
+ id: &globalDb0Collection1 globalDb0Collection1
+ database: *globalDatabase0
+ collectionName: *collection1
+
+initialData:
+ - collectionName: *collection0
+ databaseName: *database0
+ documents: []
+
+tests:
+ - description: "Test array truncation"
+ runOnRequirements:
+ - minServerVersion: "4.7"
+ operations:
+ - name: insertOne
+ object: *collection0
+ arguments:
+ document: {
+ "_id": 1,
+ "a": 1,
+ "array": ["foo", {"a": "bar"}, 1, 2, 3]
+ }
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: updateOne
+ object: *collection0
+ arguments:
+ filter: {
+ "_id": 1
+ }
+ update: [
+ {
+ "$set": {
+ "array": ["foo", {"a": "bar"}]
+ }
+ }
+ ]
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult: {
+ "operationType": "update",
+ "ns": {
+ "db": "database0",
+ "coll": "collection0"
+ },
+ # It is up to the MongoDB server to decide how to report a change.
+ # This expectation is based on the current MongoDB server behavior.
+ # Alternatively, we could have used a set of possible expectations of which only one
+ # must be satisfied, but the unified test format does not support this.
+ "updateDescription": {
+ "updatedFields": {},
+ "removedFields": [],
+ "truncatedArrays": [
+ {
+ "field": "array",
+ "newSize": 2
+ }
+ ],
+ disambiguatedPaths: { $$unsetOrMatches: { $$exists: true } }
+ }
+ }
+
+ - description: "Test with document comment"
+ runOnRequirements:
+ - minServerVersion: "4.4"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ comment: &comment0 { name: "test1" }
+ saveResultAsEntity: &changeStream0 changeStream0
+ expectEvents:
+ - client: *client0
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ pipeline:
+ - $changeStream: {}
+ comment: *comment0
+
+ - description: "Test with document comment - pre 4.4"
+ runOnRequirements:
+ - maxServerVersion: "4.2.99"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ comment: &comment0 { name: "test1" }
+ expectError:
+ isClientError: false
+ expectEvents:
+ - client: *client0
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ pipeline:
+ - $changeStream: {}
+ comment: *comment0
+
+ - description: "Test with string comment"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ comment: "comment"
+ saveResultAsEntity: &changeStream0 changeStream0
+ expectEvents:
+ - client: *client0
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ pipeline:
+ - $changeStream: {}
+ comment: "comment"
+
+ - description: "Test that comment is set on getMore"
+ runOnRequirements:
+ - minServerVersion: "4.4.0"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ comment: &commentDoc
+ key: "value"
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *collection0
+ arguments:
+ document: &new_document
+ _id: 1
+ a: 1
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectEvents:
+ - client: *client0
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ pipeline:
+ - $changeStream: {}
+ comment: *commentDoc
+ - commandStartedEvent:
+ command:
+ insert: *collection0
+ documents:
+ - *new_document
+ - commandStartedEvent:
+ command:
+ getMore: { $$type: [ int, long ] }
+ collection: *collection0
+ comment: *commentDoc
+ commandName: getMore
+ databaseName: *database0
+
+ - description: "Test that comment is not set on getMore - pre 4.4"
+ runOnRequirements:
+ - maxServerVersion: "4.3.99"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ comment: "comment"
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *collection0
+ arguments:
+ document: &new_document
+ _id: 1
+ a: 1
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectEvents:
+ - client: *client0
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ pipeline:
+ - $changeStream: {}
+ comment: "comment"
+ - commandStartedEvent:
+ command:
+ insert: *collection0
+ documents:
+ - *new_document
+ - commandStartedEvent:
+ command:
+ getMore: { $$type: [ int, long ] }
+ collection: *collection0
+ comment: { $$exists: false }
+ commandName: getMore
+ databaseName: *database0
+
+ - description: "to field is set in a rename change event"
+ runOnRequirements:
+ - minServerVersion: "4.0.1"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: dropCollection
+ object: *database0
+ arguments:
+ collection: &collection1 collection1
+ - name: rename
+ object: *collection0
+ arguments:
+ to: *collection1
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: rename
+ ns:
+ db: *database0
+ coll: *collection0
+ to:
+ db: *database0
+ coll: *collection1
+
+ - description: "Test unknown operationType MUST NOT err"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ # using $project to simulate future changes to ChangeStreamDocument structure
+ pipeline: [ { $project: { operationType: "addedInFutureMongoDBVersion", ns: 1 } } ]
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *collection0
+ arguments:
+ document: { "_id": 1, "a": 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: "addedInFutureMongoDBVersion"
+ ns:
+ db: *database0
+ coll: *collection0
+
+ - description: "Test newField added in response MUST NOT err"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ # using $project to simulate future changes to ChangeStreamDocument structure
+ pipeline: [ { $project: { operationType: 1, ns: 1, newField: "newFieldValue" } } ]
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *collection0
+ arguments:
+ document: { "_id": 1, "a": 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: "insert"
+ ns:
+ db: *database0
+ coll: *collection0
+ newField: "newFieldValue"
+
+ - description: "Test new structure in ns document MUST NOT err"
+ runOnRequirements:
+ - minServerVersion: "3.6"
+ maxServerVersion: "5.2.99"
+ - minServerVersion: "6.0"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ # using $project to simulate future changes to ChangeStreamDocument structure
+ pipeline: [ { $project: { operationType: "insert", "ns.viewOn": "db.coll" } } ]
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *collection0
+ arguments:
+ document: { "_id": 1, "a": 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: "insert"
+ ns:
+ viewOn: "db.coll"
+
+ - description: "Test modified structure in ns document MUST NOT err"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ # using $project to simulate future changes to ChangeStreamDocument structure
+ pipeline: [ { $project: { operationType: "insert", ns: { db: "$ns.db", coll: "$ns.coll", viewOn: "db.coll" } } } ]
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *collection0
+ arguments:
+ document: { "_id": 1, "a": 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: "insert"
+ ns:
+ db: *database0
+ coll: *collection0
+ viewOn: "db.coll"
+
+ - description: "Test server error on projecting out _id"
+ runOnRequirements:
+ - minServerVersion: "4.2"
+ # Server returns an error if _id is modified on versions 4.2 and higher
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: [ { $project: { _id: 0 } } ]
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *collection0
+ arguments:
+ document: { "_id": 1, "a": 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectError:
+ errorCode: 280
+ errorCodeName: "ChangeStreamFatalError"
+ errorLabelsContain: [ "NonResumableChangeStreamError" ]
+
+ - description: "Test projection in change stream returns expected fields"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: [ { $project: { optype: "$operationType", ns: 1, newField: "value" } } ]
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *collection0
+ arguments:
+ document: { "_id": 1, "a": 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ optype: "insert"
+ ns:
+ db: *database0
+ coll: *collection0
+ newField: "value"
+
+ - description: $changeStream must be the first stage in a change stream pipeline sent to the server
+ runOnRequirements:
+ - minServerVersion: "3.6.0"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: The server returns change stream responses in the specified server response format
+ runOnRequirements:
+ - minServerVersion: "3.6.0"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ _id: { $$exists: true }
+ documentKey: { $$exists: true }
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+
+ - description: Executing a watch helper on a Collection results in notifications for changes to the specified collection
+ runOnRequirements:
+ - minServerVersion: "3.6.0"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalDb0Collection1
+ arguments:
+ document: { x: 1 }
+ - name: insertOne
+ object: *globalDb1Collection0
+ arguments:
+ document: { y: 2 }
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { z: 3 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ z: 3
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: Change Stream should allow valid aggregate pipeline stages
+ runOnRequirements:
+ - minServerVersion: "3.6.0"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline:
+ - $match:
+ fullDocument.z: 3
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { y: 2 }
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { z: 3 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ z: 3
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline:
+ - $changeStream: {}
+ - $match:
+ fullDocument.z: 3
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: Executing a watch helper on a Database results in notifications for changes to all collections in the specified database.
+ runOnRequirements:
+ - minServerVersion: "3.8.0"
+ operations:
+ - name: createChangeStream
+ object: *database0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalDb0Collection1
+ arguments:
+ document: { x: 1 }
+ - name: insertOne
+ object: *globalDb1Collection0
+ arguments:
+ document: { y: 2 }
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { z: 3 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection1
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ z: 3
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: 1
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: Executing a watch helper on a MongoClient results in notifications for changes to all collections in all databases in the cluster.
+ runOnRequirements:
+ - minServerVersion: "3.8.0"
+ operations:
+ - name: createChangeStream
+ object: *client0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalDb0Collection1
+ arguments:
+ document: { x: 1 }
+ - name: insertOne
+ object: *globalDb1Collection0
+ arguments:
+ document: { y: 2 }
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { z: 3 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection1
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: insert
+ ns:
+ db: *database1
+ coll: *collection0
+ fullDocument:
+ y: 2
+ _id: { $$exists: true }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ z: 3
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: 1
+ cursor: {}
+ pipeline:
+ - $changeStream: { allChangesForCluster: true }
+ commandName: aggregate
+ databaseName: admin
+
+ - description: "Test insert, update, replace, and delete event types"
+ runOnRequirements:
+ - minServerVersion: "3.6.0"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: updateOne
+ object: *globalCollection0
+ arguments:
+ filter: { x: 1 }
+ update:
+ $set: { x: 2 }
+ - name: replaceOne
+ object: *globalCollection0
+ arguments:
+ filter: { x: 2 }
+ replacement: { x: 3 }
+ - name: deleteOne
+ object: *globalCollection0
+ arguments:
+ filter: { x: 3 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: update
+ ns:
+ db: *database0
+ coll: *collection0
+ updateDescription:
+ updatedFields: { x: 2 }
+ removedFields: []
+ truncatedArrays: { $$unsetOrMatches: { $$exists: true } }
+ disambiguatedPaths: { $$unsetOrMatches: { $$exists: true } }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: replace
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 3
+ _id: { $$exists: true }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: delete
+ ns:
+ db: *database0
+ coll: *collection0
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: Test rename and invalidate event types
+ runOnRequirements:
+ - minServerVersion: "4.0.1"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: dropCollection
+ object: *database0
+ arguments:
+ collection: *collection1
+ - name: rename
+ object: *globalCollection0
+ arguments:
+ to: *collection1
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: rename
+ ns:
+ db: *database0
+ coll: *collection0
+ to:
+ db: *database0
+ coll: *collection1
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: invalidate
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: Test drop and invalidate event types
+ runOnRequirements:
+ - minServerVersion: "4.0.1"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: dropCollection
+ object: *database0
+ arguments:
+ collection: *collection0
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: drop
+ ns:
+ db: *database0
+ coll: *collection0
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: invalidate
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor: {}
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+
+ # Test that resume logic works correctly even after consecutive retryable failures of a getMore command,
+ # with no intervening events. This is ensured by setting the batch size of the change stream to 1,
+ - description: Test consecutive resume
+ runOnRequirements:
+ - minServerVersion: "4.1.7"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *globalClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [ getMore ]
+ closeConnection: true
+ - name: createChangeStream
+ object: *collection0
+ arguments:
+ pipeline: []
+ batchSize: 1
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 1 }
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 2 }
+ - name: insertOne
+ object: *globalCollection0
+ arguments:
+ document: { x: 3 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 1
+ _id: { $$exists: true }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 2
+ _id: { $$exists: true }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: insert
+ ns:
+ db: *database0
+ coll: *collection0
+ fullDocument:
+ x: 3
+ _id: { $$exists: true }
+ expectEvents:
+ - client: *client0
+ ignoreExtraEvents: true
+ events:
+ - commandStartedEvent:
+ command:
+ aggregate: *collection0
+ cursor:
+ batchSize: 1
+ pipeline: [ { $changeStream: {} } ]
+ commandName: aggregate
+ databaseName: *database0
+
+ - description: "Test wallTime field is set in a change event"
+ runOnRequirements:
+ - minServerVersion: "6.0.0"
+ operations:
+ - name: createChangeStream
+ object: *collection0
+ arguments: { pipeline: [] }
+ saveResultAsEntity: &changeStream0 changeStream0
+ - name: insertOne
+ object: *collection0
+ arguments:
+ document: { "_id": 1, "a": 1 }
+ - name: iterateUntilDocumentOrError
+ object: *changeStream0
+ expectResult:
+ operationType: "insert"
+ ns:
+ db: *database0
+ coll: *collection0
+ wallTime: { $$exists: true }
diff --git a/source/client-backpressure/client-backpressure.md b/source/client-backpressure/client-backpressure.md
new file mode 100644
index 0000000000..6aea3d2aaa
--- /dev/null
+++ b/source/client-backpressure/client-backpressure.md
@@ -0,0 +1,442 @@
+# Client Backpressure
+
+- Status: Accepted
+- Minimum Server Version: N/A
+
+______________________________________________________________________
+
+## Abstract
+
+This specification adds the ability for drivers to automatically retry requests that fail due to server overload errors
+while applying backpressure to avoid further overloading the server.
+
+The retry behaviors defined in this specification are separate from and complementary to the retry behaviors defined in
+the [Retryable Reads](../retryable-reads/retryable-reads.md) and
+[Retryable Writes](../retryable-writes/retryable-writes.md) specifications. This specification expands retry support to
+all commands when specific server overload conditions are encountered, regardless of whether the command would normally
+be retryable under those specifications.
+
+## META
+
+The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
+"OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt).
+
+## Specification
+
+### Terms
+
+#### Ingress Connection Rate Limiter
+
+A token bucket based system introduced in MongoDB 8.2 to admit, reject or queue connection requests. It aims to prevent
+connection spikes from overloading the system.
+
+#### Ingress Request Rate Limiter
+
+A token bucket based system introduced in MongoDB 8.2 to admit a command or reject it with an overload error at the
+front door of a mongod/s. It aims to prevent command spikes from overloading the system.
+
+The ingress request rate limiter only applies to commands sent on authenticated connections.
+
+#### MongoTune
+
+Mongotune is a policy engine outside the server (mongod or mongos) which monitors a set of metrics (MongoDB or system
+host) to dynamically configure MongoDB settings. MongoTune is deployed to Atlas clusters and will dynamically configure
+the connection and request rate limiters to prevent and mitigate overloading the system.
+
+#### RetryableError label
+
+This error label indicates that a command is safely retryable regardless of the command type (read or write), its
+metadata, or any of its arguments.
+
+#### SystemOverloadedError label
+
+An error is considered to be an overload error if it contains the `SystemOverloadedError` label. This error label
+indicates that the server is overloaded. If this error label is present, drivers will backoff before attempting a retry.
+
+#### Retryable Overload Error
+
+An error which indicates that it is an overload error (contains the `SystemOverloadedError` label) and contains the
+`RetryableError` label.
+
+For example, when a request exceeds the ingress request rate limit, the following error may be returned:
+
+```js
+{
+ 'ok': 0.0,
+ 'errmsg': "Rate limiter 'ingressRequestRateLimiter' rate exceeded",
+ 'code': 462,
+ 'codeName': 'IngressRequestRateLimitExceeded',
+ 'errorLabels': ['SystemOverloadedError', 'RetryableError'],
+}
+```
+
+Note that an error is not guaranteed to contain both the `SystemOverloadedError` and the `RetryableError` labels just
+because it contains one of them.
+
+#### Goodput
+
+The throughput of positive, useful output. In the context of drivers, this refers to the number of non-error results
+that the driver processes per unit of time.
+
+See [goodput](https://en.wikipedia.org/wiki/Goodput).
+
+### Requirements for Client Backpressure
+
+#### Driver mechanisms subject to the retry policy
+
+Commands sent by the driver to the server are subject to the retry policy defined in this specification unless the
+command is included in the exceptions below.
+
+Driver commands not subject to the overload retry policy:
+
+- [Monitoring commands](../server-discovery-and-monitoring/server-monitoring.md#monitoring) and
+ [round-trip time pingers](../server-discovery-and-monitoring/server-monitoring.md#measuring-rtt) (see
+ [Why not apply the overload retry policy to monitoring and RTT connections?](./client-backpressure.md#why-not-apply-the-overload-retry-policy-to-monitoring-and-rtt-connections)).
+- Commands executed during
+ [connection establishment](../connection-monitoring-and-pooling/connection-monitoring-and-pooling.md#establishing-a-connection-internal-implementation)
+ and [reauthentication](../auth/auth.md) (see
+ [Why not apply the overload policy to authentication commands or reauthentication commands?](./client-backpressure.md#why-not-apply-the-overload-policy-to-authentication-commands-or-reauthentication-commands)).
+
+Note: Drivers communicate with [mongocryptd](../client-side-encryption/client-side-encryption.md#mongocryptd) using the
+driver's `runCommand()` API. Consequently, drivers will implicitly apply the retry policy to communication with
+mongocryptd, although in practice the retry policy would never be used because mongocryptd connections are not
+authenticated.
+
+#### Overload retry policy
+
+This specification expands the driver's retry ability to all commands if the error indicates that it is a retryable
+overload error, including those not eligible for retry under the
+[read](../retryable-reads/retryable-reads.md)/[write](../retryable-writes/retryable-writes.md) retry policies such as
+updateMany, create collection, getMore, and generic runCommand. The new command execution method obeys the following
+rules:
+
+1. `attempt` is the execution attempt number (starting with 0). Note that `attempt` includes retries for errors that are
+ not overload errors (this might include attempts under other retry policies, see
+ [Interactions with Other Retry Policies](./client-backpressure.md#interaction-with-other-retry-policies)).
+2. A retry attempt will only be permitted if:
+ 1. The error is a retryable overload error.
+ 2. We have not reached `MAX_RETRIES`.
+ - The default value of `MAX_RETRIES` is 2. Drivers MUST expose `maxAdaptiveRetries` as a configurable option for
+ this maximum. In the future, this default value or the default behavior of the driver may change without being
+ considered a breaking change.
+ - This intentionally changes the behavior of CSOT which otherwise would retry an unlimited number of times within
+ the timeout to avoid retry storms.
+ 3. (CSOT-only): There is still time for a retry attempt according to the
+ [Client Side Operations Timeout](../client-side-operations-timeout/client-side-operations-timeout.md)
+ specification.
+ 4. The command is a write and [retryWrites](../retryable-writes/retryable-writes.md#retrywrites) is enabled or the
+ command is a read and [retryReads](../retryable-reads/retryable-reads.md#retryreads) is enabled.
+ - To retry `runCommand`, both [retryWrites](../retryable-writes/retryable-writes.md#retrywrites) and
+ [retryReads](../retryable-reads/retryable-reads.md#retryreads) MUST be enabled. See
+ [Why must both `retryWrites` and `retryReads` be enabled to retry runCommand?](client-backpressure.md#why-must-both-retrywrites-and-retryreads-be-enabled-to-retry-runcommand)
+3. If the request is eligible for retry (as outlined in step 2 above), the client MUST apply exponential backoff
+ according to the following formula: `backoff = jitter * min(MAX_BACKOFF, BASE_BACKOFF * 2^(attempt - 1))`
+ - `jitter` is a random jitter value between 0 and 1.
+ - `BASE_BACKOFF` is constant 100ms.
+ - `MAX_BACKOFF` is 10000ms.
+ - This results in delays of 100ms and 200ms before accounting for jitter.
+4. If the request is eligible for retry (as outlined in step 2 above) and `enableOverloadRetargeting` is enabled, the
+ client MUST add the previously used server's address to the list of deprioritized server addresses for
+ [server selection](../server-selection/server-selection.md). Drivers MUST expose `enableOverloadRetargeting` as a
+ configurable boolean option that defaults to `false`.
+5. If the request is eligible for retry (as outlined in step 2 above) and is a retryable write:
+ 1. If the command is a part of a transaction, the instructions for command modification on retry for commands in
+ transactions MUST be followed, as outlined in the
+ [transactions](../transactions/transactions.md#interaction-with-retryable-writes) specification.
+ 2. If the command is a not a part of a transaction, the instructions for command modification on retry for retryable
+ writes MUST be followed, as outlined in the [retryable writes](../retryable-writes/retryable-writes.md)
+ specification.
+6. If the request is not eligible for any retries, then the client MUST propagate errors following the behaviors
+ described in the [retryable reads](../retryable-reads/retryable-reads.md),
+ [retryable writes](../retryable-writes/retryable-writes.md) and the [transactions](../transactions/transactions.md)
+ specifications.
+ - For the purposes of error propagation, `runCommand` is considered a write.
+
+#### Interaction with Other Retry Policies
+
+The retry policy in this specification is separate from the other retry policies defined in the
+[retryable reads](../retryable-reads/retryable-reads.md) and [retryable writes](../retryable-writes/retryable-writes.md)
+specifications. Drivers MUST ensure:
+
+- When a failed attempt is retried, backoff MUST be applied if and only if the error is an overload error.
+- If an overload error is encountered:
+ - Regardless of whether CSOT is enabled or not, the maximum number of retries for any retry policy becomes
+ `MAX_RETRIES`. This limiting applies to all retry attempts, including retries for errors that are not overload
+ errors.
+ - If CSOT is enabled and a command has been retried at least `MAX_RETRIES` times, it MUST NOT be retried further.
+
+#### Pseudocode
+
+The following pseudocode demonstrates the unified retry policy, combining the overload retry policy defined in this
+specification with the retry policies from [Retryable Reads](../retryable-reads/retryable-reads.md) and
+[Retryable Writes](../retryable-writes/retryable-writes.md). For brevity, some interactions with other specs are not
+included, such as error handling with `NoWritesPerformed` labels.
+
+```python
+# Note: the values below have been scaled down by a factor of 1000 because
+# Python's sleep API takes a duration in seconds, not milliseconds.
+BASE_BACKOFF = 0.1 # 100ms
+MAX_BACKOFF = 10 # 10000ms
+
+MAX_RETRIES = 2
+
+def execute_command_retryable(command, ...):
+ deprioritized_servers = []
+ attempt = 0
+ allowed_retries = if is_csot then math.inf else 1
+
+ while True:
+ try:
+ server = select_server(deprioritized_servers)
+ connection = server.getConnection()
+ res = execute_command(connection, command)
+ return res
+ except PyMongoError as exc:
+ is_retryable = (is_retryable_write(command, exc)
+ or is_retryable_read(command, exc)
+ or (exc.contains_error_label("RetryableError") and exc.contains_error_label("SystemOverloadedError")))
+ is_overload = exc.contains_error_label("SystemOverloadedError")
+
+ # Raise if the error is non-retryable.
+ if not is_retryable:
+ raise
+
+ attempt += 1
+ if is_overload:
+ allowed_retries = MAX_RETRIES
+
+ if attempt > allowed_retries:
+ raise
+
+ # enableOverloadRetargeting is true
+ if overload_retargeting:
+ deprioritized_servers.append(server.address)
+
+ if is_overload:
+ jitter = random.random() # Random float between [0.0, 1.0).
+ backoff = jitter * min(MAX_BACKOFF, BASE_BACKOFF * 2 ** (attempt - 1))
+
+ # If the delay exceeds the deadline, bail early.
+ if _csot.get_timeout():
+ if time.monotonic() + backoff > _csot.get_deadline():
+ raise
+
+ time.sleep(backoff)
+```
+
+#### Handshake changes
+
+Drivers conforming to this spec MUST add `"backpressure": True` to the
+[connection handshake](../mongodb-handshake/handshake.rst). This flag allows the server to identify clients which do and
+do not support backpressure. Currently, this flag is unused but in the future the server may offer different rate
+limiting behavior for clients that do not support backpressure.
+
+#### Implementation notes
+
+On some platforms sleep() can have a very low precision, meaning an attempt to sleep for 50ms may actually sleep for a
+much larger time frame. Drivers are not required to work around this limitation.
+
+### Logging Retry Attempts
+
+[As with retryable writes](../retryable-writes/retryable-writes.md#logging-retry-attempts), drivers MAY choose to log
+retry attempts for load shed commands. This specification does not define a format for such log messages.
+
+### Command Monitoring
+
+[As with retryable writes](../retryable-writes/retryable-writes.md#command-monitoring), in accordance with the
+[Command Logging and Monitoring](../command-logging-and-monitoring/command-logging-and-monitoring.md) specification,
+drivers MUST guarantee that each `CommandStartedEvent` has either a correlating `CommandSucceededEvent` or
+`CommandFailedEvent` and that every "command started" log message has either a correlating "command succeeded" log
+message or "command failed" log message. If an attempt of a retryable command encounters a retryable error, drivers MUST
+fire a `CommandFailedEvent` and emit a "command failed" log message for the retryable error and fire a separate
+`CommandStartedEvent` and emit a separate "command started" log message when executing the subsequent retry attempt.
+Note that for retries, `CommandStartedEvent`s and "command started" log message may have different `connectionId`s,
+since a server is reselected for a retry attempt.
+
+### Documentation
+
+1. Drivers MUST document that all commands support retries on server overload.
+2. Driver release notes MUST make it clear to users that they may need to adjust custom retry logic to prevent an
+ application from inadvertently retrying for too long (see [Backwards Compatibility](#backwards-compatibility) for
+ details).
+
+### Backwards Compatibility
+
+The server's rate limiting can introduce higher error rates than previously would have been exposed to users under
+periods of extreme server overload. The increased error rate is a tradeoff: given the choice between an overloaded
+server (potential crash), or at minimum dramatically slower query execution time and a stable but lowered throughput
+with higher error rate as the server load sheds, we have chosen the latter.
+
+The changes in this specification help smooth out the impact of the server's rate limiting on users by reducing the
+number of errors users see during spikes or burst workloads and help prevent retry storms by spacing out retries.
+However, older drivers do not have this benefit. Drivers MUST document that:
+
+- Users SHOULD upgrade to driver versions that officially support backpressure to avoid any impacts of server changes.
+- Users who do not upgrade might need to update application error handling to handle higher rates of overload errors.
+
+## Test Plan
+
+See the [README](./tests/README.md) for tests.
+
+## Motivation for Change
+
+New load shedding mechanisms are being introduced to the server that improve its ability to remain available under
+extreme load, however clients do not know how to handle the errors returned when one of its requests has been rejected.
+As a result, such overload errors would currently either be propagated back to applications, increasing
+externally-visible command failure rates, or be retried immediately, increasing the load on already overburdened
+servers. To minimize these effects, this specification enables clients to retry requests that have been load shed in a
+way that does not overburden already overloaded servers. This retry policy allows for more aggressive and effective load
+shedding policies to be deployed in the future. This will also help unify the currently-divergent retry policy between
+drivers and the server (mongos).
+
+## Reference Implementation
+
+The Node and Python drivers will provide the reference implementations. See
+[NODE-7142](https://jira.mongodb.org/browse/NODE-7142) and [PYTHON-5528](https://jira.mongodb.org/browse/PYTHON-5528).
+
+## Future work
+
+1. [DRIVERS-3333](https://jira.mongodb.org/browse/DRIVERS-3333) Add a backoff state into the connection pool.
+2. [DRIVERS-3241](https://jira.mongodb.org/browse/DRIVERS-3241) Add diagnostic metadata to retried commands.
+3. [DRIVERS-3352](https://jira.mongodb.org/browse/DRIVERS-3352) Add support for RetryableError labels to retryable reads
+ and writes.
+
+## Q&A
+
+### Why are drivers not required to work around timing limitations in their language's sleep() APIs?
+
+The client backpressure retry loop is primarily concerned with spreading out retries to avoid retry storms. The exact
+sleep duration is not critical to the intended behavior, so long as we sleep at least as long as we say we will.
+
+### Why override existing maximum number of retry attempt defaults for retryable reads and writes if an overload error is received at any point during an operation's retry loop?
+
+Load-shedded errors indicate that the request was rejected by the server to minimize load, not that the command failed
+for logical reasons. So, when determining the number of retries an operation should attempt:
+
+- Any load-shedded errors should be retried to give them a real attempt at success
+- If the command ultimately would have failed if it had not been load shed by the server, returning an actionable error
+ message is preferable to a generic overload error.
+
+The maximum retry attempt logic in this specification balances retry policies described in the
+[retryable reads](../retryable-reads/retryable-reads.md) and [retryable writes](../retryable-writes/retryable-writes.md)
+specifications with load-shedding behavior:
+
+- Relying on either 1 or infinite retries (depending on whether CSOT enabled or not) preserves retry behaviors defined
+ in the [retryable reads](../retryable-reads/retryable-reads.md),
+ [retryable writes](../retryable-writes/retryable-writes.md) and
+ [CSOT](../client-side-operations-timeout/client-side-operations-timeout.md) specifications when no overload errors are
+ encountered.
+- Adjusting the maximum number of retry attempts to MAX_RETRIES if an overload error is returned from the server gives
+ requests more opportunities to succeed and helps reduce application errors.
+- An alternative approach would be to retry once if we don't receive an overload error, in which case we'd retry 5
+ times. The approach chosen allows for additional retries in scenarios where a non-overload error fails on a retry
+ with an overload error.
+
+### Why not apply the overload retry policy to monitoring and RTT connections?
+
+The ingress request rate limiter only applies to authenticated connections. Neither the
+[monitoring connection](../server-discovery-and-monitoring/server-monitoring.md#monitoring) nor the
+[RTT pinger](../server-discovery-and-monitoring/server-monitoring.md#measuring-rtt) use authentication, and consequently
+will not encounter ingress operation rate limiter errors.
+
+It is conceivable that a driver attempting to establish a monitoring connection or RTT connection could encounter the
+ingress connection rate limiter. However, in these scenarios, the driver already behaves in an appropriate manner.
+
+If an error is encountered, both the RTT connections and monitoring connections already retry.
+
+- The RTT pinger retries indefinitely until the monitor is reset.
+- Monitoring failures will mark the server unknown, which will reset the monitor, triggering another monitoring request.
+
+Under most circumstances, both monitoring and RTT connections wait at least `minHeartbeatFrequencyMS` between `hello`
+commands, ensuring delays between retries. The notable exception is monitoring connections retrying network errors
+without waiting for `minHeartbeatFrequencyMS`, which is acceptable since re-establishing monitoring is the driver's top
+priority when a monitoring connection disconnects.
+
+### Why not apply the overload policy to authentication commands or reauthentication commands?
+
+The ingress request rate limiter only applies to authenticated connections. The server does not consider a connection to
+be authenticated until after the authentication workflow has completed and during reauthentication a connection is not
+considered authenticated by the server. So, authentication and reauthentication commands will not hit the ingress
+operation rate limiter.
+
+### Why must both `retryWrites` and `retryReads` be enabled to retry runCommand?
+
+[`runCommand`](../run-command/run-command.md) is not retryable under the
+[retryable reads](../retryable-reads/retryable-reads.md) and [retryable writes](../retryable-writes/retryable-writes.md)
+specifications and consequently it was not historically classified as a read or write command.
+
+The most flexible approach would be to inspect the user's command and determine if it is a read or a write. However,
+this is problematic for two reasons:
+
+- The runCommand specification specifically forbids drivers from inspecting the user's command.
+- `runCommand` is commonly used to execute commands of which the driver has no knowledge and therefore cannot determine
+ whether it is a read or write.
+
+Another option is to always consider `runCommand` retryable under the overload retry policy, regardless of the setting
+of [`retryReads`](../retryable-reads/retryable-reads.md#retryreads) and
+[`retryWrites`](../retryable-writes/retryable-writes.md#retrywrites). However, this behavior goes against a user's
+expectations: if a user disables both options, they would expect no commands to be retried.
+
+Retrying `runCommand` only when both `retryReads` and `retryWrites` are enabled is a safe default that does not have the
+pitfalls of either approach outlined by above:
+
+- This approach does not require drivers to inspect a user's command document.
+- This approach will not retry commands if a user has disabled both `retryReads` and `retryWrites`.
+
+Additionally, both `retryReads` and `retryWrites` are enabled by default, so for most users `runCommand` will be
+retried. This approach also prevents accidentally retrying a read command when only `retryWrites` is enabled, or
+retrying a write command when only `retryReads` is enabled.
+
+### Why make `maxAdaptiveRetries` configurable?
+
+Modelling and the underpinning theory for backpressure shows that the n-retries approach (retry up to N times on
+overload errors without a token bucket) can introduce retry storms as overload increases. However, the specifics of the
+workload and cluster serving that workload significantly impacts the threshold at which retry volume becomes an
+additional burden rather than a throughput improvement. Some applications and clusters may be very tolerant of many
+additional retries, while others may want to break out of the loop much earlier.
+
+The selection of 2 as a default attempts to broadly pick a sensible default for most users that will on average be a
+benefit rather than a negative during overload. However, savvy users, the users expected to be most affected by overload
+and have the most insight into the specifics of their workload and cluster, will likely find that tweaking this value on
+a per-workload basis produces better results. Additionally, there are situations where disabling overload retries
+entirely is optimal, such as non-critical workloads against a cluster shared with critical workloads. Without a knob,
+those situations will cause users to either have a strictly worse experience with a new driver, or force them to
+downgrade to an older driver to avoid the issue. These are two strong motivations to add a knob for
+`maxAdaptiveRetries`.
+
+### Why make `enableOverloadRetargeting` configurable?
+
+The current contract we've made with users utilizing `primaryPreferred` is that reads will only go to a secondary if the
+primary is unavailable. The documentation does not explicitly define unavailable, but in practice that means the primary
+is unselectable. Overload retargeting makes the primary unselectable for a retry operation if it returned an overload
+error on a previous attempt. This materially changes how often secondary reads occur. Since secondary reads can result
+in stale data, enabling overload retargeting increases the chance that users of `primaryPreferred` will get stale data
+when they did not previously. This is a potentially significant change in expected behavior. Therefore, overload
+retargeting is disabled by default with a knob to enable it.
+
+Overload retargeting significantly increases availability during overload, but it does increase the risk of getting
+stale data when used with `primaryPreferred`. Users of `primaryPreferred` may widely end up preferring that behavior. If
+that is the case, overload retargeting may be enabled by default in the future.
+
+`secondaryPreferred` does not have this same staleness issue, but it still materially changes what the preference means
+from "almost always secondary" to "sometimes primary".
+
+Note that for sharded clusters, drivers always attempt to retarget across `mongos` instances on all retryable errors,
+including overload errors, regardless of how `enableOverloadRetargeting` is set. `mongos` has a separate flag to
+retarget overload errors within shards that is independent of the driver's configuration.
+
+Alternative design considered: Overload retargeting could have been implemented as a read preference option rather than
+a client-level option. This would allow more granular control: enabling retargeting for specific operations, databases,
+or collections. However, a read preference option would require server changes to recognize the new field, and would add
+another dimension to read preference selection that users need to reason about. A client-level binary setting is simpler
+to understand and configure.
+
+## Changelog
+
+- 2026-04-14: Clarify correct retry behavior when a mix of overload and non-overload errors are encountered.
+
+- 2026-03-30: Introduce phase 1 support without token buckets.
+
+- 2026-02-20: Disable token buckets by default.
+
+- 2026-01-09: Initial version.
diff --git a/source/client-backpressure/tests/README.md b/source/client-backpressure/tests/README.md
new file mode 100644
index 0000000000..17becefd0a
--- /dev/null
+++ b/source/client-backpressure/tests/README.md
@@ -0,0 +1,121 @@
+# Client Backpressure Tests
+
+______________________________________________________________________
+
+## Introduction
+
+The YAML and JSON files in this directory are platform-independent tests meant to exercise a driver's implementation of
+retryable reads. These tests utilize the [Unified Test Format](../../unified-test-format/unified-test-format.md).
+
+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.
+
+### Prose Tests
+
+#### Test 1: Operation Retry Uses Exponential Backoff
+
+Drivers should test that retries do not occur immediately when a SystemOverloadedError is encountered. This test MUST be
+executed against a MongoDB 4.4+ server that has enabled the `configureFailPoint` command with the `errorLabels` option.
+
+1. Let `client` be a `MongoClient`
+2. Let `collection` be a collection
+3. Now, run transactions without backoff:
+ 1. Configure the random number generator used for jitter to always return `0` -- this effectively disables backoff.
+
+ 2. Configure the following failPoint:
+
+ ```javascript
+ {
+ configureFailPoint: 'failCommand',
+ mode: 'alwaysOn',
+ data: {
+ failCommands: ['insert'],
+ errorCode: 2,
+ errorLabels: ['SystemOverloadedError', 'RetryableError']
+ }
+ }
+ ```
+
+ 3. Insert the document `{ a: 1 }`. Expect that the command errors. Measure the duration of the command execution.
+
+ ```javascript
+ const start = performance.now();
+ expect(
+ await coll.insertOne({ a: 1 }).catch(e => e)
+ ).to.be.an.instanceof(MongoServerError);
+ const end = performance.now();
+ ```
+
+ 4. Configure the random number generator used for jitter to always return a number as close as possible to `1`.
+
+ 5. Execute step 3 again.
+
+ 6. Compare the time between the two runs.
+
+ ```python
+ assertTrue(absolute_value(with_backoff_time - (no_backoff_time + 0.3 seconds)) < 0.3 seconds)
+ ```
+
+ The sum of 2 backoffs is 0.3 seconds. There is a 0.3-second window to account for potential variance between the
+ two runs.
+
+#### Test 2: REMOVED
+
+#### Test 3: Overload Errors are Retried a Maximum of MAX_RETRIES times
+
+Drivers should test that overload errors are retried a maximum of MAX_RETRIES times. This test MUST be executed against
+a MongoDB 4.4+ server that has enabled the `configureFailPoint` command with the `errorLabels` option.
+
+1. Let `client` be a `MongoClient` with command event monitoring enabled.
+
+2. Let `coll` be a collection.
+
+3. Configure the following failpoint:
+
+ ```javascript
+ {
+ configureFailPoint: 'failCommand',
+ mode: 'alwaysOn',
+ data: {
+ failCommands: ['find'],
+ errorCode: 462, // IngressRequestRateLimitExceeded
+ errorLabels: ['SystemOverloadedError', 'RetryableError']
+ }
+ }
+ ```
+
+4. Perform a find operation with `coll` that fails.
+
+5. Assert that the raised error contains both the `RetryableError` and `SystemOverloadedError` error labels.
+
+6. Assert that the total number of started commands is MAX_RETRIES + 1 (3).
+
+#### Test 4: Overload Errors are Retried a Maximum of maxAdaptiveRetries times when configured
+
+Drivers should test that overload errors are retried a maximum of `maxAdaptiveRetries` times, when configured. This test
+MUST be executed against a MongoDB 4.4+ server that has enabled the `configureFailPoint` command with the `errorLabels`
+option.
+
+1. Let `client` be a `MongoClient` with `maxAdaptiveRetries=1` and command event monitoring enabled.
+
+2. Let `coll` be a collection.
+
+3. Configure the following failpoint:
+
+ ```javascript
+ {
+ configureFailPoint: 'failCommand',
+ mode: 'alwaysOn',
+ data: {
+ failCommands: ['find'],
+ errorCode: 462, // IngressRequestRateLimitExceeded
+ errorLabels: ['SystemOverloadedError', 'RetryableError']
+ }
+ }
+ ```
+
+4. Perform a find operation with `coll` that fails.
+
+5. Assert that the raised error contains both the `RetryableError` and `SystemOverloadedError` error labels.
+
+6. Assert that the total number of started commands is `maxAdaptiveRetries` + 1 (2).
diff --git a/source/client-backpressure/tests/backpressure-connection-checkin.json b/source/client-backpressure/tests/backpressure-connection-checkin.json
new file mode 100644
index 0000000000..794951ad5f
--- /dev/null
+++ b/source/client-backpressure/tests/backpressure-connection-checkin.json
@@ -0,0 +1,111 @@
+{
+ "description": "tests that connections are returned to the pool on retry attempts for overload errors",
+ "schemaVersion": "1.3",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.4",
+ "topologies": [
+ "replicaset",
+ "sharded",
+ "load-balanced"
+ ]
+ }
+ ],
+ "createEntities": [
+ {
+ "client": {
+ "id": "client",
+ "useMultipleMongoses": false,
+ "observeEvents": [
+ "connectionCheckedOutEvent",
+ "connectionCheckedInEvent"
+ ]
+ }
+ },
+ {
+ "client": {
+ "id": "fail_point_client",
+ "useMultipleMongoses": false
+ }
+ },
+ {
+ "database": {
+ "id": "database",
+ "client": "client",
+ "databaseName": "backpressure-connection-checkin"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection",
+ "database": "database",
+ "collectionName": "coll"
+ }
+ }
+ ],
+ "tests": [
+ {
+ "description": "overload error retry attempts return connections to the pool",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "find"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "find",
+ "object": "collection",
+ "arguments": {
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "eventType": "cmap",
+ "events": [
+ {
+ "connectionCheckedOutEvent": {}
+ },
+ {
+ "connectionCheckedInEvent": {}
+ },
+ {
+ "connectionCheckedOutEvent": {}
+ },
+ {
+ "connectionCheckedInEvent": {}
+ },
+ {
+ "connectionCheckedOutEvent": {}
+ },
+ {
+ "connectionCheckedInEvent": {}
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/source/client-backpressure/tests/backpressure-connection-checkin.yml b/source/client-backpressure/tests/backpressure-connection-checkin.yml
new file mode 100644
index 0000000000..ce8c1acb36
--- /dev/null
+++ b/source/client-backpressure/tests/backpressure-connection-checkin.yml
@@ -0,0 +1,60 @@
+description: tests that connections are returned to the pool on retry attempts for overload errors
+schemaVersion: "1.3"
+runOnRequirements:
+ - minServerVersion: "4.4"
+ topologies:
+ - replicaset
+ - sharded
+ - load-balanced
+createEntities:
+ - client:
+ id: client
+ useMultipleMongoses: false
+ observeEvents:
+ - connectionCheckedOutEvent
+ - connectionCheckedInEvent
+ - client:
+ id: fail_point_client
+ useMultipleMongoses: false
+ - database:
+ id: database
+ client: client
+ databaseName: backpressure-connection-checkin
+ - collection:
+ id: collection
+ database: database
+ collectionName: coll
+tests:
+ - description: overload error retry attempts return connections to the pool
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands:
+ - find
+ errorLabels:
+ - RetryableError
+ - SystemOverloadedError
+ errorCode: 2
+ - name: find
+ object: collection
+ arguments:
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+ expectEvents:
+ - client: client
+ eventType: cmap
+ events:
+ - connectionCheckedOutEvent: {}
+ - connectionCheckedInEvent: {}
+ - connectionCheckedOutEvent: {}
+ - connectionCheckedInEvent: {}
+ - connectionCheckedOutEvent: {}
+ - connectionCheckedInEvent: {}
diff --git a/source/client-backpressure/tests/backpressure-retry-loop.json b/source/client-backpressure/tests/backpressure-retry-loop.json
new file mode 100644
index 0000000000..a0b4877fac
--- /dev/null
+++ b/source/client-backpressure/tests/backpressure-retry-loop.json
@@ -0,0 +1,4553 @@
+{
+ "description": "tests that operations respect overload backoff retry loop",
+ "schemaVersion": "1.3",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.4",
+ "topologies": [
+ "replicaset",
+ "sharded",
+ "load-balanced"
+ ]
+ }
+ ],
+ "createEntities": [
+ {
+ "client": {
+ "id": "client",
+ "useMultipleMongoses": false,
+ "observeEvents": [
+ "commandStartedEvent",
+ "commandSucceededEvent",
+ "commandFailedEvent"
+ ],
+ "ignoreCommandMonitoringEvents": [
+ "killCursors"
+ ]
+ }
+ },
+ {
+ "client": {
+ "id": "internal_client",
+ "useMultipleMongoses": false
+ }
+ },
+ {
+ "database": {
+ "id": "internal_db",
+ "client": "internal_client",
+ "databaseName": "retryable-writes-tests"
+ }
+ },
+ {
+ "collection": {
+ "id": "retryable-writes-tests",
+ "database": "internal_db",
+ "collectionName": "coll"
+ }
+ },
+ {
+ "database": {
+ "id": "database",
+ "client": "client",
+ "databaseName": "retryable-writes-tests"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection",
+ "database": "database",
+ "collectionName": "coll"
+ }
+ },
+ {
+ "client": {
+ "id": "client_retryReads_false",
+ "useMultipleMongoses": false,
+ "observeEvents": [
+ "commandStartedEvent",
+ "commandSucceededEvent",
+ "commandFailedEvent"
+ ],
+ "ignoreCommandMonitoringEvents": [
+ "killCursors"
+ ],
+ "uriOptions": {
+ "retryReads": false
+ }
+ }
+ },
+ {
+ "database": {
+ "id": "database_retryReads_false",
+ "client": "client_retryReads_false",
+ "databaseName": "retryable-writes-tests"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection_retryReads_false",
+ "database": "database_retryReads_false",
+ "collectionName": "coll"
+ }
+ },
+ {
+ "client": {
+ "id": "client_retryWrites_false",
+ "useMultipleMongoses": false,
+ "observeEvents": [
+ "commandStartedEvent",
+ "commandSucceededEvent",
+ "commandFailedEvent"
+ ],
+ "ignoreCommandMonitoringEvents": [
+ "killCursors"
+ ],
+ "uriOptions": {
+ "retryWrites": false
+ }
+ }
+ },
+ {
+ "database": {
+ "id": "database_retryWrites_false",
+ "client": "client_retryWrites_false",
+ "databaseName": "retryable-writes-tests"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection_retryWrites_false",
+ "database": "database_retryWrites_false",
+ "collectionName": "coll"
+ }
+ }
+ ],
+ "initialData": [
+ {
+ "collectionName": "coll",
+ "databaseName": "retryable-writes-tests",
+ "documents": []
+ }
+ ],
+ "_yamlAnchors": {
+ "bulWriteInsertNamespace": "retryable-writes-tests.coll"
+ },
+ "tests": [
+ {
+ "description": "client.listDatabases retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "listDatabases"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "listDatabases",
+ "object": "client",
+ "arguments": {
+ "filter": {}
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "listDatabases"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "client.listDatabases (read) does not retry if retryReads=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "listDatabases"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "listDatabases",
+ "object": "client_retryReads_false",
+ "arguments": {
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryReads_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listDatabases"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "client.listDatabaseNames retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "listDatabases"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "listDatabaseNames",
+ "object": "client"
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "listDatabases"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "client.listDatabaseNames (read) does not retry if retryReads=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "listDatabases"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "listDatabaseNames",
+ "object": "client_retryReads_false",
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryReads_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listDatabases"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "client.createChangeStream retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "client",
+ "arguments": {
+ "pipeline": []
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "client.createChangeStream (read) does not retry if retryReads=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "client_retryReads_false",
+ "arguments": {
+ "pipeline": []
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryReads_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "client.clientBulkWrite retries using operation loop",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "8.0"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "bulkWrite"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "clientBulkWrite",
+ "object": "client",
+ "arguments": {
+ "models": [
+ {
+ "insertOne": {
+ "namespace": "retryable-writes-tests.coll",
+ "document": {
+ "_id": 8,
+ "x": 88
+ }
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "bulkWrite"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "bulkWrite"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "bulkWrite"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "bulkWrite"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "bulkWrite"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "bulkWrite"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "client.clientBulkWrite (write) does not retry if retryWrites=false",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "8.0"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "bulkWrite"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "clientBulkWrite",
+ "object": "client_retryWrites_false",
+ "arguments": {
+ "models": [
+ {
+ "insertOne": {
+ "namespace": "retryable-writes-tests.coll",
+ "document": {
+ "_id": 8,
+ "x": 88
+ }
+ }
+ }
+ ]
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryWrites_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "bulkWrite"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "bulkWrite"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "database.aggregate read retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "aggregate",
+ "object": "database",
+ "arguments": {
+ "pipeline": [
+ {
+ "$listLocalSessions": {}
+ },
+ {
+ "$limit": 1
+ }
+ ]
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "database.aggregate (read) does not retry if retryReads=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "aggregate",
+ "object": "database_retryReads_false",
+ "arguments": {
+ "pipeline": [
+ {
+ "$listLocalSessions": {}
+ },
+ {
+ "$limit": 1
+ }
+ ]
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryReads_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "database.listCollections retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "listCollections"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "listCollections",
+ "object": "database",
+ "arguments": {
+ "filter": {}
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "listCollections"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "database.listCollections (read) does not retry if retryReads=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "listCollections"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "listCollections",
+ "object": "database_retryReads_false",
+ "arguments": {
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryReads_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listCollections"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "database.listCollectionNames retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "listCollections"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "listCollectionNames",
+ "object": "database",
+ "arguments": {
+ "filter": {}
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "listCollections"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "database.listCollectionNames (read) does not retry if retryReads=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "listCollections"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "listCollectionNames",
+ "object": "database_retryReads_false",
+ "arguments": {
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryReads_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listCollections"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "database.runCommand retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "ping"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "runCommand",
+ "object": "database",
+ "arguments": {
+ "command": {
+ "ping": 1
+ },
+ "commandName": "ping"
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "ping"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "ping"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "ping"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "ping"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "ping"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "ping"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "database.runCommand (read) does not retry if retryReads=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "ping"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "runCommand",
+ "object": "database_retryReads_false",
+ "arguments": {
+ "command": {
+ "ping": 1
+ },
+ "commandName": "ping"
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryReads_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "ping"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "ping"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "database.runCommand (write) does not retry if retryWrites=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "ping"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "runCommand",
+ "object": "database_retryWrites_false",
+ "arguments": {
+ "command": {
+ "ping": 1
+ },
+ "commandName": "ping"
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryWrites_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "ping"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "ping"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "database.createChangeStream retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "database",
+ "arguments": {
+ "pipeline": []
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "database.createChangeStream (read) does not retry if retryReads=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "database_retryReads_false",
+ "arguments": {
+ "pipeline": []
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryReads_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.aggregate read retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "aggregate",
+ "object": "collection",
+ "arguments": {
+ "pipeline": []
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.aggregate (read) does not retry if retryReads=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "aggregate",
+ "object": "collection_retryReads_false",
+ "arguments": {
+ "pipeline": []
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryReads_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.countDocuments retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "countDocuments",
+ "object": "collection",
+ "arguments": {
+ "filter": {}
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.countDocuments (read) does not retry if retryReads=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "countDocuments",
+ "object": "collection_retryReads_false",
+ "arguments": {
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryReads_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.estimatedDocumentCount retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "count"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "estimatedDocumentCount",
+ "object": "collection"
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "count"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "count"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "count"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "count"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "count"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "count"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.estimatedDocumentCount (read) does not retry if retryReads=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "count"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "estimatedDocumentCount",
+ "object": "collection_retryReads_false",
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryReads_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "count"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "count"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.distinct retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "distinct"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "distinct",
+ "object": "collection",
+ "arguments": {
+ "fieldName": "x",
+ "filter": {}
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "distinct"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "distinct"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "distinct"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "distinct"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "distinct"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "distinct"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.distinct (read) does not retry if retryReads=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "distinct"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "distinct",
+ "object": "collection_retryReads_false",
+ "arguments": {
+ "fieldName": "x",
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryReads_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "distinct"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "distinct"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.find retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "find"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "find",
+ "object": "collection",
+ "arguments": {
+ "filter": {}
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "find"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.find (read) does not retry if retryReads=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "find"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "find",
+ "object": "collection_retryReads_false",
+ "arguments": {
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryReads_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "find"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.findOne retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "find"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "findOne",
+ "object": "collection",
+ "arguments": {
+ "filter": {}
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "find"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.findOne (read) does not retry if retryReads=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "find"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "findOne",
+ "object": "collection_retryReads_false",
+ "arguments": {
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryReads_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "find"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.listIndexes retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "listIndexes"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "listIndexes",
+ "object": "collection"
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "listIndexes"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.listIndexes (read) does not retry if retryReads=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "listIndexes"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "listIndexes",
+ "object": "collection_retryReads_false",
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryReads_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listIndexes"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.listIndexNames retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "listIndexes"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "listIndexNames",
+ "object": "collection"
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "listIndexes"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.listIndexNames (read) does not retry if retryReads=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "listIndexes"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "listIndexNames",
+ "object": "collection_retryReads_false",
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryReads_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listIndexes"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.createChangeStream retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection",
+ "arguments": {
+ "pipeline": []
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.createChangeStream (read) does not retry if retryReads=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection_retryReads_false",
+ "arguments": {
+ "pipeline": []
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryReads_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.insertOne retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "insert"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "insertOne",
+ "object": "collection",
+ "arguments": {
+ "document": {
+ "_id": 2,
+ "x": 22
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "insert"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.insertOne (write) does not retry if retryWrites=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "insert"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "insertOne",
+ "object": "collection_retryWrites_false",
+ "arguments": {
+ "document": {
+ "_id": 2,
+ "x": 22
+ }
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryWrites_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.insertMany retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "insert"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "insertMany",
+ "object": "collection",
+ "arguments": {
+ "documents": [
+ {
+ "_id": 2,
+ "x": 22
+ }
+ ]
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "insert"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.insertMany (write) does not retry if retryWrites=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "insert"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "insertMany",
+ "object": "collection_retryWrites_false",
+ "arguments": {
+ "documents": [
+ {
+ "_id": 2,
+ "x": 22
+ }
+ ]
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryWrites_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.deleteOne retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "delete"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "deleteOne",
+ "object": "collection",
+ "arguments": {
+ "filter": {}
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "delete"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.deleteOne (write) does not retry if retryWrites=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "delete"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "deleteOne",
+ "object": "collection_retryWrites_false",
+ "arguments": {
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryWrites_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "delete"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.deleteMany retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "delete"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "deleteMany",
+ "object": "collection",
+ "arguments": {
+ "filter": {}
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "delete"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.deleteMany (write) does not retry if retryWrites=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "delete"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "deleteMany",
+ "object": "collection_retryWrites_false",
+ "arguments": {
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryWrites_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "delete"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.replaceOne retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "update"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "replaceOne",
+ "object": "collection",
+ "arguments": {
+ "filter": {},
+ "replacement": {
+ "x": 22
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "update"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.replaceOne (write) does not retry if retryWrites=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "update"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "replaceOne",
+ "object": "collection_retryWrites_false",
+ "arguments": {
+ "filter": {},
+ "replacement": {
+ "x": 22
+ }
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryWrites_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "update"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.updateOne retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "update"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "updateOne",
+ "object": "collection",
+ "arguments": {
+ "filter": {},
+ "update": {
+ "$set": {
+ "x": 22
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "update"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.updateOne (write) does not retry if retryWrites=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "update"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "updateOne",
+ "object": "collection_retryWrites_false",
+ "arguments": {
+ "filter": {},
+ "update": {
+ "$set": {
+ "x": 22
+ }
+ }
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryWrites_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "update"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.updateMany retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "update"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "updateMany",
+ "object": "collection",
+ "arguments": {
+ "filter": {},
+ "update": {
+ "$set": {
+ "x": 22
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "update"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.updateMany (write) does not retry if retryWrites=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "update"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "updateMany",
+ "object": "collection_retryWrites_false",
+ "arguments": {
+ "filter": {},
+ "update": {
+ "$set": {
+ "x": 22
+ }
+ }
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryWrites_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "update"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.findOneAndDelete retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "findAndModify"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "findOneAndDelete",
+ "object": "collection",
+ "arguments": {
+ "filter": {}
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "findAndModify"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.findOneAndDelete (write) does not retry if retryWrites=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "findAndModify"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "findOneAndDelete",
+ "object": "collection_retryWrites_false",
+ "arguments": {
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryWrites_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "findAndModify"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.findOneAndReplace retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "findAndModify"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "findOneAndReplace",
+ "object": "collection",
+ "arguments": {
+ "filter": {},
+ "replacement": {
+ "x": 22
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "findAndModify"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.findOneAndReplace (write) does not retry if retryWrites=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "findAndModify"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "findOneAndReplace",
+ "object": "collection_retryWrites_false",
+ "arguments": {
+ "filter": {},
+ "replacement": {
+ "x": 22
+ }
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryWrites_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "findAndModify"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.findOneAndUpdate retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "findAndModify"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "findOneAndUpdate",
+ "object": "collection",
+ "arguments": {
+ "filter": {},
+ "update": {
+ "$set": {
+ "x": 22
+ }
+ }
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "findAndModify"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.findOneAndUpdate (write) does not retry if retryWrites=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "findAndModify"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "findOneAndUpdate",
+ "object": "collection_retryWrites_false",
+ "arguments": {
+ "filter": {},
+ "update": {
+ "$set": {
+ "x": 22
+ }
+ }
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryWrites_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "findAndModify"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.bulkWrite retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "insert"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "bulkWrite",
+ "object": "collection",
+ "arguments": {
+ "requests": [
+ {
+ "insertOne": {
+ "document": {
+ "_id": 2,
+ "x": 22
+ }
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "insert"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.bulkWrite (write) does not retry if retryWrites=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "insert"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "bulkWrite",
+ "object": "collection_retryWrites_false",
+ "arguments": {
+ "requests": [
+ {
+ "insertOne": {
+ "document": {
+ "_id": 2,
+ "x": 22
+ }
+ }
+ }
+ ]
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryWrites_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.createIndex retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "createIndexes"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "createIndex",
+ "object": "collection",
+ "arguments": {
+ "keys": {
+ "x": 11
+ },
+ "name": "x_11"
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "createIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "createIndexes"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "createIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "createIndexes"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "createIndexes"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "createIndexes"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.createIndex (write) does not retry if retryWrites=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "createIndexes"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "createIndex",
+ "object": "collection_retryWrites_false",
+ "arguments": {
+ "keys": {
+ "x": 11
+ },
+ "name": "x_11"
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryWrites_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "createIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "createIndexes"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.dropIndex retries using operation loop",
+ "operations": [
+ {
+ "name": "createIndex",
+ "object": "retryable-writes-tests",
+ "arguments": {
+ "keys": {
+ "x": 11
+ },
+ "name": "x_11"
+ }
+ },
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "dropIndexes"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "dropIndex",
+ "object": "collection",
+ "arguments": {
+ "name": "x_11"
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "dropIndexes"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.dropIndex (write) does not retry if retryWrites=false",
+ "operations": [
+ {
+ "name": "createIndex",
+ "object": "retryable-writes-tests",
+ "arguments": {
+ "keys": {
+ "x": 11
+ },
+ "name": "x_11"
+ }
+ },
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "dropIndexes"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "dropIndex",
+ "object": "collection_retryWrites_false",
+ "arguments": {
+ "name": "x_11"
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryWrites_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "dropIndexes"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.dropIndexes retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "dropIndexes"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "dropIndexes",
+ "object": "collection"
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "dropIndexes"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.dropIndexes (write) does not retry if retryWrites=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "dropIndexes"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "dropIndexes",
+ "object": "collection_retryWrites_false",
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryWrites_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "dropIndexes"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.aggregate write retries using operation loop",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "aggregate",
+ "object": "collection",
+ "arguments": {
+ "pipeline": [
+ {
+ "$out": "output"
+ }
+ ]
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.aggregate (write) does not retry if retryWrites=false",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "internal_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 1
+ },
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "aggregate",
+ "object": "collection_retryWrites_false",
+ "arguments": {
+ "pipeline": [
+ {
+ "$out": "output"
+ }
+ ]
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client_retryWrites_false",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/source/client-backpressure/tests/backpressure-retry-loop.yml b/source/client-backpressure/tests/backpressure-retry-loop.yml
new file mode 100644
index 0000000000..902726f626
--- /dev/null
+++ b/source/client-backpressure/tests/backpressure-retry-loop.yml
@@ -0,0 +1,2282 @@
+# Tests in this file are generated from backpressure-retry-loop.yml.template.
+
+description: tests that operations respect overload backoff retry loop
+
+schemaVersion: '1.3'
+
+runOnRequirements:
+ - minServerVersion: '4.4' # failCommand
+ topologies: [replicaset, sharded, load-balanced]
+
+createEntities:
+ - client:
+ id: &client client
+ useMultipleMongoses: false
+ observeEvents: [commandStartedEvent, commandSucceededEvent, commandFailedEvent]
+ ignoreCommandMonitoringEvents: [killCursors]
+
+ - client:
+ id: &internal_client internal_client
+ useMultipleMongoses: false
+
+ - database:
+ id: &internal_db internal_db
+ client: *internal_client
+ databaseName: &database_name retryable-writes-tests
+
+ - collection:
+ id: &internal_collection retryable-writes-tests
+ database: *internal_db
+ collectionName: &collection_name coll
+
+ - database:
+ id: &database database
+ client: *client
+ databaseName: *database_name
+
+ - collection:
+ id: &collection collection
+ database: *database
+ collectionName: *collection_name
+
+ - client:
+ id: &client_retryReads_false client_retryReads_false
+ useMultipleMongoses: false
+ observeEvents: [commandStartedEvent, commandSucceededEvent, commandFailedEvent]
+ ignoreCommandMonitoringEvents: [killCursors]
+ uriOptions:
+ retryReads: false
+
+ - database:
+ id: &database_retryReads_false database_retryReads_false
+ client: *client_retryReads_false
+ databaseName: *database_name
+
+ - collection:
+ id: &collection_retryReads_false collection_retryReads_false
+ database: *database_retryReads_false
+ collectionName: *collection_name
+
+ - client:
+ id: &client_retryWrites_false client_retryWrites_false
+ useMultipleMongoses: false
+ observeEvents: [commandStartedEvent, commandSucceededEvent, commandFailedEvent]
+ ignoreCommandMonitoringEvents: [killCursors]
+ uriOptions:
+ retryWrites: false
+
+ - database:
+ id: &database_retryWrites_false database_retryWrites_false
+ client: *client_retryWrites_false
+ databaseName: *database_name
+
+ - collection:
+ id: &collection_retryWrites_false collection_retryWrites_false
+ database: *database_retryWrites_false
+ collectionName: *collection_name
+
+initialData:
+- collectionName: *collection_name
+ databaseName: *database_name
+ documents: []
+
+_yamlAnchors:
+ bulWriteInsertNamespace: &client_bulk_write_ns retryable-writes-tests.coll
+
+tests:
+ - description: 'client.listDatabases retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [listDatabases]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: listDatabases
+ object: *client
+ arguments:
+ filter: {}
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: listDatabases
+ - commandFailedEvent:
+ commandName: listDatabases
+ - commandStartedEvent:
+ commandName: listDatabases
+ - commandFailedEvent:
+ commandName: listDatabases
+ - commandStartedEvent:
+ commandName: listDatabases
+ - commandSucceededEvent:
+ commandName: listDatabases
+ - description: 'client.listDatabases (read) does not retry if retryReads=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [listDatabases]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: listDatabases
+ object: *client_retryReads_false
+ arguments:
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryReads_false
+ events:
+ - commandStartedEvent:
+ commandName: listDatabases
+ - commandFailedEvent:
+ commandName: listDatabases
+
+
+ - description: 'client.listDatabaseNames retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [listDatabases]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: listDatabaseNames
+ object: *client
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: listDatabases
+ - commandFailedEvent:
+ commandName: listDatabases
+ - commandStartedEvent:
+ commandName: listDatabases
+ - commandFailedEvent:
+ commandName: listDatabases
+ - commandStartedEvent:
+ commandName: listDatabases
+ - commandSucceededEvent:
+ commandName: listDatabases
+ - description: 'client.listDatabaseNames (read) does not retry if retryReads=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [listDatabases]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: listDatabaseNames
+ object: *client_retryReads_false
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryReads_false
+ events:
+ - commandStartedEvent:
+ commandName: listDatabases
+ - commandFailedEvent:
+ commandName: listDatabases
+
+
+ - description: 'client.createChangeStream retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: createChangeStream
+ object: *client
+ arguments:
+ pipeline: []
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandSucceededEvent:
+ commandName: aggregate
+ - description: 'client.createChangeStream (read) does not retry if retryReads=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: createChangeStream
+ object: *client_retryReads_false
+ arguments:
+ pipeline: []
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryReads_false
+ events:
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+
+
+ - description: 'client.clientBulkWrite retries using operation loop'
+ runOnRequirements:
+ - minServerVersion: '8.0' # client bulk write added to server in 8.0
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [bulkWrite]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: clientBulkWrite
+ object: *client
+ arguments:
+ models:
+ - insertOne:
+ namespace: *client_bulk_write_ns
+ document: { _id: 8, x: 88 }
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: bulkWrite
+ - commandFailedEvent:
+ commandName: bulkWrite
+ - commandStartedEvent:
+ commandName: bulkWrite
+ - commandFailedEvent:
+ commandName: bulkWrite
+ - commandStartedEvent:
+ commandName: bulkWrite
+ - commandSucceededEvent:
+ commandName: bulkWrite
+ - description: 'client.clientBulkWrite (write) does not retry if retryWrites=false'
+ runOnRequirements:
+ - minServerVersion: '8.0' # client bulk write added to server in 8.0
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [bulkWrite]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: clientBulkWrite
+ object: *client_retryWrites_false
+ arguments:
+ models:
+ - insertOne:
+ namespace: *client_bulk_write_ns
+ document: { _id: 8, x: 88 }
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryWrites_false
+ events:
+ - commandStartedEvent:
+ commandName: bulkWrite
+ - commandFailedEvent:
+ commandName: bulkWrite
+
+
+ - description: 'database.aggregate read retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: aggregate
+ object: *database
+ arguments:
+ pipeline: [ { $listLocalSessions: {} }, { $limit: 1 } ]
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandSucceededEvent:
+ commandName: aggregate
+ - description: 'database.aggregate (read) does not retry if retryReads=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: aggregate
+ object: *database_retryReads_false
+ arguments:
+ pipeline: [ { $listLocalSessions: {} }, { $limit: 1 } ]
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryReads_false
+ events:
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+
+
+ - description: 'database.listCollections retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [listCollections]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: listCollections
+ object: *database
+ arguments:
+ filter: {}
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: listCollections
+ - commandFailedEvent:
+ commandName: listCollections
+ - commandStartedEvent:
+ commandName: listCollections
+ - commandFailedEvent:
+ commandName: listCollections
+ - commandStartedEvent:
+ commandName: listCollections
+ - commandSucceededEvent:
+ commandName: listCollections
+ - description: 'database.listCollections (read) does not retry if retryReads=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [listCollections]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: listCollections
+ object: *database_retryReads_false
+ arguments:
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryReads_false
+ events:
+ - commandStartedEvent:
+ commandName: listCollections
+ - commandFailedEvent:
+ commandName: listCollections
+
+
+ - description: 'database.listCollectionNames retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [listCollections]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: listCollectionNames
+ object: *database
+ arguments:
+ filter: {}
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: listCollections
+ - commandFailedEvent:
+ commandName: listCollections
+ - commandStartedEvent:
+ commandName: listCollections
+ - commandFailedEvent:
+ commandName: listCollections
+ - commandStartedEvent:
+ commandName: listCollections
+ - commandSucceededEvent:
+ commandName: listCollections
+ - description: 'database.listCollectionNames (read) does not retry if retryReads=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [listCollections]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: listCollectionNames
+ object: *database_retryReads_false
+ arguments:
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryReads_false
+ events:
+ - commandStartedEvent:
+ commandName: listCollections
+ - commandFailedEvent:
+ commandName: listCollections
+
+
+ - description: 'database.runCommand retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [ping]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: runCommand
+ object: *database
+ arguments:
+ command: { ping: 1 }
+ commandName: ping
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: ping
+ - commandFailedEvent:
+ commandName: ping
+ - commandStartedEvent:
+ commandName: ping
+ - commandFailedEvent:
+ commandName: ping
+ - commandStartedEvent:
+ commandName: ping
+ - commandSucceededEvent:
+ commandName: ping
+ - description: 'database.runCommand (read) does not retry if retryReads=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ping]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: runCommand
+ object: *database_retryReads_false
+ arguments:
+ command: { ping: 1 }
+ commandName: ping
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryReads_false
+ events:
+ - commandStartedEvent:
+ commandName: ping
+ - commandFailedEvent:
+ commandName: ping
+ - description: 'database.runCommand (write) does not retry if retryWrites=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [ping]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: runCommand
+ object: *database_retryWrites_false
+ arguments:
+ command: { ping: 1 }
+ commandName: ping
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryWrites_false
+ events:
+ - commandStartedEvent:
+ commandName: ping
+ - commandFailedEvent:
+ commandName: ping
+
+
+ - description: 'database.createChangeStream retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: createChangeStream
+ object: *database
+ arguments:
+ pipeline: []
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandSucceededEvent:
+ commandName: aggregate
+ - description: 'database.createChangeStream (read) does not retry if retryReads=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: createChangeStream
+ object: *database_retryReads_false
+ arguments:
+ pipeline: []
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryReads_false
+ events:
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+
+
+ - description: 'collection.aggregate read retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: aggregate
+ object: *collection
+ arguments:
+ pipeline: []
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandSucceededEvent:
+ commandName: aggregate
+ - description: 'collection.aggregate (read) does not retry if retryReads=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: aggregate
+ object: *collection_retryReads_false
+ arguments:
+ pipeline: []
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryReads_false
+ events:
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+
+
+ - description: 'collection.countDocuments retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: countDocuments
+ object: *collection
+ arguments:
+ filter: {}
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandSucceededEvent:
+ commandName: aggregate
+ - description: 'collection.countDocuments (read) does not retry if retryReads=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: countDocuments
+ object: *collection_retryReads_false
+ arguments:
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryReads_false
+ events:
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+
+
+ - description: 'collection.estimatedDocumentCount retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [count]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: estimatedDocumentCount
+ object: *collection
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: count
+ - commandFailedEvent:
+ commandName: count
+ - commandStartedEvent:
+ commandName: count
+ - commandFailedEvent:
+ commandName: count
+ - commandStartedEvent:
+ commandName: count
+ - commandSucceededEvent:
+ commandName: count
+ - description: 'collection.estimatedDocumentCount (read) does not retry if retryReads=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [count]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: estimatedDocumentCount
+ object: *collection_retryReads_false
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryReads_false
+ events:
+ - commandStartedEvent:
+ commandName: count
+ - commandFailedEvent:
+ commandName: count
+
+
+ - description: 'collection.distinct retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [distinct]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: distinct
+ object: *collection
+ arguments:
+ fieldName: x
+ filter: {}
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: distinct
+ - commandFailedEvent:
+ commandName: distinct
+ - commandStartedEvent:
+ commandName: distinct
+ - commandFailedEvent:
+ commandName: distinct
+ - commandStartedEvent:
+ commandName: distinct
+ - commandSucceededEvent:
+ commandName: distinct
+ - description: 'collection.distinct (read) does not retry if retryReads=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [distinct]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: distinct
+ object: *collection_retryReads_false
+ arguments:
+ fieldName: x
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryReads_false
+ events:
+ - commandStartedEvent:
+ commandName: distinct
+ - commandFailedEvent:
+ commandName: distinct
+
+
+ - description: 'collection.find retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [find]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: find
+ object: *collection
+ arguments:
+ filter: {}
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: find
+ - commandFailedEvent:
+ commandName: find
+ - commandStartedEvent:
+ commandName: find
+ - commandFailedEvent:
+ commandName: find
+ - commandStartedEvent:
+ commandName: find
+ - commandSucceededEvent:
+ commandName: find
+ - description: 'collection.find (read) does not retry if retryReads=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [find]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: find
+ object: *collection_retryReads_false
+ arguments:
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryReads_false
+ events:
+ - commandStartedEvent:
+ commandName: find
+ - commandFailedEvent:
+ commandName: find
+
+
+ - description: 'collection.findOne retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [find]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: findOne
+ object: *collection
+ arguments:
+ filter: {}
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: find
+ - commandFailedEvent:
+ commandName: find
+ - commandStartedEvent:
+ commandName: find
+ - commandFailedEvent:
+ commandName: find
+ - commandStartedEvent:
+ commandName: find
+ - commandSucceededEvent:
+ commandName: find
+ - description: 'collection.findOne (read) does not retry if retryReads=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [find]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: findOne
+ object: *collection_retryReads_false
+ arguments:
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryReads_false
+ events:
+ - commandStartedEvent:
+ commandName: find
+ - commandFailedEvent:
+ commandName: find
+
+
+ - description: 'collection.listIndexes retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [listIndexes]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: listIndexes
+ object: *collection
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: listIndexes
+ - commandFailedEvent:
+ commandName: listIndexes
+ - commandStartedEvent:
+ commandName: listIndexes
+ - commandFailedEvent:
+ commandName: listIndexes
+ - commandStartedEvent:
+ commandName: listIndexes
+ - commandSucceededEvent:
+ commandName: listIndexes
+ - description: 'collection.listIndexes (read) does not retry if retryReads=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [listIndexes]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: listIndexes
+ object: *collection_retryReads_false
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryReads_false
+ events:
+ - commandStartedEvent:
+ commandName: listIndexes
+ - commandFailedEvent:
+ commandName: listIndexes
+
+
+ - description: 'collection.listIndexNames retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [listIndexes]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: listIndexNames
+ object: *collection
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: listIndexes
+ - commandFailedEvent:
+ commandName: listIndexes
+ - commandStartedEvent:
+ commandName: listIndexes
+ - commandFailedEvent:
+ commandName: listIndexes
+ - commandStartedEvent:
+ commandName: listIndexes
+ - commandSucceededEvent:
+ commandName: listIndexes
+ - description: 'collection.listIndexNames (read) does not retry if retryReads=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [listIndexes]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: listIndexNames
+ object: *collection_retryReads_false
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryReads_false
+ events:
+ - commandStartedEvent:
+ commandName: listIndexes
+ - commandFailedEvent:
+ commandName: listIndexes
+
+
+ - description: 'collection.createChangeStream retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: createChangeStream
+ object: *collection
+ arguments:
+ pipeline: []
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandSucceededEvent:
+ commandName: aggregate
+ - description: 'collection.createChangeStream (read) does not retry if retryReads=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: createChangeStream
+ object: *collection_retryReads_false
+ arguments:
+ pipeline: []
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryReads_false
+ events:
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+
+
+ - description: 'collection.insertOne retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [insert]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: insertOne
+ object: *collection
+ arguments:
+ document: { _id: 2, x: 22 }
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: insert
+ - commandFailedEvent:
+ commandName: insert
+ - commandStartedEvent:
+ commandName: insert
+ - commandFailedEvent:
+ commandName: insert
+ - commandStartedEvent:
+ commandName: insert
+ - commandSucceededEvent:
+ commandName: insert
+ - description: 'collection.insertOne (write) does not retry if retryWrites=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [insert]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: insertOne
+ object: *collection_retryWrites_false
+ arguments:
+ document: { _id: 2, x: 22 }
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryWrites_false
+ events:
+ - commandStartedEvent:
+ commandName: insert
+ - commandFailedEvent:
+ commandName: insert
+
+
+ - description: 'collection.insertMany retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [insert]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: insertMany
+ object: *collection
+ arguments:
+ documents:
+ - { _id: 2, x: 22 }
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: insert
+ - commandFailedEvent:
+ commandName: insert
+ - commandStartedEvent:
+ commandName: insert
+ - commandFailedEvent:
+ commandName: insert
+ - commandStartedEvent:
+ commandName: insert
+ - commandSucceededEvent:
+ commandName: insert
+ - description: 'collection.insertMany (write) does not retry if retryWrites=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [insert]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: insertMany
+ object: *collection_retryWrites_false
+ arguments:
+ documents:
+ - { _id: 2, x: 22 }
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryWrites_false
+ events:
+ - commandStartedEvent:
+ commandName: insert
+ - commandFailedEvent:
+ commandName: insert
+
+
+ - description: 'collection.deleteOne retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [delete]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: deleteOne
+ object: *collection
+ arguments:
+ filter: {}
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: delete
+ - commandFailedEvent:
+ commandName: delete
+ - commandStartedEvent:
+ commandName: delete
+ - commandFailedEvent:
+ commandName: delete
+ - commandStartedEvent:
+ commandName: delete
+ - commandSucceededEvent:
+ commandName: delete
+ - description: 'collection.deleteOne (write) does not retry if retryWrites=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [delete]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: deleteOne
+ object: *collection_retryWrites_false
+ arguments:
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryWrites_false
+ events:
+ - commandStartedEvent:
+ commandName: delete
+ - commandFailedEvent:
+ commandName: delete
+
+
+ - description: 'collection.deleteMany retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [delete]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: deleteMany
+ object: *collection
+ arguments:
+ filter: {}
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: delete
+ - commandFailedEvent:
+ commandName: delete
+ - commandStartedEvent:
+ commandName: delete
+ - commandFailedEvent:
+ commandName: delete
+ - commandStartedEvent:
+ commandName: delete
+ - commandSucceededEvent:
+ commandName: delete
+ - description: 'collection.deleteMany (write) does not retry if retryWrites=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [delete]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: deleteMany
+ object: *collection_retryWrites_false
+ arguments:
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryWrites_false
+ events:
+ - commandStartedEvent:
+ commandName: delete
+ - commandFailedEvent:
+ commandName: delete
+
+
+ - description: 'collection.replaceOne retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [update]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: replaceOne
+ object: *collection
+ arguments:
+ filter: {}
+ replacement: { x: 22 }
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: update
+ - commandFailedEvent:
+ commandName: update
+ - commandStartedEvent:
+ commandName: update
+ - commandFailedEvent:
+ commandName: update
+ - commandStartedEvent:
+ commandName: update
+ - commandSucceededEvent:
+ commandName: update
+ - description: 'collection.replaceOne (write) does not retry if retryWrites=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [update]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: replaceOne
+ object: *collection_retryWrites_false
+ arguments:
+ filter: {}
+ replacement: { x: 22 }
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryWrites_false
+ events:
+ - commandStartedEvent:
+ commandName: update
+ - commandFailedEvent:
+ commandName: update
+
+
+ - description: 'collection.updateOne retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [update]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: updateOne
+ object: *collection
+ arguments:
+ filter: {}
+ update: { $set: { x: 22 } }
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: update
+ - commandFailedEvent:
+ commandName: update
+ - commandStartedEvent:
+ commandName: update
+ - commandFailedEvent:
+ commandName: update
+ - commandStartedEvent:
+ commandName: update
+ - commandSucceededEvent:
+ commandName: update
+ - description: 'collection.updateOne (write) does not retry if retryWrites=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [update]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: updateOne
+ object: *collection_retryWrites_false
+ arguments:
+ filter: {}
+ update: { $set: { x: 22 } }
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryWrites_false
+ events:
+ - commandStartedEvent:
+ commandName: update
+ - commandFailedEvent:
+ commandName: update
+
+
+ - description: 'collection.updateMany retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [update]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: updateMany
+ object: *collection
+ arguments:
+ filter: {}
+ update: { $set: { x: 22 } }
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: update
+ - commandFailedEvent:
+ commandName: update
+ - commandStartedEvent:
+ commandName: update
+ - commandFailedEvent:
+ commandName: update
+ - commandStartedEvent:
+ commandName: update
+ - commandSucceededEvent:
+ commandName: update
+ - description: 'collection.updateMany (write) does not retry if retryWrites=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [update]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: updateMany
+ object: *collection_retryWrites_false
+ arguments:
+ filter: {}
+ update: { $set: { x: 22 } }
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryWrites_false
+ events:
+ - commandStartedEvent:
+ commandName: update
+ - commandFailedEvent:
+ commandName: update
+
+
+ - description: 'collection.findOneAndDelete retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [findAndModify]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: findOneAndDelete
+ object: *collection
+ arguments:
+ filter: {}
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandFailedEvent:
+ commandName: findAndModify
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandFailedEvent:
+ commandName: findAndModify
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandSucceededEvent:
+ commandName: findAndModify
+ - description: 'collection.findOneAndDelete (write) does not retry if retryWrites=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [findAndModify]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: findOneAndDelete
+ object: *collection_retryWrites_false
+ arguments:
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryWrites_false
+ events:
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandFailedEvent:
+ commandName: findAndModify
+
+
+ - description: 'collection.findOneAndReplace retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [findAndModify]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: findOneAndReplace
+ object: *collection
+ arguments:
+ filter: {}
+ replacement: { x: 22 }
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandFailedEvent:
+ commandName: findAndModify
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandFailedEvent:
+ commandName: findAndModify
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandSucceededEvent:
+ commandName: findAndModify
+ - description: 'collection.findOneAndReplace (write) does not retry if retryWrites=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [findAndModify]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: findOneAndReplace
+ object: *collection_retryWrites_false
+ arguments:
+ filter: {}
+ replacement: { x: 22 }
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryWrites_false
+ events:
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandFailedEvent:
+ commandName: findAndModify
+
+
+ - description: 'collection.findOneAndUpdate retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [findAndModify]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: findOneAndUpdate
+ object: *collection
+ arguments:
+ filter: {}
+ update: { $set: { x: 22 } }
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandFailedEvent:
+ commandName: findAndModify
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandFailedEvent:
+ commandName: findAndModify
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandSucceededEvent:
+ commandName: findAndModify
+ - description: 'collection.findOneAndUpdate (write) does not retry if retryWrites=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [findAndModify]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: findOneAndUpdate
+ object: *collection_retryWrites_false
+ arguments:
+ filter: {}
+ update: { $set: { x: 22 } }
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryWrites_false
+ events:
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandFailedEvent:
+ commandName: findAndModify
+
+
+ - description: 'collection.bulkWrite retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [insert]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: bulkWrite
+ object: *collection
+ arguments:
+ requests:
+ - insertOne:
+ document: { _id: 2, x: 22 }
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: insert
+ - commandFailedEvent:
+ commandName: insert
+ - commandStartedEvent:
+ commandName: insert
+ - commandFailedEvent:
+ commandName: insert
+ - commandStartedEvent:
+ commandName: insert
+ - commandSucceededEvent:
+ commandName: insert
+ - description: 'collection.bulkWrite (write) does not retry if retryWrites=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [insert]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: bulkWrite
+ object: *collection_retryWrites_false
+ arguments:
+ requests:
+ - insertOne:
+ document: { _id: 2, x: 22 }
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryWrites_false
+ events:
+ - commandStartedEvent:
+ commandName: insert
+ - commandFailedEvent:
+ commandName: insert
+
+
+ - description: 'collection.createIndex retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [createIndexes]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: createIndex
+ object: *collection
+ arguments:
+ keys: { x: 11 }
+ name: "x_11"
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: createIndexes
+ - commandFailedEvent:
+ commandName: createIndexes
+ - commandStartedEvent:
+ commandName: createIndexes
+ - commandFailedEvent:
+ commandName: createIndexes
+ - commandStartedEvent:
+ commandName: createIndexes
+ - commandSucceededEvent:
+ commandName: createIndexes
+ - description: 'collection.createIndex (write) does not retry if retryWrites=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [createIndexes]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: createIndex
+ object: *collection_retryWrites_false
+ arguments:
+ keys: { x: 11 }
+ name: "x_11"
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryWrites_false
+ events:
+ - commandStartedEvent:
+ commandName: createIndexes
+ - commandFailedEvent:
+ commandName: createIndexes
+
+
+ - description: 'collection.dropIndex retries using operation loop'
+ operations:
+ - name: createIndex
+ object: *internal_collection
+ arguments:
+ keys: { x: 11 }
+ name: "x_11"
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [dropIndexes]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: dropIndex
+ object: *collection
+ arguments:
+ name: "x_11"
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: dropIndexes
+ - commandFailedEvent:
+ commandName: dropIndexes
+ - commandStartedEvent:
+ commandName: dropIndexes
+ - commandFailedEvent:
+ commandName: dropIndexes
+ - commandStartedEvent:
+ commandName: dropIndexes
+ - commandSucceededEvent:
+ commandName: dropIndexes
+ - description: 'collection.dropIndex (write) does not retry if retryWrites=false'
+ operations:
+ - name: createIndex
+ object: *internal_collection
+ arguments:
+ keys: { x: 11 }
+ name: "x_11"
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [dropIndexes]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: dropIndex
+ object: *collection_retryWrites_false
+ arguments:
+ name: "x_11"
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryWrites_false
+ events:
+ - commandStartedEvent:
+ commandName: dropIndexes
+ - commandFailedEvent:
+ commandName: dropIndexes
+
+
+ - description: 'collection.dropIndexes retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [dropIndexes]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: dropIndexes
+ object: *collection
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: dropIndexes
+ - commandFailedEvent:
+ commandName: dropIndexes
+ - commandStartedEvent:
+ commandName: dropIndexes
+ - commandFailedEvent:
+ commandName: dropIndexes
+ - commandStartedEvent:
+ commandName: dropIndexes
+ - commandSucceededEvent:
+ commandName: dropIndexes
+ - description: 'collection.dropIndexes (write) does not retry if retryWrites=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [dropIndexes]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: dropIndexes
+ object: *collection_retryWrites_false
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryWrites_false
+ events:
+ - commandStartedEvent:
+ commandName: dropIndexes
+ - commandFailedEvent:
+ commandName: dropIndexes
+
+
+ - description: 'collection.aggregate write retries using operation loop'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: aggregate
+ object: *collection
+ arguments:
+ pipeline: [{$out: "output"}]
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandSucceededEvent:
+ commandName: aggregate
+ - description: 'collection.aggregate (write) does not retry if retryWrites=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: aggregate
+ object: *collection_retryWrites_false
+ arguments:
+ pipeline: [{$out: "output"}]
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryWrites_false
+ events:
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+
diff --git a/source/client-backpressure/tests/backpressure-retry-loop.yml.template b/source/client-backpressure/tests/backpressure-retry-loop.yml.template
new file mode 100644
index 0000000000..1825b2b20d
--- /dev/null
+++ b/source/client-backpressure/tests/backpressure-retry-loop.yml.template
@@ -0,0 +1,216 @@
+# Tests in this file are generated from backpressure-retry-loop.yml.template.
+
+description: tests that operations respect overload backoff retry loop
+
+schemaVersion: '1.3'
+
+runOnRequirements:
+ - minServerVersion: '4.4' # failCommand
+ topologies: [replicaset, sharded, load-balanced]
+
+createEntities:
+ - client:
+ id: &client client
+ useMultipleMongoses: false
+ observeEvents: [commandStartedEvent, commandSucceededEvent, commandFailedEvent]
+ ignoreCommandMonitoringEvents: [killCursors]
+
+ - client:
+ id: &internal_client internal_client
+ useMultipleMongoses: false
+
+ - database:
+ id: &internal_db internal_db
+ client: *internal_client
+ databaseName: &database_name retryable-writes-tests
+
+ - collection:
+ id: &internal_collection retryable-writes-tests
+ database: *internal_db
+ collectionName: &collection_name coll
+
+ - database:
+ id: &database database
+ client: *client
+ databaseName: *database_name
+
+ - collection:
+ id: &collection collection
+ database: *database
+ collectionName: *collection_name
+
+ - client:
+ id: &client_retryReads_false client_retryReads_false
+ useMultipleMongoses: false
+ observeEvents: [commandStartedEvent, commandSucceededEvent, commandFailedEvent]
+ ignoreCommandMonitoringEvents: [killCursors]
+ uriOptions:
+ retryReads: false
+
+ - database:
+ id: &database_retryReads_false database_retryReads_false
+ client: *client_retryReads_false
+ databaseName: *database_name
+
+ - collection:
+ id: &collection_retryReads_false collection_retryReads_false
+ database: *database_retryReads_false
+ collectionName: *collection_name
+
+ - client:
+ id: &client_retryWrites_false client_retryWrites_false
+ useMultipleMongoses: false
+ observeEvents: [commandStartedEvent, commandSucceededEvent, commandFailedEvent]
+ ignoreCommandMonitoringEvents: [killCursors]
+ uriOptions:
+ retryWrites: false
+
+ - database:
+ id: &database_retryWrites_false database_retryWrites_false
+ client: *client_retryWrites_false
+ databaseName: *database_name
+
+ - collection:
+ id: &collection_retryWrites_false collection_retryWrites_false
+ database: *database_retryWrites_false
+ collectionName: *collection_name
+
+initialData:
+- collectionName: *collection_name
+ databaseName: *database_name
+ documents: []
+
+_yamlAnchors:
+ bulWriteInsertNamespace: &client_bulk_write_ns retryable-writes-tests.coll
+
+tests: {% for operation in operations %}
+ - description: '{{operation.object}}.{{operation.operation_name}}{{ " " + operation.operation_type + "" if operation.operation_name == "aggregate" else "" }} retries using operation loop' {%- if ((operation.operation_name == 'clientBulkWrite')) %}
+ runOnRequirements:
+ - minServerVersion: '8.0' # client bulk write added to server in 8.0
+ {%- endif %}
+ operations: {%- if operation.operation_name == "dropIndex" %}
+ - name: createIndex
+ object: *internal_collection
+ arguments:
+ keys: { x: 11 }
+ name: "x_11"
+ {%- endif %}
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [{{operation.command_name}}]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: {{operation.operation_name}}
+ object: *{{operation.object}}
+ {%- if operation.arguments|length > 0 %}
+ arguments:
+ {%- for arg in operation.arguments %}
+ {{arg}}
+ {%- endfor -%}
+ {%- endif %}
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: {{operation.command_name}}
+ - commandFailedEvent:
+ commandName: {{operation.command_name}}
+ - commandStartedEvent:
+ commandName: {{operation.command_name}}
+ - commandFailedEvent:
+ commandName: {{operation.command_name}}
+ - commandStartedEvent:
+ commandName: {{operation.command_name}}
+ - commandSucceededEvent:
+ commandName: {{operation.command_name}}
+
+ {%- if operation.operation_type == 'read' or operation.operation_name == "runCommand" %}
+ - description: '{{operation.object}}.{{operation.operation_name}} (read) does not retry if retryReads=false'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [{{operation.command_name}}]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: {{operation.operation_name}}
+ object: *{{operation.object}}_retryReads_false
+ {%- if operation.arguments|length > 0 %}
+ arguments:
+ {%- for arg in operation.arguments %}
+ {{arg}}
+ {%- endfor -%}
+ {%- endif %}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryReads_false
+ events:
+ - commandStartedEvent:
+ commandName: {{operation.command_name}}
+ - commandFailedEvent:
+ commandName: {{operation.command_name}}
+ {%- endif %}
+
+ {%- if operation.operation_type == 'write' or operation.operation_name == "runCommand" %}
+ - description: '{{operation.object}}.{{operation.operation_name}} (write) does not retry if retryWrites=false' {%- if ((operation.operation_name == 'clientBulkWrite')) %}
+ runOnRequirements:
+ - minServerVersion: '8.0' # client bulk write added to server in 8.0
+ {%- endif %}
+ operations: {%- if operation.operation_name == "dropIndex" %}
+ - name: createIndex
+ object: *internal_collection
+ arguments:
+ keys: { x: 11 }
+ name: "x_11"
+ {%- endif %}
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *internal_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 1 }
+ data:
+ failCommands: [{{operation.command_name}}]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: {{operation.operation_name}}
+ object: *{{operation.object}}_retryWrites_false
+ {%- if operation.arguments|length > 0 %}
+ arguments:
+ {%- for arg in operation.arguments %}
+ {{arg}}
+ {%- endfor -%}
+ {%- endif %}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client_retryWrites_false
+ events:
+ - commandStartedEvent:
+ commandName: {{operation.command_name}}
+ - commandFailedEvent:
+ commandName: {{operation.command_name}}
+ {%- endif %}
+
+{% endfor -%}
diff --git a/source/client-backpressure/tests/backpressure-retry-max-attempts.json b/source/client-backpressure/tests/backpressure-retry-max-attempts.json
new file mode 100644
index 0000000000..de52572765
--- /dev/null
+++ b/source/client-backpressure/tests/backpressure-retry-max-attempts.json
@@ -0,0 +1,2569 @@
+{
+ "description": "tests that operations retry at most maxAttempts=2 times",
+ "schemaVersion": "1.3",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.4",
+ "topologies": [
+ "replicaset",
+ "sharded",
+ "load-balanced"
+ ]
+ }
+ ],
+ "createEntities": [
+ {
+ "client": {
+ "id": "client",
+ "useMultipleMongoses": false,
+ "observeEvents": [
+ "commandStartedEvent",
+ "commandSucceededEvent",
+ "commandFailedEvent"
+ ],
+ "ignoreCommandMonitoringEvents": [
+ "killCursors"
+ ]
+ }
+ },
+ {
+ "client": {
+ "id": "fail_point_client",
+ "useMultipleMongoses": false
+ }
+ },
+ {
+ "database": {
+ "id": "database",
+ "client": "client",
+ "databaseName": "retryable-writes-tests"
+ }
+ },
+ {
+ "collection": {
+ "id": "collection",
+ "database": "database",
+ "collectionName": "coll"
+ }
+ }
+ ],
+ "_yamlAnchors": {
+ "bulkWriteInsertNamespace": "retryable-writes-tests.coll"
+ },
+ "initialData": [
+ {
+ "collectionName": "coll",
+ "databaseName": "retryable-writes-tests",
+ "documents": [
+ {
+ "_id": 1,
+ "x": 11
+ },
+ {
+ "_id": 2,
+ "x": 22
+ }
+ ]
+ }
+ ],
+ "tests": [
+ {
+ "description": "client.listDatabases retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "listDatabases"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "listDatabases",
+ "object": "client",
+ "arguments": {
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listDatabases"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "client.listDatabaseNames retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "listDatabases"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "listDatabaseNames",
+ "object": "client",
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listDatabases"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listDatabases"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "client.createChangeStream retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "client",
+ "arguments": {
+ "pipeline": []
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "client.clientBulkWrite retries at most maxAttempts=2 times",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "8.0"
+ }
+ ],
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "bulkWrite"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "clientBulkWrite",
+ "object": "client",
+ "arguments": {
+ "models": [
+ {
+ "insertOne": {
+ "namespace": "retryable-writes-tests.coll",
+ "document": {
+ "_id": 8,
+ "x": 88
+ }
+ }
+ }
+ ]
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "bulkWrite"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "bulkWrite"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "bulkWrite"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "bulkWrite"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "bulkWrite"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "bulkWrite"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "database.aggregate read retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "aggregate",
+ "object": "database",
+ "arguments": {
+ "pipeline": [
+ {
+ "$listLocalSessions": {}
+ },
+ {
+ "$limit": 1
+ }
+ ]
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "database.listCollections retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "listCollections"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "listCollections",
+ "object": "database",
+ "arguments": {
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listCollections"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "database.listCollectionNames retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "listCollections"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "listCollectionNames",
+ "object": "database",
+ "arguments": {
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listCollections"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listCollections"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "database.runCommand retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "ping"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "runCommand",
+ "object": "database",
+ "arguments": {
+ "command": {
+ "ping": 1
+ },
+ "commandName": "ping"
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "ping"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "ping"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "ping"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "ping"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "ping"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "ping"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "database.createChangeStream retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "database",
+ "arguments": {
+ "pipeline": []
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.aggregate read retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "aggregate",
+ "object": "collection",
+ "arguments": {
+ "pipeline": []
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.countDocuments retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "countDocuments",
+ "object": "collection",
+ "arguments": {
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.estimatedDocumentCount retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "count"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "estimatedDocumentCount",
+ "object": "collection",
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "count"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "count"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "count"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "count"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "count"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "count"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.distinct retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "distinct"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "distinct",
+ "object": "collection",
+ "arguments": {
+ "fieldName": "x",
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "distinct"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "distinct"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "distinct"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "distinct"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "distinct"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "distinct"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.find retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "find"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "find",
+ "object": "collection",
+ "arguments": {
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "find"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.findOne retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "find"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "findOne",
+ "object": "collection",
+ "arguments": {
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "find"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.listIndexes retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "listIndexes"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "listIndexes",
+ "object": "collection",
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listIndexes"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.listIndexNames retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "listIndexes"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "listIndexNames",
+ "object": "collection",
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "listIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "listIndexes"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.createChangeStream retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "createChangeStream",
+ "object": "collection",
+ "arguments": {
+ "pipeline": []
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.insertOne retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "insert"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "insertOne",
+ "object": "collection",
+ "arguments": {
+ "document": {
+ "_id": 2,
+ "x": 22
+ }
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.insertMany retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "insert"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "insertMany",
+ "object": "collection",
+ "arguments": {
+ "documents": [
+ {
+ "_id": 2,
+ "x": 22
+ }
+ ]
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.deleteOne retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "delete"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "deleteOne",
+ "object": "collection",
+ "arguments": {
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "delete"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.deleteMany retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "delete"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "deleteMany",
+ "object": "collection",
+ "arguments": {
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "delete"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "delete"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.replaceOne retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "update"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "replaceOne",
+ "object": "collection",
+ "arguments": {
+ "filter": {},
+ "replacement": {
+ "x": 22
+ }
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "update"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.updateOne retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "update"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "updateOne",
+ "object": "collection",
+ "arguments": {
+ "filter": {},
+ "update": {
+ "$set": {
+ "x": 22
+ }
+ }
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "update"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.updateMany retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "update"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "updateMany",
+ "object": "collection",
+ "arguments": {
+ "filter": {},
+ "update": {
+ "$set": {
+ "x": 22
+ }
+ }
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "update"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "update"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.findOneAndDelete retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "findAndModify"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "findOneAndDelete",
+ "object": "collection",
+ "arguments": {
+ "filter": {}
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "findAndModify"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.findOneAndReplace retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "findAndModify"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "findOneAndReplace",
+ "object": "collection",
+ "arguments": {
+ "filter": {},
+ "replacement": {
+ "x": 22
+ }
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "findAndModify"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.findOneAndUpdate retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "findAndModify"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "findOneAndUpdate",
+ "object": "collection",
+ "arguments": {
+ "filter": {},
+ "update": {
+ "$set": {
+ "x": 22
+ }
+ }
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "findAndModify"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "findAndModify"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.bulkWrite retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "insert"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "bulkWrite",
+ "object": "collection",
+ "arguments": {
+ "requests": [
+ {
+ "insertOne": {
+ "document": {
+ "_id": 2,
+ "x": 22
+ }
+ }
+ }
+ ]
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "insert"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "insert"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.createIndex retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "createIndexes"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "createIndex",
+ "object": "collection",
+ "arguments": {
+ "keys": {
+ "x": 11
+ },
+ "name": "x_11"
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "createIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "createIndexes"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "createIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "createIndexes"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "createIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "createIndexes"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.dropIndex retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "dropIndexes"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "dropIndex",
+ "object": "collection",
+ "arguments": {
+ "name": "x_11"
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "dropIndexes"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.dropIndexes retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "dropIndexes"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "dropIndexes",
+ "object": "collection",
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "dropIndexes"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "dropIndexes"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "collection.aggregate write retries at most maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "fail_point_client",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "aggregate"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "aggregate",
+ "object": "collection",
+ "arguments": {
+ "pipeline": [
+ {
+ "$out": "output"
+ }
+ ]
+ },
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "aggregate"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "aggregate"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/source/client-backpressure/tests/backpressure-retry-max-attempts.yml b/source/client-backpressure/tests/backpressure-retry-max-attempts.yml
new file mode 100644
index 0000000000..eec89671fa
--- /dev/null
+++ b/source/client-backpressure/tests/backpressure-retry-max-attempts.yml
@@ -0,0 +1,1368 @@
+# Tests in this file are generated from backpressure-retry-max-attempts.yml.template.
+
+description: tests that operations retry at most maxAttempts=2 times
+
+schemaVersion: '1.3'
+
+runOnRequirements:
+ -
+ minServerVersion: '4.4' # failCommand
+ topologies: [replicaset, sharded, load-balanced]
+
+createEntities:
+ - client:
+ id: &client client
+ useMultipleMongoses: false
+ observeEvents: [commandStartedEvent, commandSucceededEvent, commandFailedEvent]
+ ignoreCommandMonitoringEvents: [killCursors]
+
+ - client:
+ id: &fail_point_client fail_point_client
+ useMultipleMongoses: false
+
+ - database:
+ id: &database database
+ client: *client
+ databaseName: &database_name retryable-writes-tests
+
+ - collection:
+ id: &collection collection
+ database: *database
+ collectionName: &collection_name coll
+
+_yamlAnchors:
+ bulkWriteInsertNamespace: &client_bulk_write_ns retryable-writes-tests.coll
+
+initialData:
+ - collectionName: *collection_name
+ databaseName: *database_name
+ documents:
+ - { _id: 1, x: 11 }
+ - { _id: 2, x: 22 }
+
+tests:
+ - description: 'client.listDatabases retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [listDatabases]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: listDatabases
+ object: *client
+ arguments:
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: listDatabases
+ - commandFailedEvent:
+ commandName: listDatabases
+ - commandStartedEvent:
+ commandName: listDatabases
+ - commandFailedEvent:
+ commandName: listDatabases
+ - commandStartedEvent:
+ commandName: listDatabases
+ - commandFailedEvent:
+ commandName: listDatabases
+
+ - description: 'client.listDatabaseNames retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [listDatabases]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: listDatabaseNames
+ object: *client
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: listDatabases
+ - commandFailedEvent:
+ commandName: listDatabases
+ - commandStartedEvent:
+ commandName: listDatabases
+ - commandFailedEvent:
+ commandName: listDatabases
+ - commandStartedEvent:
+ commandName: listDatabases
+ - commandFailedEvent:
+ commandName: listDatabases
+
+ - description: 'client.createChangeStream retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: createChangeStream
+ object: *client
+ arguments:
+ pipeline: []
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+
+ - description: 'client.clientBulkWrite retries at most maxAttempts=2 times'
+ runOnRequirements:
+ - minServerVersion: '8.0' # client bulk write added to server in 8.0
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [bulkWrite]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: clientBulkWrite
+ object: *client
+ arguments:
+ models:
+ - insertOne:
+ namespace: *client_bulk_write_ns
+ document: { _id: 8, x: 88 }
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: bulkWrite
+ - commandFailedEvent:
+ commandName: bulkWrite
+ - commandStartedEvent:
+ commandName: bulkWrite
+ - commandFailedEvent:
+ commandName: bulkWrite
+ - commandStartedEvent:
+ commandName: bulkWrite
+ - commandFailedEvent:
+ commandName: bulkWrite
+
+ - description: 'database.aggregate read retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: aggregate
+ object: *database
+ arguments:
+ pipeline: [ { $listLocalSessions: {} }, { $limit: 1 } ]
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+
+ - description: 'database.listCollections retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [listCollections]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: listCollections
+ object: *database
+ arguments:
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: listCollections
+ - commandFailedEvent:
+ commandName: listCollections
+ - commandStartedEvent:
+ commandName: listCollections
+ - commandFailedEvent:
+ commandName: listCollections
+ - commandStartedEvent:
+ commandName: listCollections
+ - commandFailedEvent:
+ commandName: listCollections
+
+ - description: 'database.listCollectionNames retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [listCollections]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: listCollectionNames
+ object: *database
+ arguments:
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: listCollections
+ - commandFailedEvent:
+ commandName: listCollections
+ - commandStartedEvent:
+ commandName: listCollections
+ - commandFailedEvent:
+ commandName: listCollections
+ - commandStartedEvent:
+ commandName: listCollections
+ - commandFailedEvent:
+ commandName: listCollections
+
+ - description: 'database.runCommand retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [ping]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: runCommand
+ object: *database
+ arguments:
+ command: { ping: 1 }
+ commandName: ping
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: ping
+ - commandFailedEvent:
+ commandName: ping
+ - commandStartedEvent:
+ commandName: ping
+ - commandFailedEvent:
+ commandName: ping
+ - commandStartedEvent:
+ commandName: ping
+ - commandFailedEvent:
+ commandName: ping
+
+ - description: 'database.createChangeStream retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: createChangeStream
+ object: *database
+ arguments:
+ pipeline: []
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+
+ - description: 'collection.aggregate read retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: aggregate
+ object: *collection
+ arguments:
+ pipeline: []
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+
+ - description: 'collection.countDocuments retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: countDocuments
+ object: *collection
+ arguments:
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+
+ - description: 'collection.estimatedDocumentCount retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [count]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: estimatedDocumentCount
+ object: *collection
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: count
+ - commandFailedEvent:
+ commandName: count
+ - commandStartedEvent:
+ commandName: count
+ - commandFailedEvent:
+ commandName: count
+ - commandStartedEvent:
+ commandName: count
+ - commandFailedEvent:
+ commandName: count
+
+ - description: 'collection.distinct retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [distinct]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: distinct
+ object: *collection
+ arguments:
+ fieldName: x
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: distinct
+ - commandFailedEvent:
+ commandName: distinct
+ - commandStartedEvent:
+ commandName: distinct
+ - commandFailedEvent:
+ commandName: distinct
+ - commandStartedEvent:
+ commandName: distinct
+ - commandFailedEvent:
+ commandName: distinct
+
+ - description: 'collection.find retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [find]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: find
+ object: *collection
+ arguments:
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: find
+ - commandFailedEvent:
+ commandName: find
+ - commandStartedEvent:
+ commandName: find
+ - commandFailedEvent:
+ commandName: find
+ - commandStartedEvent:
+ commandName: find
+ - commandFailedEvent:
+ commandName: find
+
+ - description: 'collection.findOne retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [find]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: findOne
+ object: *collection
+ arguments:
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: find
+ - commandFailedEvent:
+ commandName: find
+ - commandStartedEvent:
+ commandName: find
+ - commandFailedEvent:
+ commandName: find
+ - commandStartedEvent:
+ commandName: find
+ - commandFailedEvent:
+ commandName: find
+
+ - description: 'collection.listIndexes retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [listIndexes]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: listIndexes
+ object: *collection
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: listIndexes
+ - commandFailedEvent:
+ commandName: listIndexes
+ - commandStartedEvent:
+ commandName: listIndexes
+ - commandFailedEvent:
+ commandName: listIndexes
+ - commandStartedEvent:
+ commandName: listIndexes
+ - commandFailedEvent:
+ commandName: listIndexes
+
+ - description: 'collection.listIndexNames retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [listIndexes]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: listIndexNames
+ object: *collection
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: listIndexes
+ - commandFailedEvent:
+ commandName: listIndexes
+ - commandStartedEvent:
+ commandName: listIndexes
+ - commandFailedEvent:
+ commandName: listIndexes
+ - commandStartedEvent:
+ commandName: listIndexes
+ - commandFailedEvent:
+ commandName: listIndexes
+
+ - description: 'collection.createChangeStream retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: createChangeStream
+ object: *collection
+ arguments:
+ pipeline: []
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+
+ - description: 'collection.insertOne retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [insert]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: insertOne
+ object: *collection
+ arguments:
+ document: { _id: 2, x: 22 }
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: insert
+ - commandFailedEvent:
+ commandName: insert
+ - commandStartedEvent:
+ commandName: insert
+ - commandFailedEvent:
+ commandName: insert
+ - commandStartedEvent:
+ commandName: insert
+ - commandFailedEvent:
+ commandName: insert
+
+ - description: 'collection.insertMany retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [insert]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: insertMany
+ object: *collection
+ arguments:
+ documents:
+ - { _id: 2, x: 22 }
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: insert
+ - commandFailedEvent:
+ commandName: insert
+ - commandStartedEvent:
+ commandName: insert
+ - commandFailedEvent:
+ commandName: insert
+ - commandStartedEvent:
+ commandName: insert
+ - commandFailedEvent:
+ commandName: insert
+
+ - description: 'collection.deleteOne retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [delete]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: deleteOne
+ object: *collection
+ arguments:
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: delete
+ - commandFailedEvent:
+ commandName: delete
+ - commandStartedEvent:
+ commandName: delete
+ - commandFailedEvent:
+ commandName: delete
+ - commandStartedEvent:
+ commandName: delete
+ - commandFailedEvent:
+ commandName: delete
+
+ - description: 'collection.deleteMany retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [delete]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: deleteMany
+ object: *collection
+ arguments:
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: delete
+ - commandFailedEvent:
+ commandName: delete
+ - commandStartedEvent:
+ commandName: delete
+ - commandFailedEvent:
+ commandName: delete
+ - commandStartedEvent:
+ commandName: delete
+ - commandFailedEvent:
+ commandName: delete
+
+ - description: 'collection.replaceOne retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [update]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: replaceOne
+ object: *collection
+ arguments:
+ filter: {}
+ replacement: { x: 22 }
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: update
+ - commandFailedEvent:
+ commandName: update
+ - commandStartedEvent:
+ commandName: update
+ - commandFailedEvent:
+ commandName: update
+ - commandStartedEvent:
+ commandName: update
+ - commandFailedEvent:
+ commandName: update
+
+ - description: 'collection.updateOne retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [update]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: updateOne
+ object: *collection
+ arguments:
+ filter: {}
+ update: { $set: { x: 22 } }
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: update
+ - commandFailedEvent:
+ commandName: update
+ - commandStartedEvent:
+ commandName: update
+ - commandFailedEvent:
+ commandName: update
+ - commandStartedEvent:
+ commandName: update
+ - commandFailedEvent:
+ commandName: update
+
+ - description: 'collection.updateMany retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [update]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: updateMany
+ object: *collection
+ arguments:
+ filter: {}
+ update: { $set: { x: 22 } }
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: update
+ - commandFailedEvent:
+ commandName: update
+ - commandStartedEvent:
+ commandName: update
+ - commandFailedEvent:
+ commandName: update
+ - commandStartedEvent:
+ commandName: update
+ - commandFailedEvent:
+ commandName: update
+
+ - description: 'collection.findOneAndDelete retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [findAndModify]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: findOneAndDelete
+ object: *collection
+ arguments:
+ filter: {}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandFailedEvent:
+ commandName: findAndModify
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandFailedEvent:
+ commandName: findAndModify
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandFailedEvent:
+ commandName: findAndModify
+
+ - description: 'collection.findOneAndReplace retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [findAndModify]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: findOneAndReplace
+ object: *collection
+ arguments:
+ filter: {}
+ replacement: { x: 22 }
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandFailedEvent:
+ commandName: findAndModify
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandFailedEvent:
+ commandName: findAndModify
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandFailedEvent:
+ commandName: findAndModify
+
+ - description: 'collection.findOneAndUpdate retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [findAndModify]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: findOneAndUpdate
+ object: *collection
+ arguments:
+ filter: {}
+ update: { $set: { x: 22 } }
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandFailedEvent:
+ commandName: findAndModify
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandFailedEvent:
+ commandName: findAndModify
+ - commandStartedEvent:
+ commandName: findAndModify
+ - commandFailedEvent:
+ commandName: findAndModify
+
+ - description: 'collection.bulkWrite retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [insert]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: bulkWrite
+ object: *collection
+ arguments:
+ requests:
+ - insertOne:
+ document: { _id: 2, x: 22 }
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: insert
+ - commandFailedEvent:
+ commandName: insert
+ - commandStartedEvent:
+ commandName: insert
+ - commandFailedEvent:
+ commandName: insert
+ - commandStartedEvent:
+ commandName: insert
+ - commandFailedEvent:
+ commandName: insert
+
+ - description: 'collection.createIndex retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [createIndexes]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: createIndex
+ object: *collection
+ arguments:
+ keys: { x: 11 }
+ name: "x_11"
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: createIndexes
+ - commandFailedEvent:
+ commandName: createIndexes
+ - commandStartedEvent:
+ commandName: createIndexes
+ - commandFailedEvent:
+ commandName: createIndexes
+ - commandStartedEvent:
+ commandName: createIndexes
+ - commandFailedEvent:
+ commandName: createIndexes
+
+ - description: 'collection.dropIndex retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [dropIndexes]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: dropIndex
+ object: *collection
+ arguments:
+ name: "x_11"
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: dropIndexes
+ - commandFailedEvent:
+ commandName: dropIndexes
+ - commandStartedEvent:
+ commandName: dropIndexes
+ - commandFailedEvent:
+ commandName: dropIndexes
+ - commandStartedEvent:
+ commandName: dropIndexes
+ - commandFailedEvent:
+ commandName: dropIndexes
+
+ - description: 'collection.dropIndexes retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [dropIndexes]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: dropIndexes
+ object: *collection
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: dropIndexes
+ - commandFailedEvent:
+ commandName: dropIndexes
+ - commandStartedEvent:
+ commandName: dropIndexes
+ - commandFailedEvent:
+ commandName: dropIndexes
+ - commandStartedEvent:
+ commandName: dropIndexes
+ - commandFailedEvent:
+ commandName: dropIndexes
+
+ - description: 'collection.aggregate write retries at most maxAttempts=2 times'
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [aggregate]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: aggregate
+ object: *collection
+ arguments:
+ pipeline: [{$out: "output"}]
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
+ - commandStartedEvent:
+ commandName: aggregate
+ - commandFailedEvent:
+ commandName: aggregate
diff --git a/source/client-backpressure/tests/backpressure-retry-max-attempts.yml.template b/source/client-backpressure/tests/backpressure-retry-max-attempts.yml.template
new file mode 100644
index 0000000000..ffbd08de9c
--- /dev/null
+++ b/source/client-backpressure/tests/backpressure-retry-max-attempts.yml.template
@@ -0,0 +1,90 @@
+# Tests in this file are generated from backpressure-retry-max-attempts.yml.template.
+
+description: tests that operations retry at most maxAttempts=2 times
+
+schemaVersion: '1.3'
+
+runOnRequirements:
+ -
+ minServerVersion: '4.4' # failCommand
+ topologies: [replicaset, sharded, load-balanced]
+
+createEntities:
+ - client:
+ id: &client client
+ useMultipleMongoses: false
+ observeEvents: [commandStartedEvent, commandSucceededEvent, commandFailedEvent]
+ ignoreCommandMonitoringEvents: [killCursors]
+
+ - client:
+ id: &fail_point_client fail_point_client
+ useMultipleMongoses: false
+
+ - database:
+ id: &database database
+ client: *client
+ databaseName: &database_name retryable-writes-tests
+
+ - collection:
+ id: &collection collection
+ database: *database
+ collectionName: &collection_name coll
+
+_yamlAnchors:
+ bulkWriteInsertNamespace: &client_bulk_write_ns retryable-writes-tests.coll
+
+initialData:
+ - collectionName: *collection_name
+ databaseName: *database_name
+ documents:
+ - { _id: 1, x: 11 }
+ - { _id: 2, x: 22 }
+
+tests: {% for operation in operations %}
+ - description: '{{operation.object}}.{{operation.operation_name}}{{ " " + operation.operation_type + "" if operation.operation_name == "aggregate" else "" }} retries at most maxAttempts=2 times'
+ {%- if ((operation.operation_name == 'clientBulkWrite')) %}
+ runOnRequirements:
+ - minServerVersion: '8.0' # client bulk write added to server in 8.0
+ {%- endif %}
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *fail_point_client
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [{{operation.command_name}}]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: {{operation.operation_name}}
+ object: *{{operation.object}} {%- if operation.arguments|length > 0 %}
+ arguments:
+ {%- for arg in operation.arguments %}
+ {{arg}}
+ {%- endfor -%}
+ {%- endif %}
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ # we expect 3 pairs of command started and succeeded events:
+ # 1 initial attempt and 2 retries.
+ - commandStartedEvent:
+ commandName: {{operation.command_name}}
+ - commandFailedEvent:
+ commandName: {{operation.command_name}}
+ - commandStartedEvent:
+ commandName: {{operation.command_name}}
+ - commandFailedEvent:
+ commandName: {{operation.command_name}}
+ - commandStartedEvent:
+ commandName: {{operation.command_name}}
+ - commandFailedEvent:
+ commandName: {{operation.command_name}}
+{% endfor -%}
diff --git a/source/client-backpressure/tests/getMore-retried.json b/source/client-backpressure/tests/getMore-retried.json
new file mode 100644
index 0000000000..d7607d694b
--- /dev/null
+++ b/source/client-backpressure/tests/getMore-retried.json
@@ -0,0 +1,253 @@
+{
+ "description": "getMore-retried-backpressure",
+ "schemaVersion": "1.3",
+ "runOnRequirements": [
+ {
+ "minServerVersion": "4.4"
+ }
+ ],
+ "createEntities": [
+ {
+ "client": {
+ "id": "client0",
+ "useMultipleMongoses": false,
+ "observeEvents": [
+ "commandStartedEvent",
+ "commandFailedEvent",
+ "commandSucceededEvent"
+ ]
+ }
+ },
+ {
+ "client": {
+ "id": "failPointClient",
+ "useMultipleMongoses": false
+ }
+ },
+ {
+ "database": {
+ "id": "db",
+ "client": "client0",
+ "databaseName": "default"
+ }
+ },
+ {
+ "collection": {
+ "id": "coll",
+ "database": "db",
+ "collectionName": "default"
+ }
+ }
+ ],
+ "initialData": [
+ {
+ "databaseName": "default",
+ "collectionName": "default",
+ "documents": [
+ {
+ "a": 1
+ },
+ {
+ "a": 2
+ },
+ {
+ "a": 3
+ }
+ ]
+ }
+ ],
+ "tests": [
+ {
+ "description": "getMores are retried",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "failPointClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": {
+ "times": 2
+ },
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "find",
+ "object": "coll",
+ "arguments": {
+ "batchSize": 2,
+ "filter": {},
+ "sort": {
+ "a": 1
+ }
+ },
+ "expectResult": [
+ {
+ "a": 1
+ },
+ {
+ "a": 2
+ },
+ {
+ "a": 3
+ }
+ ]
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "getMore"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "getMore"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "getMore"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "getMore"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "getMore"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "getMore"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "getMores are retried maxAttempts=2 times",
+ "operations": [
+ {
+ "name": "failPoint",
+ "object": "testRunner",
+ "arguments": {
+ "client": "failPointClient",
+ "failPoint": {
+ "configureFailPoint": "failCommand",
+ "mode": "alwaysOn",
+ "data": {
+ "failCommands": [
+ "getMore"
+ ],
+ "errorLabels": [
+ "RetryableError",
+ "SystemOverloadedError"
+ ],
+ "errorCode": 2
+ }
+ }
+ }
+ },
+ {
+ "name": "find",
+ "arguments": {
+ "batchSize": 2,
+ "filter": {}
+ },
+ "object": "coll",
+ "expectError": {
+ "isError": true,
+ "isClientError": false
+ }
+ }
+ ],
+ "expectEvents": [
+ {
+ "client": "client0",
+ "events": [
+ {
+ "commandStartedEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "find"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "getMore"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "getMore"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "getMore"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "getMore"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "getMore"
+ }
+ },
+ {
+ "commandFailedEvent": {
+ "commandName": "getMore"
+ }
+ },
+ {
+ "commandStartedEvent": {
+ "commandName": "killCursors"
+ }
+ },
+ {
+ "commandSucceededEvent": {
+ "commandName": "killCursors"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/source/client-backpressure/tests/getMore-retried.yml b/source/client-backpressure/tests/getMore-retried.yml
new file mode 100644
index 0000000000..cd8198383a
--- /dev/null
+++ b/source/client-backpressure/tests/getMore-retried.yml
@@ -0,0 +1,131 @@
+description: getMore-retried-backpressure
+schemaVersion: "1.3"
+runOnRequirements:
+ - minServerVersion: '4.4' # failCommand
+createEntities:
+ - client:
+ id: &client client0
+ useMultipleMongoses: false
+ observeEvents:
+ - commandStartedEvent
+ - commandFailedEvent
+ - commandSucceededEvent
+ - client:
+ id: &failPointClient failPointClient
+ useMultipleMongoses: false
+ - database:
+ id: db
+ client: *client
+ databaseName: &dbName default
+ - collection:
+ id: &collection coll
+ database: db
+ collectionName: &collectionName default
+initialData:
+ - databaseName: *dbName
+ collectionName: *collectionName
+ documents:
+ - { a: 1 }
+ - { a: 2 }
+ - { a: 3 }
+
+tests:
+ - description: "getMores are retried"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *failPointClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: { times: 2 }
+ data:
+ failCommands: [getMore]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: find
+ object: *collection
+ arguments:
+ # batch size of 2 with 3 docs in the collection ensures exactly one find + one getMore exhaust the cursor
+ batchSize: 2
+ filter: {}
+ # ensure stable ordering of result documents
+ sort: { a: 1 }
+ expectResult:
+ - { a: 1 }
+ - { a: 2 }
+ - { a: 3 }
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: find
+ - commandSucceededEvent:
+ commandName: find
+ # first attempt
+ - commandStartedEvent:
+ commandName: getMore
+ - commandFailedEvent:
+ commandName: getMore
+ # second attempt
+ - commandStartedEvent:
+ commandName: getMore
+ - commandFailedEvent:
+ commandName: getMore
+ # success
+ - commandStartedEvent:
+ commandName: getMore
+ - commandSucceededEvent:
+ commandName: getMore
+
+ - description: "getMores are retried maxAttempts=2 times"
+ operations:
+ - name: failPoint
+ object: testRunner
+ arguments:
+ client: *failPointClient
+ failPoint:
+ configureFailPoint: failCommand
+ mode: alwaysOn
+ data:
+ failCommands: [getMore]
+ errorLabels: [RetryableError, SystemOverloadedError]
+ errorCode: 2
+
+ - name: find
+ arguments:
+ batchSize: 2
+ filter: {}
+ object: *collection
+ expectError:
+ isError: true
+ isClientError: false
+
+ expectEvents:
+ - client: *client
+ events:
+ - commandStartedEvent:
+ commandName: find
+ - commandSucceededEvent:
+ commandName: find
+ # first attempt
+ - commandStartedEvent:
+ commandName: getMore
+ - commandFailedEvent:
+ commandName: getMore
+ # second attempt
+ - commandStartedEvent:
+ commandName: getMore
+ - commandFailedEvent:
+ commandName: getMore
+ # third attempt
+ - commandStartedEvent:
+ commandName: getMore
+ - commandFailedEvent:
+ commandName: getMore
+ - commandStartedEvent:
+ commandName: killCursors
+ - commandSucceededEvent:
+ commandName: killCursors
diff --git a/source/client-side-encryption/client-side-encryption.md b/source/client-side-encryption/client-side-encryption.md
new file mode 100644
index 0000000000..ef40981fc2
--- /dev/null
+++ b/source/client-side-encryption/client-side-encryption.md
@@ -0,0 +1,2650 @@
+# Client Side Encryption
+
+- Status: Accepted
+- Minimum Server Version: 4.2 (CSFLE), 7.0 (QE)
+- Version: 1.14.0
+
+______________________________________________________________________
+
+## Abstract
+
+MongoDB 4.2 introduced support for client side encryption, guaranteeing that sensitive data can only be encrypted and
+decrypted with access to both MongoDB and a separate key management provider (supporting AWS, Azure, GCP, a local
+provider, and KMIP). Once enabled, data can be seamlessly encrypted and decrypted with minimal application code changes.
+7.0 introduced the next generation of client side encryption based on a Structured Encryption framework which allows
+expressive encrypted search operations. This spec covers both capabilities - 1st generation, "Client Side Field Level
+Encryption" and generation 2, "Queryable Encryption" - as the associated core cryptographic library and supporting
+drivers share a common codebase.
+
+### Naming
+
+The public name of this feature is
+[In-Use Encryption](https://www.mongodb.com/docs/manual/core/security-in-use-encryption/) and consists of:
+
+- Client-Side Field Level Encryption (CSFLE).
+- Queryable Encryption (QE).
+
+Internally, In-Use Encryption is sometimes called Field Level Encryption (FLE). Internally, CSFLE is sometimes called
+Client Side Encryption (like this specification). Internally, CSFLE and QE are sometimes called FLE1 and FLE2,
+respectively.
+
+### Server support history
+
+MongoDB 4.2 added support for CSFLE. This includes `encrypt` in JSON Schema
+([SERVER-38900](https://jira.mongodb.org/browse/SERVER-38900)) and [mongocryptd](#mongocryptd)
+([SPM-1258](https://jira.mongodb.org/browse/SPM-1258)).
+
+MongoDB 5.3 added the [crypt_shared](#crypt_shared) library ([SPM-2403](https://jira.mongodb.org/browse/SPM-2403)).
+
+MongoDB 6.0 added unstable support for QE (QEv1) ([SPM-2463](https://jira.mongodb.org/browse/SPM-2463)). This includes
+`queryType=equality`.
+
+MongoDB 6.2 added unstable support for QE range queries ([SPM-2719](https://jira.mongodb.org/browse/SPM-2719)). This
+includes `queryType=rangePreview`.
+
+MongoDB 7.0 dropped QEv1 and added stable support of QE (QEv2) ([SPM-2972](https://jira.mongodb.org/browse/SPM-2972)).
+QEv1 and QEv2 are incompatible.
+
+MongoDB 8.0 dropped `queryType=rangePreview` and added `queryType=range`
+([SPM-3583](https://jira.mongodb.org/browse/SPM-3583)).
+
+MongoDB 8.2 added unstable support for QE string queries ([SPM-2880](https://jira.mongodb.org/browse/SPM-2880))
+
+MongoDB 9.0 dropped `queryType=prefixPreview|suffixPreview` and added `queryType=prefix|suffix`
+([SPM-4539](https://jira.mongodb.org/browse/SPM-4539))
+
+## META
+
+The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
+"OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt).
+
+## Terms
+
+**encrypted MongoClient**
+
+A MongoClient with client side encryption enabled.
+
+**data key**
+
+A key used to encrypt and decrypt BSON values. Data keys are encrypted with a key management service (e.g. AWS KMS) and
+stored within a document in the MongoDB key vault collection (see
+[Key vault collection schema for data keys](#key-vault-collection-schema-for-data-keys) for a description of the data
+key document). Therefore, a client needs access to both MongoDB and the external KMS service to utilize a data key.
+
+**MongoDB key vault collection**
+
+A MongoDB collection designated to contain data keys. This can either be co-located with the data-bearing cluster, or in
+a separate external MongoDB cluster.
+
+**Key Management Service (KMS)**
+
+An external service providing fixed-size encryption/decryption. Only data keys are encrypted and decrypted with KMS.
+
+**KMS providers**
+
+A map of KMS providers to credentials. Configured client-side. Example:
+
+```javascript
+kms_providers = {
+ "aws": {
+ "accessKeyId": AWS_KEYID,
+ "secretAccessKey": AWS_SECRET,
+ },
+ "local": {
+ "key": LOCAL_KEK
+ },
+}
+```
+
+**KMS provider**
+
+A configured KMS. Identified by a key in the KMS providers map. The key has the form `` or
+`:`. Examples: `aws` or `aws:myname`. In [libmongocrypt](#libmongocrypt), the key
+is referred to as the KMS ID.
+
+**KMS provider type**
+
+The type of backing KMS. Identified by the string: `aws`, `azure`, `gcp`, `kmip`, or `local`.
+
+**KMS provider name**
+
+An optional name to identify a KMS provider. Enables configuring multiple KMS providers with the same KMS provider type
+(e.g. `aws:name1` and `aws:name2` can refer to different AWS accounts).
+
+**Master Key**
+
+The underlying key the KMS service uses to encrypt and decrypt. See
+[AWS KMS Concepts](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#master_keys) for an AWS-specific
+example (other KMS providers work similarly).
+
+The master key is also sometimes referred to as a Customer Master Key (CMK).
+
+**schema**
+
+A MongoDB JSON Schema (either supplied by the server or client-side) which may include metadata about encrypted fields.
+This is a JSON Schema based on draft 4 of the JSON Schema specification,
+[as documented in the MongoDB manual.](https://www.mongodb.com/docs/manual/reference/operator/query/jsonSchema/).
+
+**[libmongocrypt](#libmongocrypt)**
+
+A library, written in C, that coordinates communication, does encryption/decryption, caches key and schemas.
+[Located here](https://github.com/mongodb/libmongocrypt).
+
+**[mongocryptd](#mongocryptd)**
+
+A local process the driver communicates with to determine how to encrypt values in a command.
+
+**[crypt_shared](#crypt_shared)**
+
+This term, spelled in all-lowercase with an underscore, refers to the client-side field-level-encryption dynamic library
+provided as part of a MongoDB Enterprise distribution. It replaces [mongocryptd](#mongocryptd) as the method of
+[marking-up a database command for encryption](../bson-binary-encrypted/binary-encrypted.md#intent-to-encrypt).
+
+See also:
+
+- [Introduction on crypt_shared](#crypt_shared)
+- [Enabling crypt_shared](#enabling-crypt_shared)
+
+**ciphertext**
+
+One of the data formats of [BSON binary encrypted](../bson-binary-encrypted/binary-encrypted.md), representing an
+encoded BSON document containing encrypted ciphertext and metadata.
+
+**Client-Side Field Level Encryption (CSFLE)**
+
+CSFLE is the first version of In-Use Encryption. CSFLE is almost entirely client-side with the exception of server-side
+JSON schema.
+
+**Queryable Encryption (QE)**
+
+Queryable Encryption the second version of In-Use Encryption. Data is encrypted client-side. Queryable Encryption
+supports indexed encrypted fields, which are further processed server-side.
+
+**In-Use Encryption**
+
+Is an umbrella term describing the both CSFLE and Queryable Encryption.
+
+ **encryptedFields**
+
+A BSON document describing the Queryable Encryption encrypted fields. This is analogous to the JSON Schema in FLE. The
+following is an example encryptedFields in extended canonical JSON:
+
+```javascript
+{
+ "escCollection": "enxcol_.CollectionName.esc",
+ "ecocCollection": "enxcol_.CollectionName.ecoc",
+ "fields": [
+ {
+ "path": "firstName",
+ "keyId": { "$binary": { "subType": "04", "base64": "AAAAAAAAAAAAAAAAAAAAAA==" }},
+ "bsonType": "string",
+ "queries": {"queryType": "equality"}
+ },
+ {
+ "path": "ssn",
+ "keyId": { "$binary": { "subType": "04", "base64": "BBBBBBBBBBBBBBBBBBBBBB==" }},
+ "bsonType": "string"
+ }
+ ]
+}
+```
+
+The acronyms within `encryptedFields` are defined as follows:
+
+- ECOC: Encrypted Compaction Collection
+- ESC: Encrypted State Collection
+
+## Introduction
+
+Client side encryption enables users to specify what fields in a collection must be encrypted, and the driver
+automatically encrypts commands and decrypts results. Automatic encryption is enterprise only. But users can manually
+encrypt and decrypt with a new ClientEncryption object.
+
+Client side encryption requires MongoDB 4.2 compatible drivers, and is only supported against 4.2 or higher servers. See
+[Why is a 4.2 server required?](#why-is-a-42-server-required).
+
+The following shows basic usage of the new API.
+
+```python
+# The schema map identifies fields on collections that must undergo encryption.
+
+schema_map = open("./schemas.json", "r").read()
+
+# AWS KMS is used to decrypt data keys stored in the key vault collection.
+
+aws_creds = open("./aws_credentials.json", "r").read()
+
+# A client is configured for automatic encryption and decryption by passing
+# AutoEncryptionOpts. Automatic encryption is an enterprise only feature.
+
+opts = AutoEncryptionOpts(
+ kms_providers={"aws": aws_creds},
+ key_vault_namespace="db.datakeys",
+ schema_map=schema_map)
+
+db = MongoClient(auto_encryption_opts=opts).db
+
+# Commands are encrypted, as determined by the JSON Schema from the schema_map.
+db.coll.insert_one({"ssn": "457-55-5462"})
+
+# Replies are decrypted.
+print(db.coll.find_one()) # { "ssn": "457-55-5462" } but stored and transported as ciphertext.
+
+# A ClientEncryption object is used for explicit encryption, decryption, and creating data keys.
+opts = ClientEncryptionOpts(kms_providers=kms, key_vault_namespace="db.datakeys")
+clientencryption = ClientEncryption(client, opts)
+
+# Use a ClientEncryption to create new data keys.
+# The master key identifies the KMS key on AWS KMS to use for encrypting the data key.
+master_key = open("./aws_masterkey.json", "r").read()
+opts = DataKeyOpts (master_key=master_key)
+created_key_id = clientencryption.create_data_key("aws", opts)
+
+# Use a ClientEncryption to explicitly encrypt and decrypt.
+opts = EncryptOpts(key_id=created_key_id,
+ algorithm="AEAD_AES_256_CBC_HMAC_SHA_512-Random")
+encrypted = clientencryption.encrypt("secret text", opts)
+# Decryption does not require the key ID or algorithm. The ciphertext indicates the key ID and algorithm used.
+decrypted = clientencryption.decrypt(encrypted)
+```
+
+There are many moving parts to client side encryption with lots of similar sounding terms. Before proceeding to
+implement the specification, the following background should provide some context.
+
+The driver interacts with multiple components to implement client side encryption.
+
+
+
+The driver communicates with…
+
+- **MongoDB cluster** to get remote JSON Schemas.
+- **MongoDB key vault collection** to get encrypted data keys and create new data keys.
+- **A KMS Provider** to decrypt fetched data keys and encrypt new data keys.
+- **mongocryptd** to ask what values in BSON commands must be encrypted (unless [crypt_shared](#crypt_shared) is in
+ use).
+
+The MongoDB key vault may be the same as the MongoDB cluster. Users may choose to have data key stored on a separate
+MongoDB cluster, or co-locate with their data.
+
+### MongoDB Key Vault collection
+
+The key vault collection is a special MongoDB collection containing key documents. See the appendix section
+[Key vault collection schema for data keys](#key-vault-collection-schema-for-data-keys) for a description of the
+documents.
+
+The key material in the key vault collection is encrypted with a separate KMS service. Therefore, encryption and
+decryption requires access to a MongoDB cluster and the KMS service.
+
+### KMS Provider
+
+A KMS provider (AWS KMS, Azure Key Vault, GCP KMS, the local provider, or KMIP) is used to decrypt data keys after
+fetching from the MongoDB Key Vault, and encrypt newly created data keys. Refer to [KMSProviders](#kmsproviders) for the
+shape of the KMS provider options.
+
+### mongocryptd
+
+`mongocryptd` is a singleton local process needed for auto-encryption when no [crypt_shared](#crypt_shared) library is
+used. It speaks the MongoDB wire protocol and the driver uses [mongocryptd](#mongocryptd) by connecting with a
+MongoClient. By default, if [crypt_shared](#crypt_shared) is unavailable, the driver should attempt to automatically
+spawn [mongocryptd](#mongocryptd). If the MongoClient is configured with `extraOptions.mongocryptdBypassSpawn` set to
+`true`, OR `bypassAutoEncryption` is set to `true`, OR `bypassQueryAnalysis` is set to `true` then the driver will not
+attempt to spawn [mongocryptd](#mongocryptd).
+
+The [mongocryptd](#mongocryptd) process is responsible for self terminating after idling for a time period. If
+[extraOptions.cryptSharedLibRequired](#extraoptions.cryptsharedlibrequired) is set to `true`, the driver will not
+connect to [mongocryptd](#mongocryptd) and instead rely on [crypt_shared](#crypt_shared) being available.
+
+### crypt_shared
+
+[crypt_shared](#crypt_shared) is a dynamically-loaded C++ library providing query analysis for auto-encryption. It
+replaces [mongocryptd](#mongocryptd) for performing query analysis to -
+[mark-up sensitive fields within a command](../bson-binary-encrypted/binary-encrypted.md#intent-to-encrypt).
+
+Drivers are not required to load and interact with [crypt_shared](#crypt_shared) directly. Instead, they inform
+[libmongocrypt](#libmongocrypt) where to find [crypt_shared](#crypt_shared) and [libmongocrypt](#libmongocrypt) will
+handle [crypt_shared](#crypt_shared) communication automatically.
+
+See also: [Enabling crypt_shared](#enabling-crypt_shared) for information on using enabling the
+[crypt_shared](#crypt_shared) library.
+
+### libmongocrypt
+
+libmongocrypt is a C library providing crypto and coordination with external components.
+[Located here](https://github.com/mongodb/libmongocrypt).
+
+**libmongocrypt is responsible for…**
+
+- orchestrating an internal state machine.
+- asking the driver to perform I/O, then handling the responses.
+ - includes constructing KMS HTTP requests and parsing KMS responses.
+- doing encryption and decryption.
+- caching data keys.
+- caching results of listCollections.
+- creating key material.
+
+**The driver is responsible for…**
+
+- performing all I/O needed at every state:
+ - speaking to [mongocryptd](#mongocryptd) to mark commands (unless [crypt_shared](#crypt_shared) is used).
+ - fetching encrypted data keys from key vault collection (mongod).
+ - running listCollections on mongod.
+ - decrypting encrypted data keys with KMS over TLS.
+- doing I/O asynchronously as needed.
+
+See [Why require including a C library?](#why-require-including-a-c-library)
+
+## User facing API
+
+Drivers MUST NOT preclude future options from being added to any of the new interfaces.
+
+Drivers MAY represent the options types in a way that is idiomatic to the driver or language. E.g. options MAY be a BSON
+document or dictionary type. The driver MAY forego validating options and instead defer validation to the underlying
+implementation.
+
+Drivers MAY deviate the spelling of option names to conform to their language's naming conventions and implement options
+in an idiomatic way (e.g. keyword arguments, builder classes, etc.).
+
+Drivers MAY use a native UUID type in place of a parameter or return type specified as a BSON binary with subtype 0x04
+as described in [Handling of Native UUID Types](../bson-binary-uuid/uuid.md).
+
+### MongoClient Changes
+
+
+
+```typescript
+class MongoClient {
+ MongoClient(... autoEncryptionOpts: AutoEncryptionOpts);
+
+ // Implementation details.
+ private mongocrypt_t libmongocrypt_handle; // Handle to libmongocrypt.
+ private Optional mongocryptd_client; // Client to mongocryptd.
+ private MongoClient keyvault_client; // Client used to run find on the key vault collection. This is either an external MongoClient, the parent MongoClient, or internal_client.
+ private MongoClient metadata_client; // Client used to run listCollections. This is either the parent MongoClient or internal_client.
+ private Optional internal_client; // An internal MongoClient. Created if no external keyVaultClient was set, or if a metadataClient is needed
+
+ // Exposition-only, used for caching automatic Azure credentials. The cached credentials may be globally cached.
+ private cachedAzureAccessToken?: AzureAccessToken;
+ private azureAccessTokenExpireTime?: PointInTime;
+}
+```
+
+
+
+```typescript
+class AutoEncryptionOpts {
+ keyVaultClient: Optional;
+ keyVaultNamespace: String;
+ kmsProviders: KMSProviders;
+ schemaMap: Optional