From 14a6c8134d0c3038cdaa34bb33bcebbb3a545ca8 Mon Sep 17 00:00:00 2001 From: Shane Harvey Date: Thu, 12 Oct 2023 13:16:17 -0700 Subject: [PATCH] DRIVERS-2578 Drivers use polling SDAM on AWS Lambda (#1452) Disable streaming SDAM by default on AWS Lambda and similar FaaS platforms. Introduce the serverMonitoringMode=stream/poll/auto URI option. Add Unified Test Format version 1.17 to add support for server heartbeat events. Clients MUST NOT use dedicated connections to measure RTT when using the polling protocol. --- .../faas-automated-testing.rst | 5 + .../server-monitoring.rst | 98 ++- .../tests/unified/serverMonitoringMode.json | 449 +++++++++++ .../tests/unified/serverMonitoringMode.yml | 173 +++++ source/unified-test-format/schema-1.17.json | 712 ++++++++++++++++++ .../unified-test-format.rst | 38 +- source/uri-options/tests/sdam-options.json | 46 ++ source/uri-options/tests/sdam-options.yml | 35 + source/uri-options/uri-options.rst | 11 +- 9 files changed, 1552 insertions(+), 15 deletions(-) create mode 100644 source/server-discovery-and-monitoring/tests/unified/serverMonitoringMode.json create mode 100644 source/server-discovery-and-monitoring/tests/unified/serverMonitoringMode.yml create mode 100644 source/unified-test-format/schema-1.17.json create mode 100644 source/uri-options/tests/sdam-options.json create mode 100644 source/uri-options/tests/sdam-options.yml diff --git a/source/faas-automated-testing/faas-automated-testing.rst b/source/faas-automated-testing/faas-automated-testing.rst index df5c3a7408..ef9be1a0b2 100644 --- a/source/faas-automated-testing/faas-automated-testing.rst +++ b/source/faas-automated-testing/faas-automated-testing.rst @@ -243,6 +243,8 @@ the function implementation the driver MUST: - Drivers MUST record the durations and counts of the heartbeats, the durations of the commands, as well as keep track of the number of open connections, and report this information in the function response as JSON. +- Drivers MUST assert no ServerHeartbeat events contain the ``awaited=True`` flag to + confirm that the streaming protocol is disabled (`DRIVERS-2578`_). Running in Continuous Integration @@ -368,6 +370,9 @@ Description of the behaviour of run-deployed-lambda-aws-tests.sh: Changelog ========= +:2023-08-21: Drivers MUST assert that the streaming protocol is disabled in the Lambda function. :2023-08-17: Fixed URI typo, added host note, increase assume role duration. :2023-06-22: Updated evergreen configuration to use task groups. :2023-04-14: Added list of supported variants, added additional template config. + +.. _DRIVERS-2578: https://jira.mongodb.org/browse/DRIVERS-2578 diff --git a/source/server-discovery-and-monitoring/server-monitoring.rst b/source/server-discovery-and-monitoring/server-monitoring.rst index 940366a9a7..a4622ce619 100644 --- a/source/server-discovery-and-monitoring/server-monitoring.rst +++ b/source/server-discovery-and-monitoring/server-monitoring.rst @@ -91,6 +91,28 @@ Round trip time. The client's measurement of the duration of one hello or legacy The RTT is used to support `localThresholdMS`_ from the Server Selection spec and `timeoutMS`_ from the `Client Side Operations Timeout Spec`_. +FaaS +```` + +A Function-as-a-Service (FaaS) environment like AWS Lambda. + +serverMonitoringMode +```````````````````` + +The serverMonitoringMode option configures which server monitoring protocol to use. Valid modes are +"stream", "poll", or "auto". The default value MUST be "auto": + +- With "stream" mode, the client MUST use the streaming protocol when the server supports + it or fall back to the polling protocol otherwise. +- With "poll" mode, the client MUST use the polling protocol. +- With "auto" mode, the client MUST behave the same as "poll" mode when running on a FaaS + platform or the same as "stream" mode otherwise. The client detects that it's + running on a FaaS platform via the same rules for generating the ``client.env`` + handshake metadata field in the `MongoDB Handshake spec`_. + +Multi-threaded or asynchronous drivers MUST implement this option. +See `Why disable the streaming protocol on FaaS platforms like AWS Lambda?`_ and +`Why introduce a knob for serverMonitoringMode?`_ Monitoring '''''''''' @@ -203,7 +225,7 @@ Clients use the streaming protocol when supported ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When a monitor discovers that the server supports the streamable hello or legacy hello -command, it MUST use the `streaming protocol`_. +command and the client does not have `streaming disabled`_, it MUST use the `streaming protocol`_. Single-threaded monitoring `````````````````````````` @@ -491,6 +513,22 @@ connected to a server that supports the awaitable hello or legacy hello commands This protocol requires an extra thread and an extra socket for each monitor to perform RTT calculations. +.. _streaming is disabled: + +Streaming disabled +`````````````````` + +The streaming protocol MUST be disabled when either: + +- the client is configured with serverMonitoringMode=poll, or +- the client is configured with serverMonitoringMode=auto and a FaaS platform is detected, or +- the server does not support streaming (eg MongoDB < 4.4). + +When the streaming protocol is disabled the client MUST use the `polling protocol`_ +and MUST NOT start an extra thread or connection for `Measuring RTT`_. + +See `Why disable the streaming protocol on FaaS platforms like AWS Lambda?`_. + Streaming hello or legacy hello ``````````````````````````````` @@ -528,9 +566,8 @@ Measuring RTT When using the streaming protocol, clients MUST issue a hello or legacy hello command to each server to measure RTT every heartbeatFrequencyMS. The RTT command -MUST be run on a dedicated connection to each server. For consistency, -clients MAY use dedicated connections to measure RTT for all servers, even -those that do not support awaitable hello or legacy hello. (See +MUST be run on a dedicated connection to each server. Clients MUST NOT use +dedicated connections to measure RTT when the streaming protocol is not used. (See `Monitors MUST use a dedicated connection for RTT commands`_.) Clients MUST update the RTT from the hello or legacy hello duration of the initial @@ -584,8 +621,8 @@ current monitoring connection. (See `Drivers cancel in-progress monitor checks`_ Polling Protocol '''''''''''''''' -The polling protocol is used to monitor MongoDB <= 4.4 servers. The client -`checks`_ a server with a hello or legacy hello command and then sleeps for +The polling protocol is used to monitor MongoDB < 4.4 servers or when `streaming is disabled`_. +The client `checks`_ a server with a hello or legacy hello command and then sleeps for heartbeatFrequencyMS before running another check. Marking the connection pool as ready (CMAP only) @@ -661,6 +698,12 @@ The event API here is assumed to be like the standard `Python Event heartbeatFrequencyMS = heartbeatFrequencyMS minHeartbeatFrequencyMS = 500 stableApi = stableApi + if serverMonitoringMode == "stream": + streamingEnabled = True + elif serverMonitoringMode == "poll": + streamingEnabled = False + else: # serverMonitoringMode == "auto" + streamingEnabled = not isFaas() # Internal Monitor state: connection = Null @@ -671,8 +714,6 @@ The event API here is assumed to be like the standard `Python Event rttMonitor = RttMonitor(serverAddress, stableApi) def run(): - # Start the RttMonitor. - rttMonitor.run() while this monitor is not stopped: previousDescription = description try: @@ -700,7 +741,10 @@ The event API here is assumed to be like the standard `Python Event serverSupportsStreaming = description.type != Unknown and description.topologyVersion != Null connectionIsStreaming = connection != Null and connection.moreToCome transitionedWithNetworkError = isNetworkError(description.error) and previousDescription.type != Unknown - if serverSupportsStreaming or connectionIsStreaming or transitionedWithNetworkError: + if streamingEnabled and serverSupportsStreaming and not rttMonitor.started: + # Start the RttMonitor. + rttMonitor.run() + if (streamingEnabled and (serverSupportsStreaming or connectionIsStreaming)) or transitionedWithNetworkError: continue wait() @@ -733,13 +777,13 @@ The event API here is assumed to be like the standard `Python Event response = connection.handshakeResponse elif connection.moreToCome: response = read next helloCommand exhaust response - elif previousDescription.topologyVersion: + elif streamingEnabled and previousDescription.topologyVersion: # Initiate streaming hello or legacy hello if connectTimeoutMS != 0: set connection timeout to connectTimeoutMS+heartbeatFrequencyMS response = call {helloCommand: 1, helloOk: True, topologyVersion: previousDescription.topologyVersion, maxAwaitTimeMS: heartbeatFrequencyMS} else: - # The server does not support topologyVersion. + # The server does not support topologyVersion or streamingEnabled=False. response = call {helloCommand: 1, helloOk: True} # If the server supports hello, then response.helloOk will be true @@ -1140,6 +1184,32 @@ the "awaited" field on server heartbeat events so that applications can differentiate a slow heartbeat in the polling protocol from a normal awaitable hello or legacy hello heartbeat in the new protocol. +Why disable the streaming protocol on FaaS platforms like AWS Lambda? +''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' + +The streaming protocol relies on the assumption that the client +can read the server's heartbeat responses in a timely manner, otherwise the +client will be acting on stale information. In many FaaS platforms, like AWS +Lambda, host applications will be suspended and resumed many minutes later. +This behavior causes a build up of heartbeat responses and the client can end +up spending a long time in a catch up phase processing outdated responses. +This problem was discovered in `DRIVERS-2246`_. + +Additionally, the streaming protocol requires an extra connection and thread +per monitored server which is expensive on platforms like AWS Lambda. The +extra connection is particularly inefficient when thousands of AWS instances +and thus thousands of clients are used. + +We decided to make polling the default behavior when running on FaaS platforms +like AWS Lambda to improve scalability, performance, and reliability. + +Why introduce a knob for serverMonitoringMode? +'''''''''''''''''''''''''''''''''''''''''''''' + +The serverMonitoringMode knob provides a workaround in cases where the polling +protocol would be a better choice but the driver is not running on a FaaS +platform. It also provides a workaround in case the FaaS detection +logic becomes outdated or inaccurate. Changelog --------- @@ -1159,6 +1229,8 @@ Changelog :2022-04-05: Preemptively cancel in progress operations when SDAM heartbeats timeout. :2022-10-05: Remove spec front matter reformat changelog. :2022-11-17: Add minimum RTT tracking and remove 90th percentile RTT. +:2023-10-05: Add serverMonitoringMode and default to the polling protocol on FaaS. + Clients MUST NOT use dedicated connections to measure RTT when using the polling protocol. ---- @@ -1183,4 +1255,6 @@ Changelog .. _Why synchronize clearing a server's pool with updating the topology?: server-discovery-and-monitoring.rst#why-synchronize-clearing-a-server-s-pool-with-updating-the-topology? .. _Client Side Operations Timeout Spec: /source/client-side-operations-timeout/client-side-operations-timeout.rst .. _timeoutMS: /source/client-side-operations-timeout/client-side-operations-timeout.rst#timeoutMS -.. _Why does the pool need to support closing in use connections as part of its clear logic?: /source/connection-monitoring-and-pooling/connection-monitoring-and-pooling.rst#Why-does-the-pool-need-to-support-closing-in-use-connections-as-part-of-its-clear-logic? \ No newline at end of file +.. _Why does the pool need to support closing in use connections as part of its clear logic?: /source/connection-monitoring-and-pooling/connection-monitoring-and-pooling.rst#Why-does-the-pool-need-to-support-closing-in-use-connections-as-part-of-its-clear-logic? +.. _DRIVERS-2246: https://jira.mongodb.org/browse/DRIVERS-2246 +.. _MongoDB Handshake spec: /source/mongodb-handshake/handshake.rst#client-env diff --git a/source/server-discovery-and-monitoring/tests/unified/serverMonitoringMode.json b/source/server-discovery-and-monitoring/tests/unified/serverMonitoringMode.json new file mode 100644 index 0000000000..7d681b4f9e --- /dev/null +++ b/source/server-discovery-and-monitoring/tests/unified/serverMonitoringMode.json @@ -0,0 +1,449 @@ +{ + "description": "serverMonitoringMode", + "schemaVersion": "1.17", + "runOnRequirements": [ + { + "topologies": [ + "single", + "sharded", + "sharded-replicaset" + ], + "serverless": "forbid" + } + ], + "tests": [ + { + "description": "connect with serverMonitoringMode=auto >=4.4", + "runOnRequirements": [ + { + "minServerVersion": "4.4.0" + } + ], + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "uriOptions": { + "serverMonitoringMode": "auto" + }, + "useMultipleMongoses": false, + "observeEvents": [ + "serverHeartbeatStartedEvent", + "serverHeartbeatSucceededEvent", + "serverHeartbeatFailedEvent" + ] + } + }, + { + "database": { + "id": "db", + "client": "client", + "databaseName": "sdam-tests" + } + } + ] + } + }, + { + "name": "runCommand", + "object": "db", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + } + }, + "expectResult": { + "ok": 1 + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "serverHeartbeatStartedEvent": {} + }, + "count": 2 + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "sdam", + "ignoreExtraEvents": true, + "events": [ + { + "serverHeartbeatStartedEvent": { + "awaited": false + } + }, + { + "serverHeartbeatSucceededEvent": { + "awaited": false + } + }, + { + "serverHeartbeatStartedEvent": { + "awaited": true + } + } + ] + } + ] + }, + { + "description": "connect with serverMonitoringMode=auto <4.4", + "runOnRequirements": [ + { + "maxServerVersion": "4.2.99" + } + ], + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "uriOptions": { + "serverMonitoringMode": "auto", + "heartbeatFrequencyMS": 500 + }, + "useMultipleMongoses": false, + "observeEvents": [ + "serverHeartbeatStartedEvent", + "serverHeartbeatSucceededEvent", + "serverHeartbeatFailedEvent" + ] + } + }, + { + "database": { + "id": "db", + "client": "client", + "databaseName": "sdam-tests" + } + } + ] + } + }, + { + "name": "runCommand", + "object": "db", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + } + }, + "expectResult": { + "ok": 1 + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "serverHeartbeatStartedEvent": {} + }, + "count": 2 + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "sdam", + "ignoreExtraEvents": true, + "events": [ + { + "serverHeartbeatStartedEvent": { + "awaited": false + } + }, + { + "serverHeartbeatSucceededEvent": { + "awaited": false + } + }, + { + "serverHeartbeatStartedEvent": { + "awaited": false + } + } + ] + } + ] + }, + { + "description": "connect with serverMonitoringMode=stream >=4.4", + "runOnRequirements": [ + { + "minServerVersion": "4.4.0" + } + ], + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "uriOptions": { + "serverMonitoringMode": "stream" + }, + "useMultipleMongoses": false, + "observeEvents": [ + "serverHeartbeatStartedEvent", + "serverHeartbeatSucceededEvent", + "serverHeartbeatFailedEvent" + ] + } + }, + { + "database": { + "id": "db", + "client": "client", + "databaseName": "sdam-tests" + } + } + ] + } + }, + { + "name": "runCommand", + "object": "db", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + } + }, + "expectResult": { + "ok": 1 + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "serverHeartbeatStartedEvent": {} + }, + "count": 2 + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "sdam", + "ignoreExtraEvents": true, + "events": [ + { + "serverHeartbeatStartedEvent": { + "awaited": false + } + }, + { + "serverHeartbeatSucceededEvent": { + "awaited": false + } + }, + { + "serverHeartbeatStartedEvent": { + "awaited": true + } + } + ] + } + ] + }, + { + "description": "connect with serverMonitoringMode=stream <4.4", + "runOnRequirements": [ + { + "maxServerVersion": "4.2.99" + } + ], + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "uriOptions": { + "serverMonitoringMode": "stream", + "heartbeatFrequencyMS": 500 + }, + "useMultipleMongoses": false, + "observeEvents": [ + "serverHeartbeatStartedEvent", + "serverHeartbeatSucceededEvent", + "serverHeartbeatFailedEvent" + ] + } + }, + { + "database": { + "id": "db", + "client": "client", + "databaseName": "sdam-tests" + } + } + ] + } + }, + { + "name": "runCommand", + "object": "db", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + } + }, + "expectResult": { + "ok": 1 + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "serverHeartbeatStartedEvent": {} + }, + "count": 2 + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "sdam", + "ignoreExtraEvents": true, + "events": [ + { + "serverHeartbeatStartedEvent": { + "awaited": false + } + }, + { + "serverHeartbeatSucceededEvent": { + "awaited": false + } + }, + { + "serverHeartbeatStartedEvent": { + "awaited": false + } + } + ] + } + ] + }, + { + "description": "connect with serverMonitoringMode=poll", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "uriOptions": { + "serverMonitoringMode": "poll", + "heartbeatFrequencyMS": 500 + }, + "useMultipleMongoses": false, + "observeEvents": [ + "serverHeartbeatStartedEvent", + "serverHeartbeatSucceededEvent", + "serverHeartbeatFailedEvent" + ] + } + }, + { + "database": { + "id": "db", + "client": "client", + "databaseName": "sdam-tests" + } + } + ] + } + }, + { + "name": "runCommand", + "object": "db", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + } + }, + "expectResult": { + "ok": 1 + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "serverHeartbeatStartedEvent": {} + }, + "count": 2 + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "sdam", + "ignoreExtraEvents": true, + "events": [ + { + "serverHeartbeatStartedEvent": { + "awaited": false + } + }, + { + "serverHeartbeatSucceededEvent": { + "awaited": false + } + }, + { + "serverHeartbeatStartedEvent": { + "awaited": false + } + } + ] + } + ] + } + ] +} diff --git a/source/server-discovery-and-monitoring/tests/unified/serverMonitoringMode.yml b/source/server-discovery-and-monitoring/tests/unified/serverMonitoringMode.yml new file mode 100644 index 0000000000..28c7853d04 --- /dev/null +++ b/source/server-discovery-and-monitoring/tests/unified/serverMonitoringMode.yml @@ -0,0 +1,173 @@ +description: serverMonitoringMode + +schemaVersion: "1.17" +# These tests cannot run on replica sets because the order of the expected +# SDAM events are non-deterministic when monitoring multiple servers. +# They also cannot run on Serverless or load balanced clusters where SDAM is disabled. +runOnRequirements: + - topologies: [single, sharded, sharded-replicaset] + serverless: forbid +tests: + - description: "connect with serverMonitoringMode=auto >=4.4" + runOnRequirements: + - minServerVersion: "4.4.0" + operations: + - name: createEntities + object: testRunner + arguments: + entities: + - client: + id: client + uriOptions: + serverMonitoringMode: "auto" + useMultipleMongoses: false + observeEvents: + - serverHeartbeatStartedEvent + - serverHeartbeatSucceededEvent + - serverHeartbeatFailedEvent + - database: + id: db + client: client + databaseName: sdam-tests + - &ping + name: runCommand + object: db + arguments: + commandName: ping + command: { ping: 1 } + expectResult: { ok: 1 } + # Wait for the second serverHeartbeatStartedEvent to ensure we start streaming. + - &waitForSecondHeartbeatStarted + name: waitForEvent + object: testRunner + arguments: + client: client + event: + serverHeartbeatStartedEvent: {} + count: 2 + expectEvents: &streamingStartedEvents + - client: client + eventType: sdam + ignoreExtraEvents: true + events: + - serverHeartbeatStartedEvent: + awaited: False + - serverHeartbeatSucceededEvent: + awaited: False + - serverHeartbeatStartedEvent: + awaited: True + + - description: "connect with serverMonitoringMode=auto <4.4" + runOnRequirements: + - maxServerVersion: "4.2.99" + operations: + - name: createEntities + object: testRunner + arguments: + entities: + - client: + id: client + uriOptions: + serverMonitoringMode: "auto" + heartbeatFrequencyMS: 500 + useMultipleMongoses: false + observeEvents: + - serverHeartbeatStartedEvent + - serverHeartbeatSucceededEvent + - serverHeartbeatFailedEvent + - database: + id: db + client: client + databaseName: sdam-tests + - *ping + # Wait for the second serverHeartbeatStartedEvent to ensure we do not stream. + - *waitForSecondHeartbeatStarted + expectEvents: &pollingStartedEvents + - client: client + eventType: sdam + ignoreExtraEvents: true + events: + - serverHeartbeatStartedEvent: + awaited: False + - serverHeartbeatSucceededEvent: + awaited: False + - serverHeartbeatStartedEvent: + awaited: False + + - description: "connect with serverMonitoringMode=stream >=4.4" + runOnRequirements: + - minServerVersion: "4.4.0" + operations: + - name: createEntities + object: testRunner + arguments: + entities: + - client: + id: client + uriOptions: + serverMonitoringMode: "stream" + useMultipleMongoses: false + observeEvents: + - serverHeartbeatStartedEvent + - serverHeartbeatSucceededEvent + - serverHeartbeatFailedEvent + - database: + id: db + client: client + databaseName: sdam-tests + - *ping + # Wait for the second serverHeartbeatStartedEvent to ensure we start streaming. + - *waitForSecondHeartbeatStarted + expectEvents: *streamingStartedEvents + + - description: "connect with serverMonitoringMode=stream <4.4" + runOnRequirements: + - maxServerVersion: "4.2.99" + operations: + - name: createEntities + object: testRunner + arguments: + entities: + - client: + id: client + uriOptions: + serverMonitoringMode: "stream" + heartbeatFrequencyMS: 500 + useMultipleMongoses: false + observeEvents: + - serverHeartbeatStartedEvent + - serverHeartbeatSucceededEvent + - serverHeartbeatFailedEvent + - database: + id: db + client: client + databaseName: sdam-tests + - *ping + # Wait for the second serverHeartbeatStartedEvent to ensure we do not stream. + - *waitForSecondHeartbeatStarted + expectEvents: *pollingStartedEvents + + - description: "connect with serverMonitoringMode=poll" + operations: + - name: createEntities + object: testRunner + arguments: + entities: + - client: + id: client + uriOptions: + serverMonitoringMode: "poll" + heartbeatFrequencyMS: 500 + useMultipleMongoses: false + observeEvents: + - serverHeartbeatStartedEvent + - serverHeartbeatSucceededEvent + - serverHeartbeatFailedEvent + - database: + id: db + client: client + databaseName: sdam-tests + - *ping + # Wait for the second serverHeartbeatStartedEvent to ensure we do not stream. + - *waitForSecondHeartbeatStarted + expectEvents: *pollingStartedEvents diff --git a/source/unified-test-format/schema-1.17.json b/source/unified-test-format/schema-1.17.json new file mode 100644 index 0000000000..7d321889a4 --- /dev/null +++ b/source/unified-test-format/schema-1.17.json @@ -0,0 +1,712 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + + "title": "Unified Test Format", + "type": "object", + "additionalProperties": false, + "required": ["description", "schemaVersion", "tests"], + "properties": { + "description": { "type": "string" }, + "schemaVersion": { "$ref": "#/definitions/version" }, + "runOnRequirements": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/definitions/runOnRequirement" } + }, + "createEntities": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/definitions/entity" } + }, + "initialData": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/definitions/collectionData" } + }, + "tests": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/definitions/test" } + }, + "_yamlAnchors": { + "type": "object", + "additionalProperties": true + } + }, + + "definitions": { + "version": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+){1,2}$" + }, + + "runOnRequirement": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "maxServerVersion": { "$ref": "#/definitions/version" }, + "minServerVersion": { "$ref": "#/definitions/version" }, + "topologies": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "enum": ["single", "replicaset", "sharded", "sharded-replicaset", "load-balanced"] + } + }, + "serverless": { + "type": "string", + "enum": ["require", "forbid", "allow"] + }, + "serverParameters": { + "type": "object", + "minProperties": 1 + }, + "auth": { "type": "boolean" }, + "csfle": { "type": "boolean" } + } + }, + + "entity": { + "type": "object", + "additionalProperties": false, + "maxProperties": 1, + "minProperties": 1, + "properties": { + "client": { + "type": "object", + "additionalProperties": false, + "required": ["id"], + "properties": { + "id": { "type": "string" }, + "uriOptions": { "type": "object" }, + "useMultipleMongoses": { "type": "boolean" }, + "observeEvents": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "enum": [ + "commandStartedEvent", + "commandSucceededEvent", + "commandFailedEvent", + "poolCreatedEvent", + "poolReadyEvent", + "poolClearedEvent", + "poolClosedEvent", + "connectionCreatedEvent", + "connectionReadyEvent", + "connectionClosedEvent", + "connectionCheckOutStartedEvent", + "connectionCheckOutFailedEvent", + "connectionCheckedOutEvent", + "connectionCheckedInEvent", + "serverDescriptionChangedEvent", + "topologyDescriptionChangedEvent" + ] + } + }, + "ignoreCommandMonitoringEvents": { + "type": "array", + "minItems": 1, + "items": { "type": "string" } + }, + "storeEventsAsEntities": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/definitions/storeEventsAsEntity" } + }, + "observeLogMessages": { + "type": "object", + "minProperties": 1, + "additionalProperties": false, + "properties": { + "command": { "$ref": "#/definitions/logSeverityLevel" }, + "topology": { "$ref": "#/definitions/logSeverityLevel" }, + "serverSelection": { "$ref": "#/definitions/logSeverityLevel" }, + "connection": { "$ref": "#/definitions/logSeverityLevel" } + } + }, + "serverApi": { "$ref": "#/definitions/serverApi" }, + "observeSensitiveCommands": { "type": "boolean" } + } + }, + "clientEncryption": { + "type": "object", + "additionalProperties": false, + "required": ["id", "clientEncryptionOpts"], + "properties": { + "id": { "type": "string" }, + "clientEncryptionOpts": { "$ref": "#/definitions/clientEncryptionOpts" } + } + }, + "database": { + "type": "object", + "additionalProperties": false, + "required": ["id", "client", "databaseName"], + "properties": { + "id": { "type": "string" }, + "client": { "type": "string" }, + "databaseName": { "type": "string" }, + "databaseOptions": { "$ref": "#/definitions/collectionOrDatabaseOptions" } + } + }, + "collection": { + "type": "object", + "additionalProperties": false, + "required": ["id", "database", "collectionName"], + "properties": { + "id": { "type": "string" }, + "database": { "type": "string" }, + "collectionName": { "type": "string" }, + "collectionOptions": { "$ref": "#/definitions/collectionOrDatabaseOptions" } + } + }, + "session": { + "type": "object", + "additionalProperties": false, + "required": ["id", "client"], + "properties": { + "id": { "type": "string" }, + "client": { "type": "string" }, + "sessionOptions": { "type": "object" } + } + }, + "bucket": { + "type": "object", + "additionalProperties": false, + "required": ["id", "database"], + "properties": { + "id": { "type": "string" }, + "database": { "type": "string" }, + "bucketOptions": { "type": "object" } + } + }, + "thread": { + "type": "object", + "additionalProperties": false, + "required": ["id"], + "properties": { + "id": { "type": "string" } + } + } + } + }, + + "logComponent": { + "type": "string", + "enum": ["command", "topology", "serverSelection", "connection"] + }, + + "logSeverityLevel": { + "type": "string", + "enum": ["emergency", "alert", "critical", "error", "warning", "notice", "info", "debug", "trace"] + }, + + "clientEncryptionOpts": { + "type": "object", + "additionalProperties": false, + "required": ["keyVaultClient", "keyVaultNamespace", "kmsProviders"], + "properties": { + "keyVaultClient": { "type": "string" }, + "keyVaultNamespace": { "type": "string" }, + "kmsProviders": { "$ref": "#/definitions/kmsProviders" } + } + }, + + "kmsProviders": { + "$defs": { + "stringOrPlaceholder": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "additionalProperties": false, + "required": ["$$placeholder"], + "properties": { + "$$placeholder": {} + } + } + ] + } + }, + "type": "object", + "additionalProperties": false, + "properties": { + "aws": { + "type": "object", + "additionalProperties": false, + "properties": { + "accessKeyId": { "$ref": "#/definitions/kmsProviders/$defs/stringOrPlaceholder" }, + "secretAccessKey": { "$ref": "#/definitions/kmsProviders/$defs/stringOrPlaceholder" }, + "sessionToken": { "$ref": "#/definitions/kmsProviders/$defs/stringOrPlaceholder" } + } + }, + "azure": { + "type": "object", + "additionalProperties": false, + "properties": { + "tenantId": { "$ref": "#/definitions/kmsProviders/$defs/stringOrPlaceholder" }, + "clientId": { "$ref": "#/definitions/kmsProviders/$defs/stringOrPlaceholder" }, + "clientSecret": { "$ref": "#/definitions/kmsProviders/$defs/stringOrPlaceholder" }, + "identityPlatformEndpoint": { "$ref": "#/definitions/kmsProviders/$defs/stringOrPlaceholder" } + } + }, + "gcp": { + "type": "object", + "additionalProperties": false, + "properties": { + "email": { "$ref": "#/definitions/kmsProviders/$defs/stringOrPlaceholder" }, + "privateKey": { "$ref": "#/definitions/kmsProviders/$defs/stringOrPlaceholder" }, + "endpoint": { "$ref": "#/definitions/kmsProviders/$defs/stringOrPlaceholder" } + } + }, + "kmip": { + "type": "object", + "additionalProperties": false, + "properties": { + "endpoint": { "$ref": "#/definitions/kmsProviders/$defs/stringOrPlaceholder" } + } + }, + "local": { + "type": "object", + "additionalProperties": false, + "properties": { + "key": { "$ref": "#/definitions/kmsProviders/$defs/stringOrPlaceholder" } + } + } + } + }, + + "storeEventsAsEntity": { + "type": "object", + "additionalProperties": false, + "required": ["id", "events"], + "properties": { + "id": { "type": "string" }, + "events": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "enum": [ + "PoolCreatedEvent", + "PoolReadyEvent", + "PoolClearedEvent", + "PoolClosedEvent", + "ConnectionCreatedEvent", + "ConnectionReadyEvent", + "ConnectionClosedEvent", + "ConnectionCheckOutStartedEvent", + "ConnectionCheckOutFailedEvent", + "ConnectionCheckedOutEvent", + "ConnectionCheckedInEvent", + "CommandStartedEvent", + "CommandSucceededEvent", + "CommandFailedEvent", + "ServerDescriptionChangedEvent", + "TopologyDescriptionChangedEvent" + ] + } + } + } + }, + + "collectionData": { + "type": "object", + "additionalProperties": false, + "required": ["collectionName", "databaseName", "documents"], + "properties": { + "collectionName": { "type": "string" }, + "databaseName": { "type": "string" }, + "createOptions": { + "type": "object", + "properties": { + "writeConcern": false + } + }, + "documents": { + "type": "array", + "items": { "type": "object" } + } + } + }, + + "expectedEventsForClient": { + "type": "object", + "additionalProperties": false, + "required": ["client", "events"], + "properties": { + "client": { "type": "string" }, + "eventType": { + "type": "string", + "enum": ["command", "cmap", "sdam"] + }, + "events": { "type": "array" }, + "ignoreExtraEvents": { "type": "boolean" } + }, + "oneOf": [ + { + "required": ["eventType"], + "properties": { + "eventType": { "const": "command" }, + "events": { + "type": "array", + "items": { "$ref": "#/definitions/expectedCommandEvent" } + } + } + }, + { + "required": ["eventType"], + "properties": { + "eventType": { "const": "cmap" }, + "events": { + "type": "array", + "items": { "$ref": "#/definitions/expectedCmapEvent" } + } + } + }, + { + "required": ["eventType"], + "properties": { + "eventType": { "const": "sdam" }, + "events": { + "type": "array", + "items": { "$ref": "#/definitions/expectedSdamEvent" } + } + } + }, + { + "additionalProperties": false, + "properties": { + "client": { "type": "string" }, + "events": { + "type": "array", + "items": { "$ref": "#/definitions/expectedCommandEvent" } + }, + "ignoreExtraEvents": { "type": "boolean" } + } + } + ] + }, + + "expectedCommandEvent": { + "type": "object", + "additionalProperties": false, + "maxProperties": 1, + "minProperties": 1, + "properties": { + "commandStartedEvent": { + "type": "object", + "additionalProperties": false, + "properties": { + "command": { "type": "object" }, + "commandName": { "type": "string" }, + "databaseName": { "type": "string" }, + "hasServiceId": { "type": "boolean" }, + "hasServerConnectionId": { "type": "boolean" } + } + }, + "commandSucceededEvent": { + "type": "object", + "additionalProperties": false, + "properties": { + "reply": { "type": "object" }, + "commandName": { "type": "string" }, + "databaseName": { "type": "string" }, + "hasServiceId": { "type": "boolean" }, + "hasServerConnectionId": { "type": "boolean" } + } + }, + "commandFailedEvent": { + "type": "object", + "additionalProperties": false, + "properties": { + "commandName": { "type": "string" }, + "databaseName": { "type": "string" }, + "hasServiceId": { "type": "boolean" }, + "hasServerConnectionId": { "type": "boolean" } + } + } + } + }, + + "expectedCmapEvent": { + "type": "object", + "additionalProperties": false, + "maxProperties": 1, + "minProperties": 1, + "properties": { + "poolCreatedEvent": { + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "poolReadyEvent": { + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "poolClearedEvent": { + "type": "object", + "additionalProperties": false, + "properties": { + "hasServiceId": { "type": "boolean" }, + "interruptInUseConnections": { "type": "boolean" } + } + }, + "poolClosedEvent": { + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "connectionCreatedEvent": { + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "connectionReadyEvent": { + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "connectionClosedEvent": { + "type": "object", + "additionalProperties": false, + "properties": { + "reason": { "type": "string" } + } + }, + "connectionCheckOutStartedEvent": { + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "connectionCheckOutFailedEvent": { + "type": "object", + "additionalProperties": false, + "properties": { + "reason": { "type": "string" } + } + }, + "connectionCheckedOutEvent": { + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "connectionCheckedInEvent": { + "type": "object", + "additionalProperties": false, + "properties": {} + } + } + }, + + "expectedSdamEvent": { + "type": "object", + "additionalProperties": false, + "maxProperties": 1, + "minProperties": 1, + "properties": { + "serverDescriptionChangedEvent": { + "type": "object", + "additionalProperties": false, + "properties": { + "previousDescription": { "$ref": "#/definitions/serverDescription" }, + "newDescription": { "$ref": "#/definitions/serverDescription" } + } + }, + "topologyDescriptionChangedEvent": { + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "serverHeartbeatStartedEvent": { + "type": "object", + "additionalProperties": false, + "properties": { + "awaited": { "type": "boolean" } + } + }, + "serverHeartbeatSucceededEvent": { + "type": "object", + "additionalProperties": false, + "properties": { + "awaited": { "type": "boolean" } + } + }, + "serverHeartbeatFailedEvent": { + "type": "object", + "additionalProperties": false, + "properties": { + "awaited": { "type": "boolean" } + } + } + } + }, + + "serverDescription": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "Standalone", + "Mongos", + "PossiblePrimary", + "RSPrimary", + "RSSecondary", + "RSOther", + "RSArbiter", + "RSGhost", + "LoadBalancer", + "Unknown" + ] + } + } + }, + + "expectedLogMessagesForClient": { + "type": "object", + "additionalProperties": false, + "required": ["client", "messages"], + "properties": { + "client": { "type": "string" }, + "messages": { + "type": "array", + "items": { "$ref": "#/definitions/expectedLogMessage" } + }, + "ignoreExtraMessages": { "type": "boolean" }, + "ignoreMessages": { + "type": "array", + "items": { "$ref": "#/definitions/expectedLogMessage" } + } + } + }, + + "expectedLogMessage": { + "type": "object", + "additionalProperties": false, + "required": ["level", "component", "data"], + "properties": { + "level": { "$ref": "#/definitions/logSeverityLevel" }, + "component": { "$ref": "#/definitions/logComponent" }, + "data": { "type": "object" }, + "failureIsRedacted": { "type": "boolean" } + } + }, + + "collectionOrDatabaseOptions": { + "type": "object", + "additionalProperties": false, + "properties": { + "readConcern": { "type": "object" }, + "readPreference": { "type": "object" }, + "writeConcern": { "type": "object" }, + "timeoutMS": { "type": "integer" } + } + }, + + "serverApi": { + "type": "object", + "additionalProperties": false, + "required": ["version"], + "properties": { + "version": { "type": "string" }, + "strict": { "type": "boolean" }, + "deprecationErrors": { "type": "boolean" } + } + }, + + "operation": { + "type": "object", + "additionalProperties": false, + "required": ["name", "object"], + "properties": { + "name": { "type": "string" }, + "object": { "type": "string" }, + "arguments": { "type": "object" }, + "ignoreResultAndError": { "type": "boolean" }, + "expectError": { "$ref": "#/definitions/expectedError" }, + "expectResult": {}, + "saveResultAsEntity": { "type": "string" } + }, + "allOf": [ + { "not": { "required": ["expectError", "expectResult"] } }, + { "not": { "required": ["expectError", "saveResultAsEntity"] } }, + { "not": { "required": ["ignoreResultAndError", "expectResult"] } }, + { "not": { "required": ["ignoreResultAndError", "expectError"] } }, + { "not": { "required": ["ignoreResultAndError", "saveResultAsEntity"] } } + ] + }, + + "expectedError": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "isError": { + "type": "boolean", + "const": true + }, + "isClientError": { "type": "boolean" }, + "isTimeoutError": { "type": "boolean" }, + "errorContains": { "type": "string" }, + "errorCode": { "type": "integer" }, + "errorCodeName": { "type": "string" }, + "errorLabelsContain": { + "type": "array", + "minItems": 1, + "items": { "type": "string" } + }, + "errorLabelsOmit": { + "type": "array", + "minItems": 1, + "items": { "type": "string" } + }, + "errorResponse": { + "type": "object" + }, + "expectResult": {} + } + }, + + "test": { + "type": "object", + "additionalProperties": false, + "required": ["description", "operations"], + "properties": { + "description": { "type": "string" }, + "runOnRequirements": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/definitions/runOnRequirement" } + }, + "skipReason": { "type": "string" }, + "operations": { + "type": "array", + "items": { "$ref": "#/definitions/operation" } + }, + "expectEvents": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/definitions/expectedEventsForClient" } + }, + "expectLogMessages": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/definitions/expectedLogMessagesForClient" } + }, + "outcome": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/definitions/collectionData" } + } + } + } + } + } diff --git a/source/unified-test-format/unified-test-format.rst b/source/unified-test-format/unified-test-format.rst index 43218840a9..72a9a691e8 100644 --- a/source/unified-test-format/unified-test-format.rst +++ b/source/unified-test-format/unified-test-format.rst @@ -4,7 +4,7 @@ Unified Test Format :Status: Accepted :Minimum Server Version: N/A -:Current Schema Version: 1.13.0 +:Current Schema Version: 1.17.0 .. contents:: @@ -598,6 +598,12 @@ The structure of this object is as follows: - `serverDescriptionChangedEvent `_ + - `serverHeartbeatStartedEvent `_ + + - `serverHeartbeatSucceededEvent `_ + + - `serverHeartbeatFailedEvent `_ + - `topologyDescriptionChangedEvent `_ .. _entity_client_ignoreCommandMonitoringEvents: @@ -1399,6 +1405,33 @@ The structure of this object is as follows: <../server-discovery-and-monitoring/server-discovery-and-monitoring.rst#servertype>`__ for a list of valid values. +.. _expectedEvent_serverHeartbeatStartedEvent: + +- ``serverHeartbeatStartedEvent``: Optional object. Assertions for one or more + `ServerHeartbeatStartedEvent <../server-discovery-and-monitoring/server-discovery-and-monitoring-logging-and-monitoring.rst#events>`__ fields. + + The structure of this object is as follows: + + - ``awaited``: Optional boolean. If specified, test runners MUST assert that the field is set and matches this value. + +.. _expectedEvent_serverHeartbeatSucceededEvent: + +- ``serverHeartbeatSucceededEvent``: Optional object. Assertions for one or more + `ServerHeartbeatSucceededEvent <../server-discovery-and-monitoring/server-discovery-and-monitoring-logging-and-monitoring.rst#events>`__ fields. + + The structure of this object is as follows: + + - ``awaited``: Optional boolean. If specified, test runners MUST assert that the field is set and matches this value. + +.. _expectedEvent_serverHeartbeatFailedEvent: + +- ``serverHeartbeatFailedEvent``: Optional object. Assertions for one or more + `ServerHeartbeatFailedEvent <../server-discovery-and-monitoring/server-discovery-and-monitoring-logging-and-monitoring.rst#events>`__ fields. + + The structure of this object is as follows: + + - ``awaited``: Optional boolean. If specified, test runners MUST assert that the field is set and matches this value. + .. _expectedEvent_topologyDescriptionChangedEvent: - ``topologyDescriptionChangedEvent``: Optional object. If present, this object @@ -4028,6 +4061,9 @@ Changelog .. Please note schema version bumps in changelog entries where applicable. +:2023-10-04: **Schema version 1.17.** + Add ``serverHeartbeatStartedEvent``, ``serverHeartbeatSucceededEvent``, and + ``serverHeartbeatFailedEvent`` for asserting on SDAM server heartbeat events. :2023-09-25: Clarify that the UTR is intended to be run against enterprise servers. :2022-07-18: **Schema version 1.16.** Add ``ignoreMessages`` and ``ignoreExtraMessages`` fields diff --git a/source/uri-options/tests/sdam-options.json b/source/uri-options/tests/sdam-options.json new file mode 100644 index 0000000000..673f5607ee --- /dev/null +++ b/source/uri-options/tests/sdam-options.json @@ -0,0 +1,46 @@ +{ + "tests": [ + { + "description": "serverMonitoringMode=auto", + "uri": "mongodb://example.com/?serverMonitoringMode=auto", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": { + "serverMonitoringMode": "auto" + } + }, + { + "description": "serverMonitoringMode=stream", + "uri": "mongodb://example.com/?serverMonitoringMode=stream", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": { + "serverMonitoringMode": "stream" + } + }, + { + "description": "serverMonitoringMode=poll", + "uri": "mongodb://example.com/?serverMonitoringMode=poll", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": { + "serverMonitoringMode": "poll" + } + }, + { + "description": "invalid serverMonitoringMode", + "uri": "mongodb://example.com/?serverMonitoringMode=invalid", + "valid": true, + "warning": true, + "hosts": null, + "auth": null, + "options": {} + } + ] +} diff --git a/source/uri-options/tests/sdam-options.yml b/source/uri-options/tests/sdam-options.yml new file mode 100644 index 0000000000..8f72ff4098 --- /dev/null +++ b/source/uri-options/tests/sdam-options.yml @@ -0,0 +1,35 @@ +tests: + - description: "serverMonitoringMode=auto" + uri: "mongodb://example.com/?serverMonitoringMode=auto" + valid: true + warning: false + hosts: ~ + auth: ~ + options: + serverMonitoringMode: "auto" + + - description: "serverMonitoringMode=stream" + uri: "mongodb://example.com/?serverMonitoringMode=stream" + valid: true + warning: false + hosts: ~ + auth: ~ + options: + serverMonitoringMode: "stream" + + - description: "serverMonitoringMode=poll" + uri: "mongodb://example.com/?serverMonitoringMode=poll" + valid: true + warning: false + hosts: ~ + auth: ~ + options: + serverMonitoringMode: "poll" + + - description: "invalid serverMonitoringMode" + uri: "mongodb://example.com/?serverMonitoringMode=invalid" + valid: true + warning: true + hosts: ~ + auth: ~ + options: {} diff --git a/source/uri-options/uri-options.rst b/source/uri-options/uri-options.rst index 035e7ddec5..8f8f4f0375 100644 --- a/source/uri-options/uri-options.rst +++ b/source/uri-options/uri-options.rst @@ -282,17 +282,23 @@ pertaining to URI options apply here. - The name of the replica set to connect to * - retryReads - - "true" or "false + - "true" or "false" - defined in `retryable reads spec `_ - no - Enables retryable reads on server 3.6+ * - retryWrites - - "true" or "false + - "true" or "false" - defined in `retryable writes spec `_ - no - Enables retryable writes on server 3.6+ + * - serverMonitoringMode + - "stream", "poll", or "auto" + - defined in `SDAM spec `__ + - required for multi-threaded or asynchronous drivers + - Configures which server monitoring protocol to use. + * - serverSelectionTimeoutMS - positive integer; a driver may also accept 0 to be used for a special case, provided that it documents the meaning - defined in `server selection spec `__ @@ -525,6 +531,7 @@ this specification MUST be updated to reflect those changes. Changelog --------- +:2023-08-21: Add serverMonitoringMode option. :2022-10-05: Remove spec front matter and reformat changelog. :2022-01-19: Add the timeoutMS option and deprecate some existing timeout options :2021-12-14: Add SOCKS5 options