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

openUri behaviour changed in v6 #10948

Closed
neil-hardlight opened this issue Nov 2, 2021 · 13 comments
Closed

openUri behaviour changed in v6 #10948

neil-hardlight opened this issue Nov 2, 2021 · 13 comments
Milestone

Comments

@neil-hardlight
Copy link

Do you want to request a feature or report a bug?
Bug

What is the current behavior?
"openUri" behaviour changed from mongoose v5 to v6. It used to be able to connect to RSGhost, now it waits 30s then reports an error because it can't find the RSPrimary. This means it is no longer possible to programatically call "rs.initiate()" which our code relies on.

i.e.

    connection.db.admin().command({replSetInitiate: {
            _id: replSetId, members: [{_id: 0, host: `${connection.host}:${connection.port}`}]
        }}, (err) => {

If the current behavior is a bug, please provide the steps to reproduce.
Create an uninitialised replica-set database (i.e. one where rs.initiate has not been called). Try to connect to it via "openUri" - wait 30s to get the following error:

{
  "message": "Server selection timed out after 30000 ms",
  "reason": {
    "type": "ReplicaSetNoPrimary",
    "servers": {"localhost:27023" => ServerDescription {_hostAddress: HostAddress, address: "localhost:27023", type: "RSGhost"},
    "stale": false,
    "compatible": true,
    "heartbeatFrequencyMS": 2500,
    "localThresholdMS": 15,
    "setName": "mainset",
    "commonWireVersion": 9
  }
}

You can see in the error above it is aware of RSGhost.

What is the expected behavior?
Allow connection to RSGhost like mongoose v5 allowed.

What are the versions of Node.js, Mongoose and MongoDB you are using? Note that "latest" is not a version.
Node 16.13.0
Mongoose 6.0.12
MongoDB 4.4.8

@neil-hardlight
Copy link
Author

I've made some progress with this, if I pass the following to "openUri" I get further:

            connection.openUri(address, {
                directConnection: true <-- this used to default to true, now defaults to false
            }, (err) => {

This allows the connection to succeed. However, because the admin command initialises the replica-set (as shown in the original post), the "serverDescriptionChanged" event is fired, and that marks the connection as "disconnected". This can be fixed by updating the following code (in connection.js - line 850):

  if (type === 'Single') {
    client.on('serverDescriptionChanged', ev => {
      const newDescription = ev.newDescription;
      if (newDescription.type === 'Standalone' || newDescription.type === 'RSPrimary') { <-- Deal with RSPrimary
        _handleReconnect();
      } else {
        conn.readyState = STATES.disconnected;
      }
    });
  } else if (type.startsWith('ReplicaSet')) {

OR

Reinstate the following code that was in mongoose v5:

      server.s.topology.on('serverHeartbeatSucceeded', () => {
        _handleReconnect();
      });

It's actually this "serverHeartbeatSucceeded" code that means v5 works for us - because we never get the "newDescription.type" as "Standalone", it always ends up being "RSPrimary". Our database tends to go through the following states: "RSGhost", "RSOther", "RSSecondary", "RSPrimary" - which it settles on and becomes usable. These description changes happen after calling:

connection.db.admin().command({replSetInitiate: {

@IslandRhythms IslandRhythms added the confirmed-bug We've confirmed this is a bug in Mongoose and will fix it. label Nov 8, 2021
@vkarpov15 vkarpov15 added docs This issue is due to a mistake or omission in the mongoosejs.com documentation and removed confirmed-bug We've confirmed this is a bug in Mongoose and will fix it. labels Nov 10, 2021
@vkarpov15
Copy link
Collaborator

We will add a note about this to the docs. The directConnection behavior is correct: Mongoose 6 should throw an error when connecting to a replica set that isn't instantiated.

@vkarpov15 vkarpov15 added this to the 6.0.15 milestone Nov 10, 2021
@neil-hardlight
Copy link
Author

Ok - I'm not sure I understand the response. I'd like to know how to connect to an uninitiated replica-set and programmatically initiate it via an admin command? Once initiated, mongoose connection should report that it is connected, i.e. the "readyState" should be "connected".

Thanks in advance.

@vkarpov15
Copy link
Collaborator

As you suggested:

connection.openUri(address, {
  directConnection: true <-- connect directly to a member of the replica set
});

An alternative approach;

connection.openUri(address, {
  connectWithNoPrimary: true <-- connect even if no primary found
});

@neil-hardlight
Copy link
Author

I get that directConnection works - and solves the first issue I was having around connecting to an uninitiated replicaSet. The follow-up question is more around the bug that mongoose does not correctly set the "readyState" of the connection after telling the relica-set to initiate via the admin command.

connection.db.admin().command({replSetInitiate: {

The above call causes a series of "serverDescriptionChanged" events. Once these have finished firing, the connection to MongoDB is active - but mongoose doesn't think it is (i.e. the "readyState" is still reporting disconnected) - because the "serverDescriptionChanged" callback is not correctly detecting the change from RSGhost to RSPrimary. The fix is in my original post.

@vkarpov15 vkarpov15 removed the docs This issue is due to a mistake or omission in the mongoosejs.com documentation label Nov 24, 2021
@neil-hardlight
Copy link
Author

We use mongoose for a number of high profile games - and unfortunately this is a blocking bug, meaning we can not update from v5 to v6. I'd rather avoid forking the repo to apply the fix.

This is the fix. connection.js - line ~850
if (newDescription.type === 'Standalone' || newDescription.type === 'RSPrimary') { <-- Deal with RSPrimary

If you connect to a replica-set using "directConnection: true" then the server description does not use "Standalone", it uses "RSPrimary" (among other RS identifiers - like RSGhost).

@neil-hardlight
Copy link
Author

Hi - I've created a minimal repo case to highlight the issue.

Before running the code, initialise a new MongoDB database using:

mongod --replSet "main" --dbpath ./Data

Do not initiate the database - that will be done in code.

'use strict';

const mongoose = require('mongoose');

const testSchema = new mongoose.Schema({
    name: String
});

async function test() {
    const connection = mongoose.createConnection();

    connection.on('open', () => {
        console.log('open');
    });

    connection.on('connected', () => {
        console.log('connected');
    });

    connection.on('disconnected', () => {
        console.log('disconnected');
    });

    connection.on('reconnect', () => {
        console.log(`reconnect`);
    });

    connection.on('reconnected', () => {
        console.log(`reconnected`);
    });

    connection.on('error', (err) => {
        console.log(`error: ${err.message}`);
    });

    console.log(`openUri`);
    await connection.openUri('mongodb://localhost:27017/', {directConnection: true, useUnifiedTopology: true, serverSelectionTimeoutMS: 2000});

    connection.client.on('serverDescriptionChanged', (change) => {
        console.log(`serverDescriptionChanged: previous "${change.previousDescription.type}" new "${change.newDescription.type}"`);
    });

    console.log(`replSetInitiate`);
    connection.db.admin().command({replSetInitiate: {
        _id: 'main', members: [{_id: 0, host: `localhost:27017`}]
    }}, (err) => {
        if (err) {
            // Some errors are ok - like "AlreadyInitiated".
            console.error(`replSetInitiate error: ${err.message}`);
        } else {
            console.log('replSetInitiate callback with no error');
        }
        console.log(`connection status: ${connection.readyState}`);

        // Give time for replica-set to initiate. Admin command returns before it's complete.
        setTimeout(() => {
            const TestModel = connection.model('Test', testSchema);
            TestModel.create({name: 'test'}, (err) => {
                if (err) {
                    console.error(`create error: ${err.message}`);
                    return process.exit(1);
                }
                console.log('create success');
                process.exit(0);
            });
        }, 3000);
    });
}

test();

Run the code without my fix, you will see the following output (note, I'm using mongoose v6.0.14):

openUri
connected
open
replSetInitiate
replSetInitiate callback with no error
connection status: 1
disconnected
serverDescriptionChanged: previous "RSGhost" new "RSPrimary"
create error: Operation `tests.insertOne()` buffering timed out after 10000ms

Notice the document "create" failed.

With the following one line fix in connection.js:

if (newDescription.type === 'Standalone' || newDescription.type === 'RSPrimary') { <-- Deal with RSPrimary

You get the following output instead:

openUri
connected
open
replSetInitiate
replSetInitiate callback with no error
connection status: 1
serverDescriptionChanged: previous "RSGhost" new "RSPrimary"
create success

Notice that the document creation is successful.

I hope this helps.

@vkarpov15 vkarpov15 modified the milestones: 6.0.16, 6.0.15 Dec 5, 2021
@vkarpov15
Copy link
Collaborator

Fix should be in v6.0.15, thanks for your patience 👍

@neil-hardlight
Copy link
Author

Thanks for the heads-up. I can confirm that the fix has worked. Thanks!

@MarcusElevait
Copy link

Also had the problem with version 6.0.14 that the connection fails and we had to set the directConnection option to true. Upgrading to 6.0.15 didn't solve it for us. So we continue to set this option.

MongooseModule.forRootAsync({
            imports: [ConfigModule],
            useFactory: async (config: ConfigService) => {
                return {
                    uri: DBUtil.getMongoDBConnectionUrl(config),
                    dbName: config.get('AICOBE_MONGODB_DATABASE'),
                    socketTimeoutMS: 15000,
                };
            },
            inject: [ConfigService],
        })

While uri was "mongodb://user:password@ip:port"

Error log is:

{
"context":"ExceptionHandler",
"level":"error",
"message":"getaddrinfo EAI_AGAIN mongodb_stndln",
"stack":
[
"
MongooseServerSelectionError: getaddrinfo EAI_AGAIN mongodb_stndln    
at NativeConnection.Connection.openUri (/home/marcus/projects/aicosy/node_modules/mongoose/lib/connection.js:797:32)
at Mongoose.createConnection (/home/marcus/projects/aicosy/node_modules/mongoose/lib/index.js:276:10)    
at Function.<anonymous> (/home/marcus/projects/aicosy/node_modules/@nestjs/mongoose/dist/mongoose-core.module.js:82:63)    
at Generator.next (<anonymous>)    
at /home/marcus/projects/aicosy/node_modules/@nestjs/mongoose/dist/mongoose-core.module.js:20:71    
at new Promise (<anonymous>)    
at __awaiter (/home/marcus/projects/aicosy/node_modules/@nestjs/mongoose/dist/mongoose-core.module.js:16:12)    
at /home/marcus/projects/aicosy/node_modules/@nestjs/mongoose/dist/mongoose-core.module.js:81:80    
at Observable._subscribe (/home/marcus/projects/aicosy/node_modules/rxjs/src/internal/observable/defer.ts:53:15)    
at Observable._trySubscribe (/home/marcus/projects/aicosy/node_modules/rxjs/src/internal/Observable.ts:244:19)
"
],
"timestamp":"2021-12-08T10:57:27.467Z"}

@neil-hardlight
Copy link
Author

My understand is: under the hood, "directConnection" used to default to true in mongoose v5, now it defaults to false in mongoose v6. So in my code I now always pass "directConnection: true" to the "openUri" API. The bug I reported was not to do with having to turn "directConnection" on (that to me was an acceptable change as node-modules with major version number changes will always have breaking changes), but more to do with how mongoose handled the connection readyStatus...it did not correctly report a valid connection to MongoDB. This bug is now fixed.

@vkarpov15
Copy link
Collaborator

@MarcusElevait this looks like a separate issue, the getaddrinfo error makes it look like you're running into the replica set hostnames issue

@MarcusElevait
Copy link

@vkarpov15 Thanks I will check that.

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

No branches or pull requests

4 participants