Mongoose: [BUG.v5.2.1] ClientSession cannot be serialized to BSON - Sessions

Created on 4 Jul 2018  路  6Comments  路  Source: Automattic/mongoose

Hello; trying to use simple transaction case as described in the documentation; I'm getting an error

ClientSession cannot be serialized to BSON.
    at ClientSession.toBSON (/eliot-local-git/node_modules/mongoose/node_modules/mongodb-core/lib/sessions.js:225:11)
    at serializeInto (/eliot-local-git/node_modules/bson/lib/bson/parser/serializer.js:895:23)
    at serializeObject (/eliot-local-git/node_modules/bson/lib/bson/parser/serializer.js:347:18)
    at serializeInto (/eliot-local-git/node_modules/bson/lib/bson/parser/serializer.js:937:17)
    at BSON.serialize (/eliot-local-git/node_modules/bson/lib/bson/bson.js:63:28)
    at Query.toBin (/eliot-local-git/node_modules/mongoose/node_modules/mongodb-core/lib/connection/commands.js:144:25)
    at serializeCommands (/eliot-local-git/node_modules/mongoose/node_modules/mongodb-core/lib/connection/pool.js:1044:43)
    at Pool.write (/eliot-local-git/node_modules/mongoose/node_modules/mongodb-core/lib/connection/pool.js:1260:3)
    at Cursor._find (/eliot-local-git/node_modules/mongoose/node_modules/mongodb-core/lib/cursor.js:326:22)
    at nextFunction (/eliot-local-git/node_modules/mongoose/node_modules/mongodb-core/lib/cursor.js:673:10)
    at Cursor.next (/eliot-local-git/node_modules/mongoose/node_modules/mongodb-core/lib/cursor.js:824:3)
    at Cursor._next (/eliot-local-git/node_modules/mongoose/node_modules/mongodb/lib/cursor.js:211:36)
    at nextObject (/eliot-local-git/node_modules/mongoose/node_modules/mongodb/lib/operations/cursor_ops.js:186:10)
    at next (/eliot-local-git/node_modules/mongoose/node_modules/mongodb/lib/operations/cursor_ops.js:165:3)
    at executeOperation (/eliot-local-git/node_modules/mongoose/node_modules/mongodb/lib/utils.js:420:24)
    at Cursor.next (/eliot-local-git/node_modules/mongoose/node_modules/mongodb/lib/cursor.js:253:10)

My code look like :

        const session = await model.startSession();

        const oneCompany = await model.findOne({}, {
          projection: '_id',
          session,
        });

        const oneLanguage = await classes.Language.schema.getModel().findOneAndUpdate({
          _id: 'aaaaa0000000000000000000',
        }, {
          'name.description': 'lelz',
        }, {
          new: true,
          projection: '_id',
          session,
        });

        await session.commitTransaction();

        session.endSession();

Versions I use :

node.js v9.11.1
[email protected]
[email protected] (mongo native node driver)
[email protected] (mongo native node driver)
mongodb v4.0.0 (database)

Thanks

docs

Most helpful comment

This is a common gotcha that we should add docs for, but your issue is that the 2nd arg to Model.findOne() is always a projection. You need to do await A.findOne({}, null, { session });

All 6 comments

I haven't used sessions at all yet, so I'm really not sure if this should work or not. I was able to create a complete repro script that demonstrates the outcome based on some assumptions about @GregoryNEUT's code sample.

6663.js

#!/usr/bin/env node --no-deprecation
'use strict';

const { ATLASSRV } = require('/Users/lineus/.env');
const mongoose = require('mongoose');
mongoose.connect(ATLASSRV.replace(/test/, 'gh-6663'));
const conn = mongoose.connection;
const Schema = mongoose.Schema;

const schemaA = new Schema({
  name: String
});

const schemaB = new Schema({
  name: String
});

const A = mongoose.model('A', schemaA);
const B = mongoose.model('B', schemaB);

const a = new A({ name: 'Andrew' });
const b = new B({ name: 'Billy' });

async function run() {
  await conn.dropDatabase();
  await a.save();
  await b.save();

  const session = await A.startSession();
  const foundA = await A.findOne({}, { session });
  await B.findOneAndUpdate({}, { name: foundA.name }, { session });
  await session.commitTransaction();
  session.endSession();

  let newB = await B.findOne({});
  console.log(`hello from ${newB.name}`);
  return conn.close();
}

run().catch(console.error);

Output:

