PYTHON-5306 - Fix use of public MongoClient attributes before connection#2285
PYTHON-5306 - Fix use of public MongoClient attributes before connection#2285NoahStapp merged 12 commits intomongodb:masterfrom
Conversation
|
|
||
| .. versionadded:: 4.0 | ||
| """ | ||
| if self._topology is None: |
There was a problem hiding this comment.
If the client has not connected yet, return a TopologyDescription with placeholder servers derived from the seeds passed to the client's constructor.
There was a problem hiding this comment.
We should make sure the returned TopologyDescription has the same _topology_id. Otherwise it could be confusing because _topology_id will change.
There was a problem hiding this comment.
Also can you give an example of what topology_description looks like in the connect=False srv case?
There was a problem hiding this comment.
<TopologyDescription id: 67ffb68a6e12ca176991f521, topology_type: Unknown, servers: [<ServerDescription (('test1.test.build.10gen.cc', None), 27017) server_type: Unknown, rtt: None>]>
There was a problem hiding this comment.
Please include an example of this new/old behavior in the Jira ticket when closing.
| to any servers, or a network partition causes it to lose connection | ||
| to all servers. | ||
| """ | ||
| if self._topology is None: |
There was a problem hiding this comment.
As the docstring defines, we expect nodes to return an empty set if the client has not yet connected.
|
|
||
| .. versionadded:: 4.0 | ||
| """ | ||
| if self._topology is None: |
There was a problem hiding this comment.
We should make sure the returned TopologyDescription has the same _topology_id. Otherwise it could be confusing because _topology_id will change.
test/asynchronous/test_client.py
Outdated
| {("unknown", None): ServerDescription(("unknown", None))}, | ||
| ) | ||
| await client.aconnect() | ||
| self.assertEqual(await client.address, None) |
There was a problem hiding this comment.
Can we add a tests for address and all the helper helpers without calling aconnect?
There was a problem hiding this comment.
This test was mistakenly included: our test SRV URIs aren't real servers, so we can't actually connect to them.
There was a problem hiding this comment.
This is actually a larger restriction: the new test needs to be run against a real SRV server, or have accurate responses mocked out, to ensure correct behavior.
For all non-SRV URIs, we immediately create a Topology, making this test on such connections useless.
There was a problem hiding this comment.
We do have real SRV addresses which resolve to localhost:
>>> client = MongoClient("mongodb+srv://test1.test.build.10gen.cc", tlsInsecure=True)
>>> client.topology_description
<TopologyDescription id: 68000f362bab7f34cc9902c2, topology_type: ReplicaSetWithPrimary, servers: [<ServerDescription ('localhost', 27017) server_type: RSPrimary, rtt: 0.001482416999351699>]>There was a problem hiding this comment.
Ah, I forgot that SRV implies replica + SSL, thanks!
test/asynchronous/test_client.py
Outdated
| # arbiters causes client to block until connected | ||
| await c.arbiters | ||
| self.assertIsNotNone(c._topology) | ||
| c = await self.async_rs_or_single_client(connect=False) |
There was a problem hiding this comment.
In this kind of test it's a good idea to close each client once it's no longer needed to avoid building up unclosed clients.
There was a problem hiding this comment.
async_rs_or_single_client already automatically closes all clients at the end of the method.
There was a problem hiding this comment.
I know but I'm saying it's better to close them explicitly to avoid building up unclosed clients.
There was a problem hiding this comment.
We don't currently use that pattern in our testcases, relying on the helper client creation methods to clean up clients for us. If it's better to avoid relying on those, should we rethink our approach?
There was a problem hiding this comment.
I'm saying for tests that specifically open multiple/many clients, it's good practice to add an explicit call to close().
There was a problem hiding this comment.
Not trying to pushback stubbornly here, just want to keep our codebase consistent where possible since we don't currently explicitly close clients created with the helpers. Do we have a measurable improvement or benefit from explicitly closing clients that will be cleaned up by the test?
There was a problem hiding this comment.
It's not strictly a performance issue. It's also about easier debugging. Leaving open clients around unnecessarily like this makes debugging harder because each client has many threads/tasks causing noise.
There was a problem hiding this comment.
Ah, makes sense! I'll open a ticket to make sure we follow the same pattern elsewhere too.
|
|
||
| .. versionadded:: 4.0 | ||
| """ | ||
| if self._topology is None: |
There was a problem hiding this comment.
Also can you give an example of what topology_description looks like in the connect=False srv case?
pymongo/synchronous/mongo_client.py
Outdated
| None, | ||
| None, | ||
| None, | ||
| TopologySettings(), |
There was a problem hiding this comment.
This is missing certain settings that are publicly accessible, like TopologyDescription.heartbeat_frequency.
In the ticket I suggested making the MongoClient constructor always create the Topology instance, and then mutate it later once the SRV info is resolved. Did you consider that approach? It would avoid the problem of attempting to mock out these attributes and avoid the type-ignores since everything would always be non-None.
(It could be that this approach is a better fit, I just want to know if an alternative was considered.)
There was a problem hiding this comment.
I started with the approach you suggested, but switched to the current implementation for a few reasons:
topology_descriptionis the only public attribute that requires changes of meaningful size. The rest either already do IO or arenodes, which has a trivial workaround.- We don't currently mutate
Topologyinstances orTopologySettingsinstances directly. To do so, we'd need to make more code changes and be confident that we haven't overlooked any problematic edge cases. - I foresee more potential bugs/confusion caused by introducing a new state specifically for SRV Topologies where we know that the Topology we currently have is not actually correct, and will be mutated upon SRV resolution.
This approach has fewer changes that could cause errors, especially since it isolates changes to resolving the specific issue at hand.
There was a problem hiding this comment.
Thanks, could you update TopologySettings() here to be more accurate then?
There was a problem hiding this comment.
TopologySettings sets defaults for each attribute, is passing no arguments to it invalid?
There was a problem hiding this comment.
Like I said above, client.topology_description.heartbeat_frequency is one example of an attribute we need to set here to avoid heartbeat_frequency appearing to change from one value to another. There may be others.
There was a problem hiding this comment.
For example:
>>> client = MongoClient(..., connect=False, heartbeatFrequencyMS=99999)
>>> client.topology_description.heartbeat_frequency
10
>>> client.admin.command("ping")
...
>>> client.topology_description.heartbeat_frequency
99.999There was a problem hiding this comment.
Sorry, I misunderstood what you meant. The example makes it clear, thanks!
There was a problem hiding this comment.
Now we always initialize a TopologySettings in the constructor. If SRV is enabled, we create a new one after resolution but keep the existing topology id.
test/asynchronous/test_client.py
Outdated
| with self.assertRaises(ConnectionFailure): | ||
| await c.pymongo_test.test.find_one() | ||
|
|
||
| @async_client_context.require_replica_set |
There was a problem hiding this comment.
I believe we can remove require_replica_set.
There was a problem hiding this comment.
Removing it causes failures on any non-replica topology: https://spruce.mongodb.com/task/mongo_python_driver_mongodb_latest_test_python3.9_auth_ssl_standalone_cov_patch_846b1fc25ced5979ade934bb27d6ffbb60f68267_68010865cc419c000710589f_25_04_17_13_56_05?execution=0&sortBy=STATUS&sortDir=ASC
I assume this is due to SRV requiring SSL + replica to function.
There was a problem hiding this comment.
Oh I see. "mongodb+srv://test1.test.build.10gen.cc" returns 2 hosts so it can't connect to standalone. It can connect to Mongos though so we should run this test on both replica set and sharded.
There was a problem hiding this comment.
require_no_standalone, then?
There was a problem hiding this comment.
Load balancers probably dont work here so it would need to be @require_no_standalone+@require_no_load_balancer+@require_no_load_balancer
test/asynchronous/test_client.py
Outdated
| # arbiters causes client to block until connected | ||
| await c.arbiters | ||
| self.assertIsNotNone(c._topology) | ||
| c = await self.async_rs_or_single_client(connect=False) |
There was a problem hiding this comment.
I'm saying for tests that specifically open multiple/many clients, it's good practice to add an explicit call to close().
pymongo/asynchronous/mongo_client.py
Outdated
| self._timeout: float | None = None | ||
| self._topology_settings: TopologySettings = None # type: ignore[assignment] | ||
| self._event_listeners: _EventListeners | None = None | ||
| self._initial_topology_id: Optional[ObjectId] = None # type: ignore[assignment] |
There was a problem hiding this comment.
Could we remove the type ignore here?
|
|
||
| .. versionadded:: 4.0 | ||
| """ | ||
| if self._topology is None: |
There was a problem hiding this comment.
Please include an example of this new/old behavior in the Jira ticket when closing.
pymongo/synchronous/mongo_client.py
Outdated
| None, | ||
| None, | ||
| None, | ||
| TopologySettings(), |
There was a problem hiding this comment.
Thanks, could you update TopologySettings() here to be more accurate then?
ShaneHarvey
left a comment
There was a problem hiding this comment.
Let's add a changelog entry and call it a day!
doc/changelog.rst
Outdated
| - Fixed a bug that could raise ``UnboundLocalError`` when creating asynchronous connections over SSL. | ||
| - Fixed a bug causing SRV hostname validation to fail when resolver and resolved hostnames are identical with three domain levels. | ||
| - Fixed a bug that caused direct use of ``pymongo.uri_parser`` to raise an ``AttributeError``. | ||
| - Fixed a bug that could cause public ``pymongo.MongoClient`` and ``pymongo.AsyncMongoClient`` attributes to raise errors when accessed before a connection was established. |
There was a problem hiding this comment.
Let's describe it a bit more specifically, something like:
Fixed a bug where clients created with connect=False and a "mongodb+srv://" connection string
could cause public ``pymongo.MongoClient`` and ``pymongo.AsyncMongoClient`` attributes (topology_description,
nodes, address, primary, secondaries, arbiters) to incorrectly return a Database, leading to type
errors such as: "NotImplementedError: Database objects do not implement truth value testing or bool()".
doc/changelog.rst
Outdated
| - Fixed a bug where clients created with connect=False and a "mongodb+srv://" connection string | ||
| could cause public ``pymongo.MongoClient`` and ``pymongo.AsyncMongoClient`` attributes (topology_description, | ||
| nodes, address, primary, secondaries, arbiters) to incorrectly return a Database, leading to type | ||
| errors such as: "NotImplementedError: Database objects do not implement truth value testing or bool(). |
There was a problem hiding this comment.
Missing trailing " for "NotImplementedError.
The public attributes of
MongoClientaretopology_description,nodes,address,primary,secondaries, andarbiters. Of these, onlytopology_descriptionandnodesdo not already perform network I/O. The rest can safely ensure connection as part of their execution without modifying their previous behavior.