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

Post-hooks do not work with AsyncLocalStorage #10020

Closed
jonathan-wilkinson opened this issue Mar 12, 2021 · 12 comments
Closed

Post-hooks do not work with AsyncLocalStorage #10020

jonathan-wilkinson opened this issue Mar 12, 2021 · 12 comments
Labels
enhancement This issue is a user-facing general improvement that doesn't fix a bug or add a new feature

Comments

@jonathan-wilkinson
Copy link
Contributor

Issue: Bug
Behaviour:

When running a Mongoose query inside of an AsyncLocalStorage context the correct context is not picked up inside post-type hooks. But it is correctly picked up inside pre-type hooks.

Reproduction

I couldn't load this reproduction into REPL (it didn't like installing mongoose memory server). So I've copied and pasted below.

This script creates two models: Model and TwinModel. After Model has saved (post-save hook) it saves aTwinModel that duplicates its data. Both models use a post-save hook to log the ids of their respective documents into an AsyncLocalStorage context.

The script then runs two functions within that same AsyncLocalStorage context. It then logs the result of that context. The expectation is that each context should have references to the two models (one Model and one TwinModel created within those contexts).

// package.json
{
  "dependencies": {
    "mongodb-memory-server": "^6.9.6",
    "mongoose": "^5.12.0"
  }
}
// index.js
// @ts-check
const mongoose = require("mongoose")
const { MongoMemoryServer} = require("mongodb-memory-server");
const { AsyncLocalStorage } = require("async_hooks");

(async () => {
    /**
     * SETUP CTX
     */
    const CTX = new AsyncLocalStorage();

    /**
     * SETUP DATABASE
     */
    const mongod = new MongoMemoryServer();
    const uri = await mongod.getUri();

    /**
     * CONNECT MONGOOSE
     */
    await mongoose.connect(uri, { useNewUrlParser: true, useUnifiedTopology: true });

    /**
     * CREATE SCHEMAS
     */
    const schema = new mongoose.Schema({ name: String });
    const twinSchema = new mongoose.Schema({ twins: String });

    // Hook that pushes ID of doc to `CTX`
    const pushToCtx = function () {
        const name =this.name || this.twins;
        const type = this.name ? 'Obj' : 'Twin';
        const id = this.id.slice(-4);
        console.log(`Pushing ${type} ${name} ${id} to CTX`);

        const store = CTX.getStore();
        store.ids = (store.ids || []).concat(id);
    };

    schema.post('save', function () {
        return new TwinModel({ twins: this.name }).save();
    });
    schema.post('save', pushToCtx);
    twinSchema.post('save', pushToCtx);
    
    
    const Model = mongoose.model('Model', schema);
    const TwinModel = mongoose.model('TwinModel', twinSchema);

    // TEST

    await CTX.run({ ctxId: 'a'}, async () => {
        const a = new Model({ name: 'a' });
        await a.save();    

        const store = CTX.getStore();
        console.log("A", store);
    })

    await CTX.run({ ctxId: 'b'}, async () => {
        const b = new Model({ name: 'b' });
        await b.save();    

        const store = CTX.getStore();
        console.log("B", store);
    });



    process.exit()
})()

Expected Behaviour

When I run a Mongoose query inside of an AsyncLocalStorage context I can retrieve the correct context from inside any of the downstream function calls including any Mongoose hooks.

Versions

Node: v12.21.0 / v14.8.0
Mongoose: 5.12.0

@IslandRhythms IslandRhythms added can't reproduce Mongoose devs have been unable to reproduce this issue. Close after 14 days of inactivity. and removed can't reproduce Mongoose devs have been unable to reproduce this issue. Close after 14 days of inactivity. labels Mar 16, 2021
@IslandRhythms
Copy link
Collaborator

IslandRhythms commented Mar 16, 2021

const mongoose = require('mongoose');
const { MongoMemoryServer} = require("mongodb-memory-server");
const { AsyncLocalStorage } = require("async_hooks");

(async () => {
    /**
     * SETUP CTX
     */
    const CTX = new AsyncLocalStorage();

    /**
     * SETUP DATABASE
     */
    const mongod = new MongoMemoryServer();
    const uri = await mongod.getUri();

    /**
     * CONNECT MONGOOSE
     */
    await mongoose.connect(uri, { useNewUrlParser: true, useUnifiedTopology: true });

    /**
     * CREATE SCHEMAS
     */
    const schema = new mongoose.Schema({ name: String });
    const twinSchema = new mongoose.Schema({ twins: String });

    // Hook that pushes ID of doc to `CTX`
    const pushToCtx = function () {
        const name =this.name || this.twins;
        const type = this.name ? 'Obj' : 'Twin';
        const id = this.id.slice(-4);
        console.log(`Pushing ${type} ${name} ${id} to CTX`);

        const store = CTX.getStore();
        store.ids = (store.ids || []).concat(id);
    };

    schema.post('save', function () {
        console.log('Hello');
        return new TwinModel({ twins: this.name }).save();
    });
    schema.pre('save', function() {
        console.log('Hi');
    });
    schema.pre('save', pushToCtx);
    twinSchema.pre('save', pushToCtx);
    schema.post('save', pushToCtx);
    twinSchema.post('save', pushToCtx);
    
    
    const Model = mongoose.model('Model', schema);
    const TwinModel = mongoose.model('TwinModel', twinSchema);

    // TEST

    await CTX.run({ ctxId: 'a'}, async () => {
        const a = new Model({ name: 'a' });
        await a.save();    

        const store = CTX.getStore();
        console.log("A", store);
    })

    await CTX.run({ ctxId: 'b'}, async () => {
        const b = new Model({ name: 'b' });
        await b.save();    

        const store = CTX.getStore();
        console.log("B", store);
    });
    await mongoose.connection.dropDatabase();
    process.exit()
})()