issues: ./6663.js
Error: ClientSession cannot be serialized to BSON.
    at ClientSession.toBSON (/Users/lineus/dev/Help/mongoose5/node_modules/mongodb-core/lib/sessions.js:225:11)
    at serializeInto (/Users/lineus/dev/Help/mongoose5/node_modules/bson/lib/bson/parser/serializer.js:895:23)
    at serializeObject (/Users/lineus/dev/Help/mongoose5/node_modules/bson/lib/bson/parser/serializer.js:347:18)
    at serializeInto (/Users/lineus/dev/Help/mongoose5/node_modules/bson/lib/bson/parser/serializer.js:937:17)
    at BSON.serialize (/Users/lineus/dev/Help/mongoose5/node_modules/bson/lib/bson/bson.js:63:28)
    at Query.toBin (/Users/lineus/dev/Help/mongoose5/node_modules/mongodb-core/lib/connection/commands.js:144:25)
    at serializeCommands (/Users/lineus/dev/Help/mongoose5/node_modules/mongodb-core/lib/connection/pool.js:1044:43)
    at Pool.write (/Users/lineus/dev/Help/mongoose5/node_modules/mongodb-core/lib/connection/pool.js:1260:3)
    at Cursor._find (/Users/lineus/dev/Help/mongoose5/node_modules/mongodb-core/lib/cursor.js:326:22)
    at nextFunction (/Users/lineus/dev/Help/mongoose5/node_modules/mongodb-core/lib/cursor.js:673:10)
    at Cursor.next (/Users/lineus/dev/Help/mongoose5/node_modules/mongodb-core/lib/cursor.js:824:3)
    at Cursor._next (/Users/lineus/dev/Help/mongoose5/node_modules/mongodb/lib/cursor.js:211:36)
    at nextObject (/Users/lineus/dev/Help/mongoose5/node_modules/mongodb/lib/operations/cursor_ops.js:186:10)
    at next (/Users/lineus/dev/Help/mongoose5/node_modules/mongodb/lib/operations/cursor_ops.js:165:3)
    at executeOperation (/Users/lineus/dev/Help/mongoose5/node_modules/mongodb/lib/utils.js:420:24)
    at Cursor.next (/Users/lineus/dev/Help/mongoose5/node_modules/mongodb/lib/cursor.js:253:10)
^C
issues:

something about creating the session on the first model, and passing it to a query on the second model feels wrong

This is a common gotcha that we should add docs for, but your issue is that the 2nd arg to Model.findOne() is always a projection. You need to do await A.findOne({}, null, { session });

@vkarpov15 LOL 馃 I can't believe I missed that.

I had to make a couple of adjustments to my example that were wrong beyond the missing projection. In case it might help someone in the future here is the working code:

6663.js

#!/usr/bin/env node --no-deprecation
'use strict';

const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017,localhost:27018,localhost:27019/gh-6663?replicaSet=rs');
const conn = mongoose.connection;
const Schema = mongoose.Schema;

const schemaA = new Schema({
  name: String
});

const schemaB = new Schema({
  name: String
});

const A = mongoose.model('A', schemaA);
const B = mongoose.model('B', schemaB);

const a = new A({ name: 'Andrew' });
const b = new B({ name: 'Billy' });

async function run() {
  await conn.dropDatabase();
  await a.save();
  await b.save();

  const session = await A.startSession();
  await session.startTransaction();
  const foundA = await A.findOne({}, {}, { session });
  await B.findOneAndUpdate({}, { name: foundA.name }, { session });

  await session.commitTransaction();
  session.endSession();

  let newB = await B.findOne({});
  console.log(`hello from ${newB.name}`);
  return conn.close();
}

run().catch(console.error);

Output:

issues: ./6663.js
hello from Andrew
issues:

It is a real bug in debug environment.

mongoose.set('debug', true);

@vylan that's a very good point. Opened up #6712 to track and will fix asap

const {MongoClient} = require('mongodb');
const uri = 'mongodb://localhost:27017,localhost:27018,localhost:27019/test-mongodb';
MongoClient.connect(uri, {useNewUrlParser: true, replicaSet: 'rs'}).then(async (client) => {
    // console.log(client);
    const db = client.db();
    await db.dropDatabase();

    await db.collection('Account').insertMany([
        {name: 'A', balance: 5},
        {name: 'B', balance: 10}
    ]);

    await transfer('A', 'B', 4); // Success
    try {
        await transfer('A', 'B', 6);
    } catch (error) {
        console.log(error);
    }
    // The actual transfer logic
    async function transfer(from, to, amount) {
        const session = client.startSession();
        session.startTransaction();
        try {
            const opts = {session, returnOriginal: false};
            const B = await db.collection('Account').findOneAndUpdate({name: to}, {$inc: {balance: amount}}, opts).then(res => res.value);
            const A = await db.collection('Account').findOneAndUpdate({name: from}, {$inc: {balance: -amount}}, opts).then(res => res.value);
            if (A.balance < 0) {
                // If A would have negative balance, fail and abort the transaction
                // `session.abortTransaction()` will undo the above `findOneAndUpdate()`
                throw new Error('Insufficient funds: ' + (A.balance + amount));
            }
            await session.commitTransaction();
            session.endSession();
            return {from: A, to: B};
        } catch (error) {
            // If an error occurred, abort the whole transaction and
            // undo any changes that might have happened
            await session.abortTransaction();
            session.endSession();
            throw error; // Rethrow so calling function sees error
        }
    }
}).catch(console.log);


@vkarpov15 your example worked

Was this page helpful?
0 / 5 - 0 ratings