From 9e86ce08ee3b7c9fc48fd290e7bbaa5b7da00566 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 13 Feb 2026 07:25:42 -0600 Subject: [PATCH 1/5] PYTHON-5695 Clarify NoWritesPerformed error label behavior when multiple retries occur --- test/asynchronous/test_retryable_writes.py | 176 ++++++++++++++++++++- test/test_retryable_writes.py | 176 ++++++++++++++++++++- 2 files changed, 350 insertions(+), 2 deletions(-) diff --git a/test/asynchronous/test_retryable_writes.py b/test/asynchronous/test_retryable_writes.py index ddb1d39eb7..4967288258 100644 --- a/test/asynchronous/test_retryable_writes.py +++ b/test/asynchronous/test_retryable_writes.py @@ -46,11 +46,12 @@ from pymongo.errors import ( AutoReconnect, ConnectionFailure, - OperationFailure, + NotPrimaryError, ServerSelectionTimeoutError, WriteConcernError, ) from pymongo.monitoring import ( + CommandFailedEvent, CommandSucceededEvent, ConnectionCheckedOutEvent, ConnectionCheckOutFailedEvent, @@ -601,5 +602,178 @@ def raise_connection_err_select_server(*args, **kwargs): self.assertEqual(sent_txn_id, final_txn_id, msg) +class TestErrorPropagationAfterEncounteringMultipleErrors(AsyncIntegrationTest): + # Only run against replica sets as mongos does not propagate the NoWritesPerformed label to the drivers. + @async_client_context.require_replica_set + # Run against server versions 6.0 and above. + @async_client_context.require_version_min(6, 0) + async def asyncSetUp(self) -> None: + await super().asyncSetUp() + await self.configure_fail_point(async_client_context.client, {}, off=True) + + async def asyncTearDown(self): + await self.configure_fail_point(async_client_context.client, {}, off=True) + return await super().asyncTearDown() + + async def test_01_drivers_return_the_correct_error_when_receiving_only_errors_without_NoWritesPerformed( + self + ): + # Create a client with retryWrites=true. + listener = OvertCommandListener() + task = None + + # Configure a fail point with error code 91 (ShutdownInProgress) with the RetryableError and SystemOverloadedError error labels. + command_args = { + "configureFailPoint": "failCommand", + "mode": {"times": 1}, + "data": { + "failCommands": ["insert"], + "errorLabels": ["RetryableError", "SystemOverloadedError"], + "errorCode": 91, + }, + } + + # Via the command monitoring CommandFailedEvent, configure a fail point with error code 10107 (NotWritablePrimary). + command_args_inner = { + "configureFailPoint": "failCommand", + "mode": "alwaysOn", + "data": { + "failCommands": ["insert"], + "errorCode": 10107, + "errorLabels": ["RetryableError", "SystemOverloadedError"], + }, + } + + def failed(event: CommandFailedEvent) -> None: + # Configure the 10107 fail point command only if the the failed event is for the 91 error configured in step 2. + nonlocal task + assert event.failure["code"] == 91 + task = asyncio.create_task(self.configure_fail_point(client, command_args_inner)) + + listener.failed = failed + + client = await self.async_rs_client(retryWrites=True, event_listeners=[listener]) + self.addAsyncCleanup(client.close) + + async with self.fail_point(command_args): + # Attempt an insertOne operation on any record for any database and collection. + # Expect the insertOne to fail with a server error. + with self.assertRaises(NotPrimaryError) as exc: + await client.test.test.insert_one({}) + + await task + + # Assert that the error code of the server error is 10107. + assert exc.exception.errors["code"] == 10107 + + async def test_02_drivers_return_the_correct_error_when_receiving_only_errors_with_NoWritesPerformed( + self + ): + # Create a client with retryWrites=true. + listener = OvertCommandListener() + task = None + + # Configure a fail point with error code 91 (ShutdownInProgress) with the RetryableError and SystemOverloadedError error labels. + command_args = { + "configureFailPoint": "failCommand", + "mode": {"times": 1}, + "data": { + "failCommands": ["insert"], + "errorLabels": ["RetryableError", "SystemOverloadedError", "NoWritesPerformed"], + "errorCode": 91, + }, + } + + # Via the command monitoring CommandFailedEvent, configure a fail point with error code `10107` (NotWritablePrimary) + # and a NoWritesPerformed label. + command_args_inner = { + "configureFailPoint": "failCommand", + "mode": "alwaysOn", + "data": { + "failCommands": ["insert"], + "errorCode": 10107, + "errorLabels": ["RetryableError", "SystemOverloadedError", "NoWritesPerformed"], + }, + } + + def failed(event: CommandFailedEvent) -> None: + # Configure the 10107 fail point command only if the the failed event is for the 91 error configured in step 2. + nonlocal task + assert event.failure["code"] == 91 + task = asyncio.create_task(self.configure_fail_point(client, command_args_inner)) + + listener.failed = failed + + client = await self.async_rs_client(retryWrites=True, event_listeners=[listener]) + self.addAsyncCleanup(client.close) + + async with self.fail_point(command_args): + # Attempt an insertOne operation on any record for any database and collection. + # Expect the insertOne to fail with a server error. + with self.assertRaises(NotPrimaryError) as exc: + await client.test.test.insert_one({}) + + await task + + # Assert that the error code of the server error is 91. + assert exc.exception.errors["code"] == 91 + + async def test_03_drivers_return_the_correct_error_when_receiving_some_errors_with_NoWritesPerformed_and_some_without_NoWritesPerformed( + self + ): + # Create a client with retryWrites=true. + listener = OvertCommandListener() + task = None + + # Configure the client to listen to CommandFailedEvents. In the attached listener, configure a fail point with error + # code `91` (NotWritablePrimary) and the `NoWritesPerformed`, `RetryableError` and `SystemOverloadedError` labels. + command_args_inner = { + "configureFailPoint": "failCommand", + "mode": {"times": 1}, + "data": { + "failCommands": ["insert"], + "errorLabels": ["RetryableError", "SystemOverloadedError", "NoWritesPerformed"], + "errorCode": 91, + }, + } + + # Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and + # `SystemOverloadedError` error labels but without the `NoWritesPerformed` error label. + command_args = { + "configureFailPoint": "failCommand", + "mode": {"times": 1}, + "data": { + "failCommands": ["insert"], + "errorCode": 91, + "errorLabels": ["RetryableError", "SystemOverloadedError"], + }, + } + + def failed(event: CommandFailedEvent) -> None: + # Configure the fail point command only if the the failed event is for the 91 error configured in step 2. + nonlocal task + print("here I am boo") + assert event.failure["code"] == 91 + task = asyncio.create_task(self.configure_fail_point(client, command_args_inner)) + + listener.failed = failed + + client = await self.async_rs_client(retryWrites=True, event_listeners=[listener]) + self.addAsyncCleanup(client.close) + + async with self.fail_point(command_args): + # Attempt an insertOne operation on any record for any database and collection. + # Expect the insertOne to fail with a server error. + with self.assertRaises(NotPrimaryError) as exc: + await client.test.test.insert_one({}) + + await task + + # Assert that the error code of the server error is 91. + assert exc.exception.errors["code"] == 91 + # Assert that the error does not contain the error label `NoWritesPerformed`. + assert "NoWritesPerformed" not in exc.exception.errors["errorLabels"] + + if __name__ == "__main__": unittest.main() diff --git a/test/test_retryable_writes.py b/test/test_retryable_writes.py index a74a3e8030..7979ba68a6 100644 --- a/test/test_retryable_writes.py +++ b/test/test_retryable_writes.py @@ -46,11 +46,12 @@ from pymongo.errors import ( AutoReconnect, ConnectionFailure, - OperationFailure, + NotPrimaryError, ServerSelectionTimeoutError, WriteConcernError, ) from pymongo.monitoring import ( + CommandFailedEvent, CommandSucceededEvent, ConnectionCheckedOutEvent, ConnectionCheckOutFailedEvent, @@ -597,5 +598,178 @@ def raise_connection_err_select_server(*args, **kwargs): self.assertEqual(sent_txn_id, final_txn_id, msg) +class TestErrorPropagationAfterEncounteringMultipleErrors(IntegrationTest): + # Only run against replica sets as mongos does not propagate the NoWritesPerformed label to the drivers. + @client_context.require_replica_set + # Run against server versions 6.0 and above. + @client_context.require_version_min(6, 0) + def setUp(self) -> None: + super().setUp() + self.configure_fail_point(client_context.client, {}, off=True) + + def tearDown(self): + self.configure_fail_point(client_context.client, {}, off=True) + return super().tearDown() + + def test_01_drivers_return_the_correct_error_when_receiving_only_errors_without_NoWritesPerformed( + self + ): + # Create a client with retryWrites=true. + listener = OvertCommandListener() + task = None + + # Configure a fail point with error code 91 (ShutdownInProgress) with the RetryableError and SystemOverloadedError error labels. + command_args = { + "configureFailPoint": "failCommand", + "mode": {"times": 1}, + "data": { + "failCommands": ["insert"], + "errorLabels": ["RetryableError", "SystemOverloadedError"], + "errorCode": 91, + }, + } + + # Via the command monitoring CommandFailedEvent, configure a fail point with error code 10107 (NotWritablePrimary). + command_args_inner = { + "configureFailPoint": "failCommand", + "mode": "alwaysOn", + "data": { + "failCommands": ["insert"], + "errorCode": 10107, + "errorLabels": ["RetryableError", "SystemOverloadedError"], + }, + } + + def failed(event: CommandFailedEvent) -> None: + # Configure the 10107 fail point command only if the the failed event is for the 91 error configured in step 2. + nonlocal task + assert event.failure["code"] == 91 + task = asyncio.create_task(self.configure_fail_point(client, command_args_inner)) + + listener.failed = failed + + client = self.rs_client(retryWrites=True, event_listeners=[listener]) + self.addCleanup(client.close) + + with self.fail_point(command_args): + # Attempt an insertOne operation on any record for any database and collection. + # Expect the insertOne to fail with a server error. + with self.assertRaises(NotPrimaryError) as exc: + client.test.test.insert_one({}) + + task + + # Assert that the error code of the server error is 10107. + assert exc.exception.errors["code"] == 10107 + + def test_02_drivers_return_the_correct_error_when_receiving_only_errors_with_NoWritesPerformed( + self + ): + # Create a client with retryWrites=true. + listener = OvertCommandListener() + task = None + + # Configure a fail point with error code 91 (ShutdownInProgress) with the RetryableError and SystemOverloadedError error labels. + command_args = { + "configureFailPoint": "failCommand", + "mode": {"times": 1}, + "data": { + "failCommands": ["insert"], + "errorLabels": ["RetryableError", "SystemOverloadedError", "NoWritesPerformed"], + "errorCode": 91, + }, + } + + # Via the command monitoring CommandFailedEvent, configure a fail point with error code `10107` (NotWritablePrimary) + # and a NoWritesPerformed label. + command_args_inner = { + "configureFailPoint": "failCommand", + "mode": "alwaysOn", + "data": { + "failCommands": ["insert"], + "errorCode": 10107, + "errorLabels": ["RetryableError", "SystemOverloadedError", "NoWritesPerformed"], + }, + } + + def failed(event: CommandFailedEvent) -> None: + # Configure the 10107 fail point command only if the the failed event is for the 91 error configured in step 2. + nonlocal task + assert event.failure["code"] == 91 + task = asyncio.create_task(self.configure_fail_point(client, command_args_inner)) + + listener.failed = failed + + client = self.rs_client(retryWrites=True, event_listeners=[listener]) + self.addCleanup(client.close) + + with self.fail_point(command_args): + # Attempt an insertOne operation on any record for any database and collection. + # Expect the insertOne to fail with a server error. + with self.assertRaises(NotPrimaryError) as exc: + client.test.test.insert_one({}) + + task + + # Assert that the error code of the server error is 91. + assert exc.exception.errors["code"] == 91 + + def test_03_drivers_return_the_correct_error_when_receiving_some_errors_with_NoWritesPerformed_and_some_without_NoWritesPerformed( + self + ): + # Create a client with retryWrites=true. + listener = OvertCommandListener() + task = None + + # Configure the client to listen to CommandFailedEvents. In the attached listener, configure a fail point with error + # code `91` (NotWritablePrimary) and the `NoWritesPerformed`, `RetryableError` and `SystemOverloadedError` labels. + command_args_inner = { + "configureFailPoint": "failCommand", + "mode": {"times": 1}, + "data": { + "failCommands": ["insert"], + "errorLabels": ["RetryableError", "SystemOverloadedError", "NoWritesPerformed"], + "errorCode": 91, + }, + } + + # Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and + # `SystemOverloadedError` error labels but without the `NoWritesPerformed` error label. + command_args = { + "configureFailPoint": "failCommand", + "mode": {"times": 1}, + "data": { + "failCommands": ["insert"], + "errorCode": 91, + "errorLabels": ["RetryableError", "SystemOverloadedError"], + }, + } + + def failed(event: CommandFailedEvent) -> None: + # Configure the fail point command only if the the failed event is for the 91 error configured in step 2. + nonlocal task + print("here I am boo") + assert event.failure["code"] == 91 + task = asyncio.create_task(self.configure_fail_point(client, command_args_inner)) + + listener.failed = failed + + client = self.rs_client(retryWrites=True, event_listeners=[listener]) + self.addCleanup(client.close) + + with self.fail_point(command_args): + # Attempt an insertOne operation on any record for any database and collection. + # Expect the insertOne to fail with a server error. + with self.assertRaises(NotPrimaryError) as exc: + client.test.test.insert_one({}) + + task + + # Assert that the error code of the server error is 91. + assert exc.exception.errors["code"] == 91 + # Assert that the error does not contain the error label `NoWritesPerformed`. + assert "NoWritesPerformed" not in exc.exception.errors["errorLabels"] + + if __name__ == "__main__": unittest.main() From f78f0b2db92fb6ca8aa491096a242e6e02772710 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 13 Feb 2026 15:56:40 -0600 Subject: [PATCH 2/5] update test --- test/asynchronous/test_retryable_writes.py | 88 +++++++++++++--------- test/test_retryable_writes.py | 88 +++++++++++++--------- 2 files changed, 108 insertions(+), 68 deletions(-) diff --git a/test/asynchronous/test_retryable_writes.py b/test/asynchronous/test_retryable_writes.py index 4967288258..a2eac05078 100644 --- a/test/asynchronous/test_retryable_writes.py +++ b/test/asynchronous/test_retryable_writes.py @@ -43,6 +43,7 @@ from bson.int64 import Int64 from bson.raw_bson import RawBSONDocument from bson.son import SON +from pymongo import MongoClient from pymongo.errors import ( AutoReconnect, ConnectionFailure, @@ -609,18 +610,23 @@ class TestErrorPropagationAfterEncounteringMultipleErrors(AsyncIntegrationTest): @async_client_context.require_version_min(6, 0) async def asyncSetUp(self) -> None: await super().asyncSetUp() - await self.configure_fail_point(async_client_context.client, {}, off=True) - - async def asyncTearDown(self): - await self.configure_fail_point(async_client_context.client, {}, off=True) - return await super().asyncTearDown() + self.setup_client = MongoClient() + self.addCleanup(self.setup_client.close) + + # TODO: After PYTHON-4595 we can use async event handlers and remove this workaround. + def configure_fail_point_sync(self, command_args, off=False): + cmd = {"configureFailPoint": "failCommand"} + cmd.update(command_args) + if off: + cmd["mode"] = "off" + cmd.pop("data", None) + self.setup_client.admin.command(cmd) async def test_01_drivers_return_the_correct_error_when_receiving_only_errors_without_NoWritesPerformed( self ): # Create a client with retryWrites=true. listener = OvertCommandListener() - task = None # Configure a fail point with error code 91 (ShutdownInProgress) with the RetryableError and SystemOverloadedError error labels. command_args = { @@ -646,32 +652,35 @@ async def test_01_drivers_return_the_correct_error_when_receiving_only_errors_wi def failed(event: CommandFailedEvent) -> None: # Configure the 10107 fail point command only if the the failed event is for the 91 error configured in step 2. - nonlocal task + if listener.failed_events: + return assert event.failure["code"] == 91 - task = asyncio.create_task(self.configure_fail_point(client, command_args_inner)) + self.configure_fail_point_sync(command_args_inner) + listener.failed_events.append(event) listener.failed = failed client = await self.async_rs_client(retryWrites=True, event_listeners=[listener]) self.addAsyncCleanup(client.close) - async with self.fail_point(command_args): - # Attempt an insertOne operation on any record for any database and collection. - # Expect the insertOne to fail with a server error. - with self.assertRaises(NotPrimaryError) as exc: - await client.test.test.insert_one({}) + self.configure_fail_point_sync(command_args) - await task + # Attempt an insertOne operation on any record for any database and collection. + # Expect the insertOne to fail with a server error. + with self.assertRaises(NotPrimaryError) as exc: + await client.test.test.insert_one({}) # Assert that the error code of the server error is 10107. assert exc.exception.errors["code"] == 10107 + # Disable the fail point. + self.configure_fail_point_sync({}, off=True) + async def test_02_drivers_return_the_correct_error_when_receiving_only_errors_with_NoWritesPerformed( self ): # Create a client with retryWrites=true. listener = OvertCommandListener() - task = None # Configure a fail point with error code 91 (ShutdownInProgress) with the RetryableError and SystemOverloadedError error labels. command_args = { @@ -697,39 +706,43 @@ async def test_02_drivers_return_the_correct_error_when_receiving_only_errors_wi } def failed(event: CommandFailedEvent) -> None: + if listener.failed_events: + return # Configure the 10107 fail point command only if the the failed event is for the 91 error configured in step 2. - nonlocal task assert event.failure["code"] == 91 - task = asyncio.create_task(self.configure_fail_point(client, command_args_inner)) + self.configure_fail_point_sync(command_args_inner) + listener.failed_events.append(event) listener.failed = failed client = await self.async_rs_client(retryWrites=True, event_listeners=[listener]) self.addAsyncCleanup(client.close) - async with self.fail_point(command_args): - # Attempt an insertOne operation on any record for any database and collection. - # Expect the insertOne to fail with a server error. - with self.assertRaises(NotPrimaryError) as exc: - await client.test.test.insert_one({}) + self.configure_fail_point_sync(command_args) - await task + # Attempt an insertOne operation on any record for any database and collection. + # Expect the insertOne to fail with a server error. + with self.assertRaises(NotPrimaryError) as exc: + await client.test.test.insert_one({}) # Assert that the error code of the server error is 91. assert exc.exception.errors["code"] == 91 + # Disable the fail point. + self.configure_fail_point_sync({}, off=True) + async def test_03_drivers_return_the_correct_error_when_receiving_some_errors_with_NoWritesPerformed_and_some_without_NoWritesPerformed( self ): + # TODO: read the expected behavior and add breakpoint() to the retry loop # Create a client with retryWrites=true. listener = OvertCommandListener() - task = None # Configure the client to listen to CommandFailedEvents. In the attached listener, configure a fail point with error # code `91` (NotWritablePrimary) and the `NoWritesPerformed`, `RetryableError` and `SystemOverloadedError` labels. command_args_inner = { "configureFailPoint": "failCommand", - "mode": {"times": 1}, + "mode": "alwaysOn", "data": { "failCommands": ["insert"], "errorLabels": ["RetryableError", "SystemOverloadedError", "NoWritesPerformed"], @@ -751,29 +764,36 @@ async def test_03_drivers_return_the_correct_error_when_receiving_some_errors_wi def failed(event: CommandFailedEvent) -> None: # Configure the fail point command only if the the failed event is for the 91 error configured in step 2. - nonlocal task - print("here I am boo") + print("hi") + if listener.failed_events: + return + print("ho") assert event.failure["code"] == 91 - task = asyncio.create_task(self.configure_fail_point(client, command_args_inner)) + self.configure_fail_point_sync(command_args_inner) + listener.failed_events.append(event) listener.failed = failed client = await self.async_rs_client(retryWrites=True, event_listeners=[listener]) self.addAsyncCleanup(client.close) - async with self.fail_point(command_args): - # Attempt an insertOne operation on any record for any database and collection. - # Expect the insertOne to fail with a server error. - with self.assertRaises(NotPrimaryError) as exc: - await client.test.test.insert_one({}) + self.configure_fail_point_sync(command_args) - await task + # Attempt an insertOne operation on any record for any database and collection. + # Expect the insertOne to fail with a server error. + from pymongo.errors import OperationFailure + + with self.assertRaises(Exception) as exc: + await client.test.test.insert_one({}) # Assert that the error code of the server error is 91. assert exc.exception.errors["code"] == 91 # Assert that the error does not contain the error label `NoWritesPerformed`. assert "NoWritesPerformed" not in exc.exception.errors["errorLabels"] + # Disable the fail point. + self.configure_fail_point_sync({}, off=True) + if __name__ == "__main__": unittest.main() diff --git a/test/test_retryable_writes.py b/test/test_retryable_writes.py index 7979ba68a6..85e11bf900 100644 --- a/test/test_retryable_writes.py +++ b/test/test_retryable_writes.py @@ -43,6 +43,7 @@ from bson.int64 import Int64 from bson.raw_bson import RawBSONDocument from bson.son import SON +from pymongo import MongoClient from pymongo.errors import ( AutoReconnect, ConnectionFailure, @@ -605,18 +606,23 @@ class TestErrorPropagationAfterEncounteringMultipleErrors(IntegrationTest): @client_context.require_version_min(6, 0) def setUp(self) -> None: super().setUp() - self.configure_fail_point(client_context.client, {}, off=True) - - def tearDown(self): - self.configure_fail_point(client_context.client, {}, off=True) - return super().tearDown() + self.setup_client = MongoClient() + self.addCleanup(self.setup_client.close) + + # TODO: After PYTHON-4595 we can use async event handlers and remove this workaround. + def configure_fail_point_sync(self, command_args, off=False): + cmd = {"configureFailPoint": "failCommand"} + cmd.update(command_args) + if off: + cmd["mode"] = "off" + cmd.pop("data", None) + self.setup_client.admin.command(cmd) def test_01_drivers_return_the_correct_error_when_receiving_only_errors_without_NoWritesPerformed( self ): # Create a client with retryWrites=true. listener = OvertCommandListener() - task = None # Configure a fail point with error code 91 (ShutdownInProgress) with the RetryableError and SystemOverloadedError error labels. command_args = { @@ -642,32 +648,35 @@ def test_01_drivers_return_the_correct_error_when_receiving_only_errors_without_ def failed(event: CommandFailedEvent) -> None: # Configure the 10107 fail point command only if the the failed event is for the 91 error configured in step 2. - nonlocal task + if listener.failed_events: + return assert event.failure["code"] == 91 - task = asyncio.create_task(self.configure_fail_point(client, command_args_inner)) + self.configure_fail_point_sync(command_args_inner) + listener.failed_events.append(event) listener.failed = failed client = self.rs_client(retryWrites=True, event_listeners=[listener]) self.addCleanup(client.close) - with self.fail_point(command_args): - # Attempt an insertOne operation on any record for any database and collection. - # Expect the insertOne to fail with a server error. - with self.assertRaises(NotPrimaryError) as exc: - client.test.test.insert_one({}) + self.configure_fail_point_sync(command_args) - task + # Attempt an insertOne operation on any record for any database and collection. + # Expect the insertOne to fail with a server error. + with self.assertRaises(NotPrimaryError) as exc: + client.test.test.insert_one({}) # Assert that the error code of the server error is 10107. assert exc.exception.errors["code"] == 10107 + # Disable the fail point. + self.configure_fail_point_sync({}, off=True) + def test_02_drivers_return_the_correct_error_when_receiving_only_errors_with_NoWritesPerformed( self ): # Create a client with retryWrites=true. listener = OvertCommandListener() - task = None # Configure a fail point with error code 91 (ShutdownInProgress) with the RetryableError and SystemOverloadedError error labels. command_args = { @@ -693,39 +702,43 @@ def test_02_drivers_return_the_correct_error_when_receiving_only_errors_with_NoW } def failed(event: CommandFailedEvent) -> None: + if listener.failed_events: + return # Configure the 10107 fail point command only if the the failed event is for the 91 error configured in step 2. - nonlocal task assert event.failure["code"] == 91 - task = asyncio.create_task(self.configure_fail_point(client, command_args_inner)) + self.configure_fail_point_sync(command_args_inner) + listener.failed_events.append(event) listener.failed = failed client = self.rs_client(retryWrites=True, event_listeners=[listener]) self.addCleanup(client.close) - with self.fail_point(command_args): - # Attempt an insertOne operation on any record for any database and collection. - # Expect the insertOne to fail with a server error. - with self.assertRaises(NotPrimaryError) as exc: - client.test.test.insert_one({}) + self.configure_fail_point_sync(command_args) - task + # Attempt an insertOne operation on any record for any database and collection. + # Expect the insertOne to fail with a server error. + with self.assertRaises(NotPrimaryError) as exc: + client.test.test.insert_one({}) # Assert that the error code of the server error is 91. assert exc.exception.errors["code"] == 91 + # Disable the fail point. + self.configure_fail_point_sync({}, off=True) + def test_03_drivers_return_the_correct_error_when_receiving_some_errors_with_NoWritesPerformed_and_some_without_NoWritesPerformed( self ): + # TODO: read the expected behavior and add breakpoint() to the retry loop # Create a client with retryWrites=true. listener = OvertCommandListener() - task = None # Configure the client to listen to CommandFailedEvents. In the attached listener, configure a fail point with error # code `91` (NotWritablePrimary) and the `NoWritesPerformed`, `RetryableError` and `SystemOverloadedError` labels. command_args_inner = { "configureFailPoint": "failCommand", - "mode": {"times": 1}, + "mode": "alwaysOn", "data": { "failCommands": ["insert"], "errorLabels": ["RetryableError", "SystemOverloadedError", "NoWritesPerformed"], @@ -747,29 +760,36 @@ def test_03_drivers_return_the_correct_error_when_receiving_some_errors_with_NoW def failed(event: CommandFailedEvent) -> None: # Configure the fail point command only if the the failed event is for the 91 error configured in step 2. - nonlocal task - print("here I am boo") + print("hi") + if listener.failed_events: + return + print("ho") assert event.failure["code"] == 91 - task = asyncio.create_task(self.configure_fail_point(client, command_args_inner)) + self.configure_fail_point_sync(command_args_inner) + listener.failed_events.append(event) listener.failed = failed client = self.rs_client(retryWrites=True, event_listeners=[listener]) self.addCleanup(client.close) - with self.fail_point(command_args): - # Attempt an insertOne operation on any record for any database and collection. - # Expect the insertOne to fail with a server error. - with self.assertRaises(NotPrimaryError) as exc: - client.test.test.insert_one({}) + self.configure_fail_point_sync(command_args) - task + # Attempt an insertOne operation on any record for any database and collection. + # Expect the insertOne to fail with a server error. + from pymongo.errors import OperationFailure + + with self.assertRaises(Exception) as exc: + client.test.test.insert_one({}) # Assert that the error code of the server error is 91. assert exc.exception.errors["code"] == 91 # Assert that the error does not contain the error label `NoWritesPerformed`. assert "NoWritesPerformed" not in exc.exception.errors["errorLabels"] + # Disable the fail point. + self.configure_fail_point_sync({}, off=True) + if __name__ == "__main__": unittest.main() From 0dc9c28714067b6e316014fdc8c6d92b46035e63 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 17 Feb 2026 07:28:48 -0600 Subject: [PATCH 3/5] typing --- test/__init__.py | 2 +- test/asynchronous/__init__.py | 2 +- test/asynchronous/test_retryable_writes.py | 10 ++++------ test/test_retryable_writes.py | 10 ++++------ 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/test/__init__.py b/test/__init__.py index 8540c442e0..f14b8ad255 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -490,7 +490,7 @@ def require_connection(self, func: Any) -> Any: func=func, ) - def require_version_min(self, *ver): + def require_version_min(self, *ver) -> Any: """Run a test only if the server version is at least ``version``.""" other_version = Version(*ver) return self._require( diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index 4dde0acf1f..70c68163ae 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -490,7 +490,7 @@ def require_connection(self, func: Any) -> Any: func=func, ) - def require_version_min(self, *ver): + def require_version_min(self, *ver) -> Any: """Run a test only if the server version is at least ``version``.""" other_version = Version(*ver) return self._require( diff --git a/test/asynchronous/test_retryable_writes.py b/test/asynchronous/test_retryable_writes.py index a2eac05078..b539243e96 100644 --- a/test/asynchronous/test_retryable_writes.py +++ b/test/asynchronous/test_retryable_writes.py @@ -614,7 +614,7 @@ async def asyncSetUp(self) -> None: self.addCleanup(self.setup_client.close) # TODO: After PYTHON-4595 we can use async event handlers and remove this workaround. - def configure_fail_point_sync(self, command_args, off=False): + def configure_fail_point_sync(self, command_args, off=False) -> None: cmd = {"configureFailPoint": "failCommand"} cmd.update(command_args) if off: @@ -624,7 +624,7 @@ def configure_fail_point_sync(self, command_args, off=False): async def test_01_drivers_return_the_correct_error_when_receiving_only_errors_without_NoWritesPerformed( self - ): + ) -> None: # Create a client with retryWrites=true. listener = OvertCommandListener() @@ -678,7 +678,7 @@ def failed(event: CommandFailedEvent) -> None: async def test_02_drivers_return_the_correct_error_when_receiving_only_errors_with_NoWritesPerformed( self - ): + ) -> None: # Create a client with retryWrites=true. listener = OvertCommandListener() @@ -733,7 +733,7 @@ def failed(event: CommandFailedEvent) -> None: async def test_03_drivers_return_the_correct_error_when_receiving_some_errors_with_NoWritesPerformed_and_some_without_NoWritesPerformed( self - ): + ) -> None: # TODO: read the expected behavior and add breakpoint() to the retry loop # Create a client with retryWrites=true. listener = OvertCommandListener() @@ -764,10 +764,8 @@ async def test_03_drivers_return_the_correct_error_when_receiving_some_errors_wi def failed(event: CommandFailedEvent) -> None: # Configure the fail point command only if the the failed event is for the 91 error configured in step 2. - print("hi") if listener.failed_events: return - print("ho") assert event.failure["code"] == 91 self.configure_fail_point_sync(command_args_inner) listener.failed_events.append(event) diff --git a/test/test_retryable_writes.py b/test/test_retryable_writes.py index 85e11bf900..1be93de77e 100644 --- a/test/test_retryable_writes.py +++ b/test/test_retryable_writes.py @@ -610,7 +610,7 @@ def setUp(self) -> None: self.addCleanup(self.setup_client.close) # TODO: After PYTHON-4595 we can use async event handlers and remove this workaround. - def configure_fail_point_sync(self, command_args, off=False): + def configure_fail_point_sync(self, command_args, off=False) -> None: cmd = {"configureFailPoint": "failCommand"} cmd.update(command_args) if off: @@ -620,7 +620,7 @@ def configure_fail_point_sync(self, command_args, off=False): def test_01_drivers_return_the_correct_error_when_receiving_only_errors_without_NoWritesPerformed( self - ): + ) -> None: # Create a client with retryWrites=true. listener = OvertCommandListener() @@ -674,7 +674,7 @@ def failed(event: CommandFailedEvent) -> None: def test_02_drivers_return_the_correct_error_when_receiving_only_errors_with_NoWritesPerformed( self - ): + ) -> None: # Create a client with retryWrites=true. listener = OvertCommandListener() @@ -729,7 +729,7 @@ def failed(event: CommandFailedEvent) -> None: def test_03_drivers_return_the_correct_error_when_receiving_some_errors_with_NoWritesPerformed_and_some_without_NoWritesPerformed( self - ): + ) -> None: # TODO: read the expected behavior and add breakpoint() to the retry loop # Create a client with retryWrites=true. listener = OvertCommandListener() @@ -760,10 +760,8 @@ def test_03_drivers_return_the_correct_error_when_receiving_some_errors_with_NoW def failed(event: CommandFailedEvent) -> None: # Configure the fail point command only if the the failed event is for the 91 error configured in step 2. - print("hi") if listener.failed_events: return - print("ho") assert event.failure["code"] == 91 self.configure_fail_point_sync(command_args_inner) listener.failed_events.append(event) From db2c107af8687877854f41dc48409d78666b9cb6 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 17 Feb 2026 16:47:09 -0600 Subject: [PATCH 4/5] fix handling of client opts --- test/asynchronous/test_retryable_writes.py | 2 +- test/test_retryable_writes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/asynchronous/test_retryable_writes.py b/test/asynchronous/test_retryable_writes.py index b539243e96..57c7e2eea6 100644 --- a/test/asynchronous/test_retryable_writes.py +++ b/test/asynchronous/test_retryable_writes.py @@ -610,7 +610,7 @@ class TestErrorPropagationAfterEncounteringMultipleErrors(AsyncIntegrationTest): @async_client_context.require_version_min(6, 0) async def asyncSetUp(self) -> None: await super().asyncSetUp() - self.setup_client = MongoClient() + self.setup_client = MongoClient(**async_client_context.default_client_options) self.addCleanup(self.setup_client.close) # TODO: After PYTHON-4595 we can use async event handlers and remove this workaround. diff --git a/test/test_retryable_writes.py b/test/test_retryable_writes.py index 1be93de77e..b04c324a45 100644 --- a/test/test_retryable_writes.py +++ b/test/test_retryable_writes.py @@ -606,7 +606,7 @@ class TestErrorPropagationAfterEncounteringMultipleErrors(IntegrationTest): @client_context.require_version_min(6, 0) def setUp(self) -> None: super().setUp() - self.setup_client = MongoClient() + self.setup_client = MongoClient(**client_context.default_client_options) self.addCleanup(self.setup_client.close) # TODO: After PYTHON-4595 we can use async event handlers and remove this workaround. From 5d07136044cc6b28468af80ba9b03fe91ec76741 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 17 Feb 2026 16:57:42 -0600 Subject: [PATCH 5/5] typing --- test/__init__.py | 2 +- test/asynchronous/__init__.py | 2 +- test/asynchronous/test_retryable_writes.py | 6 +++--- test/test_retryable_writes.py | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/__init__.py b/test/__init__.py index f14b8ad255..8540c442e0 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -490,7 +490,7 @@ def require_connection(self, func: Any) -> Any: func=func, ) - def require_version_min(self, *ver) -> Any: + def require_version_min(self, *ver): """Run a test only if the server version is at least ``version``.""" other_version = Version(*ver) return self._require( diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index 70c68163ae..4dde0acf1f 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -490,7 +490,7 @@ def require_connection(self, func: Any) -> Any: func=func, ) - def require_version_min(self, *ver) -> Any: + def require_version_min(self, *ver): """Run a test only if the server version is at least ``version``.""" other_version = Version(*ver) return self._require( diff --git a/test/asynchronous/test_retryable_writes.py b/test/asynchronous/test_retryable_writes.py index 57c7e2eea6..6b1b8ef681 100644 --- a/test/asynchronous/test_retryable_writes.py +++ b/test/asynchronous/test_retryable_writes.py @@ -607,7 +607,7 @@ class TestErrorPropagationAfterEncounteringMultipleErrors(AsyncIntegrationTest): # Only run against replica sets as mongos does not propagate the NoWritesPerformed label to the drivers. @async_client_context.require_replica_set # Run against server versions 6.0 and above. - @async_client_context.require_version_min(6, 0) + @async_client_context.require_version_min(6, 0) # type: ignore[untyped-decorator] async def asyncSetUp(self) -> None: await super().asyncSetUp() self.setup_client = MongoClient(**async_client_context.default_client_options) @@ -671,7 +671,7 @@ def failed(event: CommandFailedEvent) -> None: await client.test.test.insert_one({}) # Assert that the error code of the server error is 10107. - assert exc.exception.errors["code"] == 10107 + assert exc.exception.errors["code"] == 10107 # type:ignore[call-overload] # Disable the fail point. self.configure_fail_point_sync({}, off=True) @@ -726,7 +726,7 @@ def failed(event: CommandFailedEvent) -> None: await client.test.test.insert_one({}) # Assert that the error code of the server error is 91. - assert exc.exception.errors["code"] == 91 + assert exc.exception.errors["code"] == 91 # type:ignore[call-overload] # Disable the fail point. self.configure_fail_point_sync({}, off=True) diff --git a/test/test_retryable_writes.py b/test/test_retryable_writes.py index b04c324a45..dd55d371dc 100644 --- a/test/test_retryable_writes.py +++ b/test/test_retryable_writes.py @@ -603,7 +603,7 @@ class TestErrorPropagationAfterEncounteringMultipleErrors(IntegrationTest): # Only run against replica sets as mongos does not propagate the NoWritesPerformed label to the drivers. @client_context.require_replica_set # Run against server versions 6.0 and above. - @client_context.require_version_min(6, 0) + @client_context.require_version_min(6, 0) # type: ignore[untyped-decorator] def setUp(self) -> None: super().setUp() self.setup_client = MongoClient(**client_context.default_client_options) @@ -667,7 +667,7 @@ def failed(event: CommandFailedEvent) -> None: client.test.test.insert_one({}) # Assert that the error code of the server error is 10107. - assert exc.exception.errors["code"] == 10107 + assert exc.exception.errors["code"] == 10107 # type:ignore[call-overload] # Disable the fail point. self.configure_fail_point_sync({}, off=True) @@ -722,7 +722,7 @@ def failed(event: CommandFailedEvent) -> None: client.test.test.insert_one({}) # Assert that the error code of the server error is 91. - assert exc.exception.errors["code"] == 91 + assert exc.exception.errors["code"] == 91 # type:ignore[call-overload] # Disable the fail point. self.configure_fail_point_sync({}, off=True)