Output:
Hi
Pushing Obj a 9868 to CTX
Hello
Pushing Twin a 9869 to CTX
Pushing Twin a 9869 to CTX
Pushing Obj a 9868 to CTX
A { ctxId: 'a', ids: [ '9868', '9869', '9869', '9868' ] }
Hi
Pushing Obj b 986a to CTX
Hello
Pushing Twin b 986b to CTX
Pushing Twin b 986b to CTX
Pushing Obj b 986a to CTX
B { ctxId: 'b', ids: [ '986a' ] }

I am on Node 15.7.0
Please provide what your output with your script is.

@IslandRhythms IslandRhythms added needs clarification This issue doesn't have enough information to be actionable. Close after 14 days of inactivity and removed can't reproduce Mongoose devs have been unable to reproduce this issue. Close after 14 days of inactivity. labels Mar 16, 2021
@jonathan-wilkinson
Copy link
Contributor Author

@IslandRhythms

This is the output of my script:

Pushing Twin a 80a7 to CTX
Pushing Obj a 80a6 to CTX
A { ctxId: 'a', ids: [ '80a7', '80a6' ] }
Pushing Twin b 80a9 to CTX
Pushing Obj b 80a8 to CTX
B { ctxId: 'b' }

The first two objects are pushed into the correct context . But the second two are not.

But if we rerun this with post-hooks changed to pre hooks we get this:

Pushing Twin a 10eb to CTX
Pushing Obj a 10ea to CTX
A { ctxId: 'a', ids: [ '10eb', '10ea' ] }
Pushing Twin b 10ed to CTX
Pushing Obj b 10ec to CTX
B { ctxId: 'b', ids: [ '10ed', '10ec' ] }

With pre-hooks all objects are pushed into the correct context.

@jonathan-wilkinson
Copy link
Contributor Author

jonathan-wilkinson commented Mar 17, 2021

I've been trying to dig into this myself and I think I've found the source of the problem: AsyncLocalStorage gets lost when using callbacks on the mongo-driver.

To test, if you change https://github.com/Automattic/mongoose/blob/master/lib/model.js#L274 to use promises (as below), it works.

    this[modelCollectionSymbol]
      .insertOne(obj, saveOptions)
      .then(ret => {
        callback(null, ret);
      })
      .catch(err => {
        if (err) {
          _setIsNew(_this, true);

          callback(err, null);
        }
      });

It looks like a bug has been raised but rejected as "won't fix" on the MongoDB driver: https://jira.mongodb.org/browse/NODE-3010 .

If there's no objections to making the change, I'm happy to make a PR.

@IslandRhythms
Copy link
Collaborator

@vkarpov15 I will have to defer to you on this.

@IslandRhythms IslandRhythms added has repro script There is a repro script, the Mongoose devs need to confirm that it reproduces the issue and removed needs clarification This issue doesn't have enough information to be actionable. Close after 14 days of inactivity labels Mar 17, 2021
@vkarpov15
Copy link
Collaborator

We'll have to look into how async-local-storage works. We considered converting to using promises under the hood for a similar issue with a different npm module in #7292 , but decided against it since we found an easy fix. We'll see if we can do something similar here.

@vkarpov15 vkarpov15 added this to the 5.12.x milestone Mar 19, 2021
@jonathan-wilkinson
Copy link
Contributor Author

@vkarpov15

I took a look at that linked issue. The same problem happens using the express-http-context library/workaround (it relies on the same async_hooks module under the hood). It just wasn't visible from the tests in that issue because it only uses pre hooks (i.e. should execute before the context is lost by the mongo lib) and only runs the request once.

I can’t imagine that there will be an easy fix here as the context is getting lost in the internals of the mongo lib.

