Skip to content

mongoose adapter trouble with connect #378

@0x0a0d

Description

@0x0a0d

I created a pull #338 that fix a problem in connect() method of adapter mongoose
Today, I've created a new project with moleculer-db-adapter-mongoose and I still got error message when connect to mongodb

problem

The code below can be used to reproduce the problem

const { ServiceBroker } = require('moleculer');
const { Schema } = require('mongoose')
const DbService = require('moleculer-db')
const MongooseDbAdapter = require('moleculer-db-adapter-mongoose')

const broker = new ServiceBroker({
  logger: {
    type: 'Console',
    options: {
      formatter: 'short',
    }
  }
})

broker.createService({
  name: 'foo',
  schema: new Schema({}),
  modelName: 'foo',
  mixins: [DbService],
  adapter: new MongooseDbAdapter("mongodb://127.0.0.1/test"),
})

broker.start().then(() => {
  console.log('Broker started')
  return broker.stop()
}).catch(() => {
  console.log('Broker failed to start')
  console.error(err)
})

Here is screenshot

image

You can see that the foo service was not make connection to db server but success at retry

For more detail, here what happen

  • First because I used schema, so the line below resolve with the conn from mongoose.createConnection
    } else if (this.schema) {
    conn = new Promise(resolve =>{
    const c = mongoose.createConnection(this.uri, this.opts);
    this.model = c.model(this.modelName, this.schema);
    resolve(c);
    });
  • Next, after connect successfully, the promise should be resolved with conn.then(), but instead check conn is connected or not, the code use mongoose.connection that always point to mongoose.connections[0] (default mongoose connection, but with any call of mongoose.createConnection will result to append new connection to mongoose.connections).
  • The connect() was thrown an error and catched by moleculer-db that asked adapter to reconnect.
    setTimeout(() => {
    this.logger.warn("Reconnecting...");
    connecting();
    }, 1000);
  • A stroke of luck has happened here
    At line 83, when adapter got schema field, adapter create a model
    this.model = c.model(this.modelName, this.schema);

    When adapter do connect() again, because model field is set, so it jump to below code that use mongoose.connection to connect
    if (this.model) {
    /* istanbul ignore next */
    if (mongoose.connection.readyState == 1) {
    this.db = mongoose.connection;
    return Promise.resolve();
    } else if (mongoose.connection.readyState == 2) {
    conn = mongoose.connection.asPromise();
    } else {
    conn = mongoose.connect(this.uri, this.opts);
    }
    } else if (this.schema) {

    So at this time, connection was successfully

But for every services use schema will share same mongoose connection

updated code

const { ServiceBroker } = require('moleculer');
const { Schema } = require('mongoose')
const DbService = require('moleculer-db')
const MongooseDbAdapter = require('moleculer-db-adapter-mongoose')

const broker = new ServiceBroker({
  logger: {
    type: 'Console',
    options: {
      formatter: 'short',
    }
  }
})

const foo = broker.createService({
  name: 'foo',
  schema: new Schema({}),
  modelName: 'foo',
  mixins: [DbService],
  adapter: new MongooseDbAdapter("mongodb://127.0.0.1/test"),
})

const bar = broker.createService({
  name: 'bar',
  schema: new Schema({}),
  modelName: 'bar',
  mixins: [DbService],
  adapter: new MongooseDbAdapter("mongodb://127.0.0.1/test"),
})

broker.start().then(() => {
  console.log('Broker started')
  console.log(`Connection of foo and bar is ${foo.adapter.conn === bar.adapter.conn ? 'same' : 'different'}`)
  return broker.stop()
}).catch(() => {
  console.log('Broker failed to start')
  console.error(err)
})

screenshot

image

fix

  • I copied use this.model.db as NativeConnection instance #338 connect and disconnect methods, create a patch file using by patch-package module that fix this problem
    patches.zip
    Unzip this file to root path then run npx patch-package

  • or copy below lines to overwrite connect, disconnect method in moleculer-db-adapter-mongoose/src/index.js

    /**
    * Connect to database
    *
    * @returns {Promise}
    *
    * @memberof MongooseDbAdapter
    */
    connect() {
    return new Promise((resolve) => {
    if (this.model) {
    // model field exists in service schema, should check if model has been connected
    if (this.model.db) {
    // if this.model.db is existed, adapter had connected before, just return this.model.db
    // Model.prototype.db
    // Connection the model uses.
    // https://mongoosejs.com/docs/api/model.html#model_Model-db
    resolve(this.model.db);
    return;
    }
    /* istanbul ignore next */
    if (
    mongoose.connection.readyState === mongoose.connection.states.connected ||
    mongoose.connection.readyState === mongoose.connection.states.connecting
    ) {
    // if mongoose is connecting, will handle below
    resolve(mongoose.connection);
    } else {
    // everything else cases mean we not yet do connect before, make it
    const conn = mongoose.createConnection(this.uri, this.opts);
    resolve(conn);
    }
    } else if (this.schema) {
    const conn = mongoose.createConnection(this.uri, this.opts);
    this.model = conn.model(this.modelName, this.schema);
    resolve(conn);
    }
    }).then(conn => {
    this.conn = conn;
    if (this.conn.db != null) {
    return this.conn.db;
    } else if (this.conn.readyState === mongoose.connection.states.connecting) {
    return new Promise((resolve, reject) => {
    this.conn.once("error", reject);
    this.conn.once("connected", () => {
    this.conn.removeListener("error", reject);
    resolve(this.conn.db);
    });
    });
    }
    throw new MoleculerError("MongoDB connection failed to get DB object");
    }).then(db => {
    this.db = db;
    this.service.logger.info("MongoDB adapter has connected successfully.");
    // do not use this.db.on() because of next line
    // DeprecationWarning: Listening to events on the Db class has been deprecated and will be removed in the next major version.
    /* istanbul ignore next */
    this.conn.on("disconnected", () => this.service.logger.warn("Mongoose adapter has disconnected."));
    this.conn.on("error", err => this.service.logger.error("MongoDB error.", err));
    this.conn.on("reconnect", () => this.service.logger.info("Mongoose adapter has reconnected."));
    });
    }
    /**
    * Disconnect from database
    *
    * @returns {Promise}
    *
    * @memberof MongooseDbAdapter
    */
    disconnect() {
    return new Promise(resolve => {
    if (this.conn == null) {
    // model was created and mongoose maybe connected before call .connect()
    mongoose.connection.close(resolve);
    } else if (this.conn.readyState === mongoose.connection.states.connecting) {
    // disconnect() was called before connect() success
    // try to disconnect at nextTick
    setTimeout(() => resolve(this.disconnect()), 0);
    } else if (this.conn.close) {
    this.conn.close(resolve);
    } else if (this.db != null && this.db.close) {
    this.db.close(resolve);
    } else {
    // for unknown cases
    mongoose.connection.close(resolve);
    }
    });
    }

need support

@icebob if you think this fix is correct, can you (or someone else) can help me to write tests? I not good to write test and dont have enough time at this moment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions