Search code examples
mongodbpymongodatabase-replication

ServerSelectionTimeoutError on mongo replica set using pymongo


I am trying to connect to a MongoDB replica set using pymongo, but I keep getting the error: pymongo.errors.ServerSelectionTimeoutError: No replica set members match selector. In the error message it's also specified that my topology type is ReplicaSetNoPrimary, which is odd, as connecting with mongo bash shows a clear primary.

Note that the replica set works fine and is usable via mongo bash on the master node. Also, I have added firewall rules to allow both inbound and outbound traffic on the specified ports, just to make sure this isn't the issue. I am using docker-compose for the cluster. The file:

version: "3.9"
services:
  mongo-master:
    image: mongo:latest
    container_name: mongo_master
    volumes:
      - ./data/master:/data/db
    ports:
      - 27017:27017
    command: mongod --replSet dbrs & mongo --eval rs.initiate(`cat rs_config.json`)
    stdin_open: true
    tty: true

  mongo-slave-1:
    image: mongo:latest
    container_name: mongo_slave_1
    volumes:
      - ./data/slave_1:/data/db
    ports:
      - 27018:27017
    command: mongod --replSet dbrs
    stdin_open: true
    tty: true

  mongo-slave-2:
    image: mongo:latest
    container_name: mongo_slave_2
    volumes:
      - ./data/slave_2:/data/db
    ports:
      - 27019:27017
    command: mongod --replSet dbrs
    stdin_open: true
    tty: true

The rs_config.json file used above:

{
    "_id" : "dbrs",
    "members" : [
        {
            "_id" : 0,
            "host" : "mongo_master:27017",
            "priority" : 10
        },
        {
            "_id" : 1,
            "host" : "mongo_slave_1:27017"
        },
        {
            "_id" : 2,
            "host" : "mongo_slave_2:27017"
        }
    ]
}

The error raises on the last line here:

self.__client = MongoClient(["localhost:27017", "localhost:27018", "localhost:27019"], replicaset="dbrs")
self.__collection = self.__client[self.__db_name][collection.value]
self.__collection.insert_one(dictionary_object)

I ommitted some code for brevity, but you can assume all class attributes and dictionary_object are well defined according to pymongo docs. Also please note that I have tried many different ways to initialize MongoClient, including a connection string (as in the docs), and the connect=False optional parameter as advised in some blogs. The issue persists...

Edit: I tried adding "mongo_master" to my etc/hosts file pointing at 127.0.0.1 and changing the connection string from localhost to that, and it works with the replica set. This is a bad workaround but maybe can help in figuring out a solution.

Thanks in advance for any help!


Solution

  • To get a connection to a MongoDB replicaset from an external client, you must be able to resolve the hostnames from the local client.

    https://docs.mongodb.com/manual/tutorial/deploy-replica-set/#connectivity

    Ensure that network traffic can pass securely between all members of the set and all clients in the network.

    So, add the following to your /etc/hosts file:

    127.0.0.1 mongodb-1
    127.0.0.1 mongodb-2
    127.0.0.1 mongodb-3
    

    To be able to connect both internally and externally, you will need to run each MongoDB service on different ports.

    The following script will initiate a 3-node MongoDB replicaset and run a test client. I recommend using the Bitnami image as it takes care of the replset initiation for you. (Borrowing heavily from this configuration)

    #!/bin/bash
    
    PROJECT_NAME=replset_test
    
    MONGODB_VERSION=4.4
    PYTHON_VERSION=3.9.6
    PYMONGO_VERSION=4.0.1
    
    cd "$(mktemp -d)" || exit
    
    cat << EOF > Dockerfile
    FROM python:${PYTHON_VERSION}-slim-buster
    COPY requirements.txt /tmp/
    RUN pip install -r /tmp/requirements.txt
    COPY ${PROJECT_NAME}.py .
    CMD [ "python", "./${PROJECT_NAME}.py" ]
    EOF
    
    cat << EOF > requirements.txt
    pymongo==${PYMONGO_VERSION}
    EOF
    
    cat << EOF > ${PROJECT_NAME}.py
    from pymongo import MongoClient
    
    connection_string = 'mongodb://root:password123@mongodb-1:27017,mongodb-2:27018,mongodb-3:27019/mydatabase?authSource=admin&replicaSet=replicaset'
    client = MongoClient(connection_string)
    db = client.db
    db['mycollection'].insert_one({'a': 1})
    record = db['mycollection'].find_one()
    if record is not None:
        print(f'{__file__}: MongoDB connection working using connection string "{connection_string}"')
    EOF
    
    cp ${PROJECT_NAME}.py ${PROJECT_NAME}_external.py
    
    cat << EOF > docker-compose.yaml
    version: '3.9'
    
    services:
      mongodb-1:
        image: docker.io/bitnami/mongodb:${MONGODB_VERSION}
        ports:
          - 27017:27017
        environment:
          - MONGODB_ADVERTISED_HOSTNAME=mongodb-1
          - MONGODB_PORT_NUMBER=27017
          - MONGODB_REPLICA_SET_MODE=primary
          - MONGODB_ROOT_PASSWORD=password123
          - MONGODB_REPLICA_SET_KEY=replicasetkey123
        volumes:
          - 'mongodb_master_data:/bitnami/mongodb'
    
      mongodb-2:
        image: docker.io/bitnami/mongodb:${MONGODB_VERSION}
        ports:
          - 27018:27018
        depends_on:
          - mongodb-1
        environment:
          - MONGODB_ADVERTISED_HOSTNAME=mongodb-2
          - MONGODB_PORT_NUMBER=27018
          - MONGODB_REPLICA_SET_MODE=secondary
          - MONGODB_INITIAL_PRIMARY_HOST=mongodb-primary
          - MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD=password123
          - MONGODB_REPLICA_SET_KEY=replicasetkey123
    
      mongodb-3:
        image: docker.io/bitnami/mongodb:${MONGODB_VERSION}
        ports:
          - 27019:27019
        depends_on:
          - mongodb-1
        environment:
          - MONGODB_ADVERTISED_HOSTNAME=mongodb-3
          - MONGODB_PORT_NUMBER=27019
          - MONGODB_REPLICA_SET_MODE=secondary
          - MONGODB_INITIAL_PRIMARY_HOST=mongodb-primary
          - MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD=password123
          - MONGODB_REPLICA_SET_KEY=replicasetkey123
    
      ${PROJECT_NAME}:
        container_name: ${PROJECT_NAME}
        build: .
        depends_on:
          - mongodb-1
          - mongodb-2
          - mongodb-3
    
    volumes:
      mongodb_master_data:
        driver: local
    EOF
    
    docker rm --force $(docker ps -a -q --filter name=mongo) 2>&1 > /dev/null
    docker rm --force $(docker ps -a -q --filter name=${PROJECT_NAME}) 2>&1 > /dev/null
    docker-compose up --build -d
    python ${PROJECT_NAME}.py
    docker ps -a -q --filter name=${PROJECT_NAME}
    docker logs $(docker ps -a -q --filter name=${PROJECT_NAME})
    

    If all is ok you will get an output confirming both internal and external connectivity:

    /tmp/tmp.QM9tQPE8Dj/replset_test.py: MongoDB connection working using connection string "mongodb://root:password123@mongodb-1:27017,mongodb-2:27018,mongodb-3:27019/mydatabase?authSource=admin&replicaSet=replicaset"
    d53e8c41ad20
    //./replset_test.py: MongoDB connection working using connection string "mongodb://root:password123@mongodb-1:27017,mongodb-2:27018,mongodb-3:27019/mydatabase?authSource=admin&replicaSet=replicaset"