One other option would be to explicitly bind the callback to the correct context. But that seems like the worst of all options.

    const AsyncResource = require("async_hooks”).AsyncResource; // || some shim ;
    const asyncResource = new AsyncResource('save');

    this[modelCollectionSymbol].insertOne(obj, saveOptions, function(err, ret) {
      asyncResource.runInAsyncScope(function() {  
        if (err) {
          _setIsNew(_this, true);

          callback(err, null);
          return;
        }

        callback(null, ret);

      }, this);
    });

wujjpp added a commit to wujjpp/mongoose that referenced this issue May 6, 2021
@vkarpov15 vkarpov15 modified the milestones: 5.12.x, 5.x Unprioritized Jun 30, 2021
@vkarpov15 vkarpov15 added confirmed-bug We've confirmed this is a bug in Mongoose and will fix it. enhancement This issue is a user-facing general improvement that doesn't fix a bug or add a new feature and removed has repro script There is a repro script, the Mongoose devs need to confirm that it reproduces the issue seen confirmed-bug We've confirmed this is a bug in Mongoose and will fix it. labels Jun 30, 2021
@andreialecu
Copy link
Contributor

andreialecu commented Oct 19, 2021

I've been struggling with this today.

Trying to add instrumentation via a plugin, and I need to pass some state to the post hook but it gets lost.

@jonathan-wilkinson have you been able to find any workaround that doesn't involve patching mongoose?

Edit: Ah, just thought of this now, and it solves my problem:

The pre hook works, so I can set something like this._context = storage.getStore(); and then I can read it from this._context in the post hook.

@royalrover
Copy link

the same question, fixed use the method above @andreialecu

@Uzlopak
Copy link
Collaborator

Uzlopak commented Sep 1, 2022

Maybe connected to mongoosejs/kareem#26

@vkarpov15
Copy link
Collaborator

@royalrover have you tried #7292 (comment) ?

@vkarpov15 vkarpov15 modified the milestones: 7.x Unprioritized, 7.4.0, 7.3.1 Jun 12, 2023
@vkarpov15
Copy link
Collaborator

Making a note to try this out again given recent changes in the MongoDB node driver. MongoDB node driver dropped callback support, so this may be fixed

@vkarpov15 vkarpov15 modified the milestones: 7.3.1, 7.3.2 Jun 15, 2023
@vkarpov15
Copy link
Collaborator

Looks like this is fixed. When running the following script against Mongoose 7:

const mongoose = require('mongoose');
const { AsyncLocalStorage } = require("async_hooks");

(async () => {
    /**
     * SETUP CTX
     */
    const CTX = new AsyncLocalStorage();

    /**
     * SETUP DATABASE
     */
    const uri = 'mongodb://127.0.0.1:27017/mongoose_test';

    /**
     * CONNECT MONGOOSE
     */
    await mongoose.connect(uri, { useNewUrlParser: true, useUnifiedTopology: true });

    /**
     * CREATE SCHEMAS
     */
    const schema = new mongoose.Schema({ name: String });
    const twinSchema = new mongoose.Schema({ twins: String });

    // Hook that pushes ID of doc to `CTX`
    const pushToCtx = function () {
        const name =this.name || this.twins;
        const type = this.name ? 'Obj' : 'Twin';
        const id = this.id.slice(-4);
        console.log(`Pushing ${type} ${name} ${id} to CTX`);

        const store = CTX.getStore();
        store.ids = (store.ids || []).concat(id);
    };

    schema.post('save', function () {
        console.log('Hello');
        return new TwinModel({ twins: this.name }).save();
    });
    schema.pre('save', function() {
        console.log('Hi');
    });
    schema.pre('save', pushToCtx);
    twinSchema.pre('save', pushToCtx);
    schema.post('save', pushToCtx);
    twinSchema.post('save', pushToCtx);


    const Model = mongoose.model('Model', schema);
    const TwinModel = mongoose.model('TwinModel', twinSchema);

    // TEST

    await CTX.run({ ctxId: 'a'}, async () => {
        const a = new Model({ name: 'a' });
        await a.save();

        const store = CTX.getStore();
        console.log("A", store);
    })

    await CTX.run({ ctxId: 'b'}, async () => {
        const b = new Model({ name: 'b' });
        await b.save();

        const store = CTX.getStore();
        console.log("B", store);
    });
    await mongoose.connection.dropDatabase();
    process.exit()
})()

I get the following output:

Hi
Pushing Obj a 9390 to CTX
Hello
Pushing Twin a 9392 to CTX
Pushing Twin a 9392 to CTX
Pushing Obj a 9390 to CTX
A { ctxId: 'a', ids: [ '9390', '9392', '9392', '9390' ] }
Hi
Pushing Obj b 9394 to CTX
Hello
Pushing Twin b 9396 to CTX
Pushing Twin b 9396 to CTX
Pushing Obj b 9394 to CTX
B { ctxId: 'b', ids: [ '9394', '9396', '9396', '9394' ] }

Correct ids are showing up on the correct context.

@vkarpov15 vkarpov15 removed this from the 7.3.2 milestone Jul 3, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement This issue is a user-facing general improvement that doesn't fix a bug or add a new feature
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants