Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PYTHON-3636 MongoClient should perform SRV resolution lazily #2191

Open
wants to merge 61 commits into
base: master
Choose a base branch
from

Conversation

sleepyStick
Copy link
Contributor

@sleepyStick sleepyStick commented Mar 11, 2025

@sleepyStick sleepyStick marked this pull request as ready for review March 13, 2025 16:23
Copy link
Contributor

@NoahStapp NoahStapp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some small clarity tweaks and async-specific changes, but great start!

@sleepyStick sleepyStick requested a review from NoahStapp March 14, 2025 16:27
@@ -58,10 +59,11 @@
cast,
)

import pymongo.asynchronous.uri_parser
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unneeded import since uri_parser is also imported below.

from bson.codec_options import DEFAULT_CODEC_OPTIONS, CodecOptions, TypeRegistry
from bson.timestamp import Timestamp
from pymongo import _csot, common, helpers_shared, periodic_executor, uri_parser
from pymongo.asynchronous import client_session, database
from pymongo import _csot, common, helpers_shared, periodic_executor, uri_parser_shared
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unneeded import since uri_parser_shared is also imported from below.

else:
from dns import asyncresolver

return await asyncresolver.resolve(*args, **kwargs) # type:ignore[return-value]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need to check if resolver.resolve exists in the async case. If it doesn't, that means the version of dnspython in use is too old for async support and we need to throw an error.

return result


def _validate_uri(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method does no network IO, can it be moved to uri_parser_shared.py to reduce code duplication?

@sleepyStick sleepyStick requested a review from NoahStapp March 14, 2025 18:49
}


if __name__ == "__main__":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part should be moved back to pymongo/uri_parser.py.

if hasattr(self, "_topology"):
return hash(self._topology)
else:
raise InvalidOperation("Cannot hash client until it is connected")
Copy link
Member

@ShaneHarvey ShaneHarvey Mar 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems odd to disallow comparison and hashing without "connecting" first. I wonder if there's a better way to define these without relying on SRV or TXT resolution. Like could we compare some subset of MongoClient arguments?

]
]
else:
options = ["host={self._host}", "port={self._port}"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not safe. self._host is the entire URI so doing this would leak the user credentials.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, i see, what should I use in the repr here instead then?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about this?:

AsyncMongoClient(host='mongodb+srv://fqdn', ...

@sleepyStick sleepyStick requested a review from ShaneHarvey March 19, 2025 22:50
@@ -1911,28 +1911,37 @@ async def test_service_name_from_kwargs(self):
srvServiceName="customname",
connect=False,
)
await client.aconnect()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these open/close calls still needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes because these are srv uris

ttl = rrset.ttl if rrset else 0
return nodes, ttl
__doc__ = original_doc
__all__ = ["maybe_decode", "_SrvResolver"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for checking, let's remove it.

print(exc) # noqa: T201
sys.exit(0)
from pymongo.synchronous.uri_parser import * # noqa: F403
from pymongo.synchronous.uri_parser import __doc__ as original_doc
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By __doc__ I mean a module level docstring like:

"""Tools to parse and validate a MongoDB URI."""

return NotImplemented

def __ne__(self, other: Any) -> bool:
return not self == other

def __hash__(self) -> int:
return hash(self._topology)
if self._topology is None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need to address hashing and equality. It's broken to change the hash of an object since it means that a single client could be added twice to a dictionary or set. The hash (and eq) methods need to use a single deterministic approach:

set = {}
c = AsyncMongoClient('mongodb+srv://...')
set.add(c)
await c.aconnect()
set.add(c)
assert len(set) == 1

This means we need to use the eq_props() approach in all cases.

@sleepyStick sleepyStick requested a review from ShaneHarvey March 21, 2025 21:47
@@ -750,6 +755,9 @@ def __init__(
port = self.PORT
if not isinstance(port, int):
raise TypeError(f"port must be an instance of int, not {type(port)}")
self._host = host
self._port = port
self._topology: Optional[Topology] = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding assert self._topology is not None so many times feels a bit off since it adds runtime overhead for a static typing issue. What if we do this instead?:

        self._topology: Topology = None  # type: ignore[assignment]

self._kill_cursors_executor.join(), # type: ignore[func-returns-value]
return_exceptions=True,
)
if self._topology is not None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than indenting this code (which causes code churn) how about we do this?:

if self._topology is not None:
    return
session_ids = self._topology.pop_all_sessions()
...

)
with self.assertRaisesRegex(ConfigurationError, "Invalid URI host: mongodb is not"):
client = self.simple_client("mongodb+srv://mongodb")
await client.aconnect()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behavior change is important to call out in the changelog and jira ticket.

"%s:%d" % (host, port) if port is not None else host
for host, port in self._topology_settings.seeds
if self._topology is None:
options = self._resolve_srv_info["seeds"]
Copy link
Member

@ShaneHarvey ShaneHarvey Mar 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have tests for this? If not could we add some? I'd like to see the behavior difference in repr().

Copy link
Member

@ShaneHarvey ShaneHarvey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Nice work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants