Repository: celery/celery

Issue:
MongoDB backend does not support mongodb+srv:// URL's
## Checklist

https://github.com/celery/celery/blob/master/celery/backends/mongodb.py#L143-L146

## Steps to reproduce

Set the MongoDB URL to one like:

```mongodb+srv://mongo.private.corp.example.com/celery?ssl=false```

## Expected behavior

This works.

## Actual behavior

This fails because the URL parsing does not match on `mongodb+srv://` instead trying to match `mongodb://` only.


Discussion:
Could you please elaborate on the proposed URL format? Some references would be nice as well.
@georgepsarakis it's documented here: https://docs.mongodb.com/manual/reference/connection-string/#dns-seedlist-connection-format

Resolving patch:
diff --git a/celery/backends/mongodb.py b/celery/backends/mongodb.py
--- a/celery/backends/mongodb.py
+++ b/celery/backends/mongodb.py
@@ -6,7 +6,7 @@
 
 from kombu.exceptions import EncodeError
 from kombu.utils.objects import cached_property
-from kombu.utils.url import maybe_sanitize_url
+from kombu.utils.url import maybe_sanitize_url, urlparse
 
 from celery import states
 from celery.exceptions import ImproperlyConfigured
@@ -75,8 +75,7 @@ def __init__(self, app=None, **kwargs):
 
         # update conf with mongo uri data, only if uri was given
         if self.url:
-            if self.url == 'mongodb://':
-                self.url += 'localhost'
+            self.url = self._ensure_mongodb_uri_compliance(self.url)
 
             uri_data = pymongo.uri_parser.parse_uri(self.url)
             # build the hosts list to create a mongo connection
@@ -120,6 +119,17 @@ def __init__(self, app=None, **kwargs):
             self.options.update(config.pop('options', {}))
             self.options.update(config)
 
+    @staticmethod
+    def _ensure_mongodb_uri_compliance(url):
+        parsed_url = urlparse(url)
+        if not parsed_url.scheme.startswith('mongodb'):
+            url = 'mongodb+{}'.format(url)
+
+        if url == 'mongodb://':
+            url += 'localhost'
+
+        return url
+
     def _prepare_client_options(self):
         if pymongo.version_tuple >= (3,):
             return {'maxPoolSize': self.max_pool_size}


Test patch:
diff --git a/t/unit/backends/test_mongodb.py b/t/unit/backends/test_mongodb.py
--- a/t/unit/backends/test_mongodb.py
+++ b/t/unit/backends/test_mongodb.py
@@ -121,6 +121,62 @@ def test_init_with_settings(self):
 
         mb = MongoBackend(app=self.app, url='mongodb://')
 
+    @patch('dns.resolver.query')
+    def test_init_mongodb_dns_seedlist(self, dns_resolver_query):
+        from dns.rdtypes.IN.SRV import SRV
+        from dns.rdtypes.ANY.TXT import TXT
+        from dns.name import Name
+
+        self.app.conf.mongodb_backend_settings = None
+
+        def mock_resolver(_, record_type):
+            if record_type == 'SRV':
+                return [
+                    SRV(0, 0, 0, 0, 27017, Name(labels=hostname))
+                    for hostname in [
+                        b'mongo1.example.com'.split(b'.'),
+                        b'mongo2.example.com'.split(b'.'),
+                        b'mongo3.example.com'.split(b'.')
+                    ]
+                ]
+            elif record_type == 'TXT':
+                return [TXT(0, 0, [b'replicaSet=rs0'])]
+
+        dns_resolver_query.side_effect = mock_resolver
+
+        # uri with user, password, database name, replica set,
+        # DNS seedlist format
+        uri = ('srv://'
+               'celeryuser:celerypassword@'
+               'dns-seedlist-host.example.com/'
+               'celerydatabase')
+
+        mb = MongoBackend(app=self.app, url=uri)
+        assert mb.mongo_host == [
+            'mongo1.example.com:27017',
+            'mongo2.example.com:27017',
+            'mongo3.example.com:27017',
+        ]
+        assert mb.options == dict(
+            mb._prepare_client_options(),
+            replicaset='rs0',
+            ssl=True
+        )
+        assert mb.user == 'celeryuser'
+        assert mb.password == 'celerypassword'
+        assert mb.database_name == 'celerydatabase'
+
+    def test_ensure_mongodb_uri_compliance(self):
+        mb = MongoBackend(app=self.app, url=None)
+        compliant_uri = mb._ensure_mongodb_uri_compliance
+
+        assert compliant_uri('mongodb://') == 'mongodb://localhost'
+
+        assert compliant_uri('mongodb+something://host') == \
+            'mongodb+something://host'
+
+        assert compliant_uri('something://host') == 'mongodb+something://host'
+
     @pytest.mark.usefixtures('depends_on_current_app')
     def test_reduce(self):
         x = MongoBackend(app=self.app)