Skip to content

Saving model with auto-fetched relations causes stack overflow #279

Closed

Description

@dirkmc

If a model has autoFetch true, and you fetch the model then try to save it, Node reports:

RangeError: Maximum call stack size exceeded
2 Aug 19:22:00 - [nodemon] app crashed - waiting for file changes before starting...

If the auto-fetched field is overwritten it saves ok:

db.Post.get(1337, function(e,p) {
    p.user = null;
    p.channel = null;
    p.template = null;
    p.save({ updated_at: new Date() }, function(err) {
        console.log('done')
    });
});

Activity

  1. dresende commented on Aug 3, 2013

    @dresende
    Owner

    Please tell use node version, orm version.

  2. dirkmc commented on Aug 3, 2013

    @dirkmc
    Author

    This is with the latest version of orm on the master branch

    mac> node --version
    v0.10.12
    
  3. dirkmc commented on Aug 4, 2013

    @dirkmc
    Author

    In Instance.js I added some debugging output so that it looks like this:

    var saveInstance = function (cb, saveOptions) {
        console.log('------------')
        handleValidations(function (err) {
            if (err) {
                return saveError(cb, err);
            }
    
            if (opts.is_new) {
                waitHooks([ "beforeCreate", "beforeSave" ], function (err) {
                    if (err) {
                        return saveError(cb, err);
                    }
    
                    return saveNew(cb, saveOptions, getInstanceData());
                });
            } else {
                waitHooks([ "beforeSave" ], function (err) {
                    if (err) {
                        return saveError(cb, err);
                    }
    
                    console.log(opts.table, opts.data.id)
                    console.log(opts.changes);
    
                    if (opts.changes.length === 0) {
                        console.log('saving associations')
                        return saveAssociations(function (err) {
                            return afterSave(cb, false, err);
                        });
                    }
                        console.log('saving persisted')
    
                    return savePersisted(cb, saveOptions, getInstanceData());
                });
            }
        });
    };

    Is this line correct?

                    if (opts.changes.length === 0) {
    

    This is the console output:

        ...
    ------------
    users 1
    []
    saving associations
    ------------
    posts 1337
    []
    saving associations
    ------------
    users 1
    []
    saving associations
    ------------
    posts 1337
    []
    saving associations
    ------------
    users 1
    []
    saving associations
    ------------
    posts 1337
    []
    saving associations
    ------------
    users 1
    []
    saving associations
    
    RangeError: Maximum call stack size exceeded
    

    In my definitions I have this:

    models.User = db.define("users", {
        // ...
    }, {
        autoFetch: true,
        autoFetchLimit: 1,
    
    models.Post = db.define("posts", {
        // ...
    }, {
        autoFetch: true,
        autoFetchLimit: 1,
    
    // ...
    models.Post.hasOne("user", models.User);
    models.Post.hasOne("channel", models.Channel);
    models.Post.hasOne("template", models.Template);
    
    models.User.hasMany("channels", models.Channel, {
        // ...
    }, {
        reverse: "users", // Other side of many-to-many
        mergeTable: "subscriptions",
        mergeId: "user_id",
        mergeAssocId: "channel_id"
    });
  4. goulwenlorcy commented on Aug 7, 2013

    @goulwenlorcy

    Hello, I have the same problem with a simple code (latest version of orm & node v0.8.9)

                this.models.permissions.get(uid, function (err, item) {
                   item.save(arg, function (err) {     
                         /* ... */
                    });               
                });

    Node crash with:

    RangeError: Maximum call stack size exceeded

    Without autoFetch, it works.

  5. notheotherben commented on Aug 7, 2013

    @notheotherben
    Collaborator

    @goulwenlorcy would you be able to test with v2.0.15 of ORM and tell us whether it has the same issue there? Just trying to narrow down where the issue may have crept in.

  6. goulwenlorcy commented on Aug 7, 2013

    @goulwenlorcy

    With the v2.0.15, I haven't the crash error but it doesn't work and the /* ... */ part is executed 2 times.

  7. dresende commented on Aug 7, 2013

    @dresende
    Owner

    This line (and subsequent scope)..

     if (opts.changes.length === 0) {

    .. was added by me to try to solve the double call to .save(). Not sure how it happened. It makes the workflow continue if there are no changes (avoiding calling UPDATE on the model but continue to check associations).

  8. dresende commented on Aug 7, 2013

    @dresende
    Owner

    @goulwenlorcy please post a complete example. I cannot reproduce your error..

  9. dirkmc commented on Aug 7, 2013

    @dirkmc
    Author

    Here's a complete example:

    app.use(orm.express(Config.db.uri, {
        define: function (db, models) {
            models.User = db.define("users", {
                email: String
            });
    
            models.Post = db.define("posts", {
                context: String
            }, {
                // If I comment out this line it works
                autoFetch: true
            });
    
            models.Post.hasOne("user", models.User);
    
            models.Post.get(1337, function(e,p) {
                // If I uncomment this line it works
                // p.user = null;
                p.save({ updated_at: new Date() }, function(err) {
                    console.log('done')
                });
            });
        }
    }));

    This produces a stack overflow error:

    RangeError: Maximum call stack size exceeded
    
  10. dresende commented on Aug 11, 2013

    @dresende
    Owner

    Please try v2.1.0 and tell us if the same happens.

  11. petrovi4 commented on Aug 12, 2013

    @petrovi4

    I have same bug in v2.1.0

  12. dirkmc commented on Aug 12, 2013

    @dirkmc
    Author

    Hi, I'm afraid I see the same bug also. Diogo are you not able to repro with the code I pasted above?

  13. dxg commented on Aug 12, 2013

    @dxg
    Collaborator

    I've reproduced it, going to add a failing test case in a moment

  14. notheotherben commented on Aug 12, 2013

    @notheotherben
    Collaborator

    Just a heads up - the test runner is currently not running test. I've fixed it in my repo (2e50cf2) which I'm planning on merging as soon as I can get a free minute to make Travis pass all tests. (Having issues with the SQLite driver, but ORM itself is working correctly)

  15. added a commit that references this issue on Aug 12, 2013
  16. 21 remaining items

  17. notheotherben commented on Aug 13, 2013

    @notheotherben
    Collaborator

    Stack Traces

    First Call to Callback

    • setup.validations.stalkId
    • saveInstanceExtra
    • afterSave
    • saveInstance
    • saveAssociation
    • saveInstanceExtra
    • saveInstance START
    • nextHook
    • nextHook
    • export.wait

    Second Call to Callback

    • setup.validations.stalkId
    • saveInstanceExtra
    • afterSave
    • saveInstance
    • saveAssociations
    • saveInstance START
    • nextHook
    • nextHook
    • export.wait
    • nextHook

    Breakdown:

    saveInstance -> saveInstanceExtra -> saveAssociation -> saveInstance -> afterSave -> saveInstanceExtra
    saveInstance -> saveAssociations -> saveInstance -> afterSave -> saveInstanceExtra

  18. notheotherben commented on Aug 13, 2013

    @notheotherben
    Collaborator

    Looks like it may be caused by a race condition in saveAssociations. The assumption is made that all the loops which begin async operations to save associations (if any) will complete before the async operations do. The result is that pending === 0 can hold in as many as 3 places.

    I'm going to try and fix the issue quickly and I'll let you know how it turns out.

  19. notheotherben commented on Aug 13, 2013

    @notheotherben
    Collaborator

    Okay, the issue can be fixed by doing the following:

    Initialize pending = 1 at Instance.js:225
    Test if(--pending === 0) at Instance.js:289

    Rationale
    Treat the pending completion of loops as a pending operation, and only allow us to complete once the loops have finished AND all async operations have completed.

  20. dirkmc commented on Aug 13, 2013

    @dirkmc
    Author

    Works for me

  21. dxg commented on Aug 13, 2013

    @dxg
    Collaborator

    Just made the changes, it appears to call the callback multiple times... Has fixed the infinite loop issue though

    Hahaha yeah that's where I was at yesterday before I went to sleep.
    We need to add a test case for 8f8e2db .

    On a side note, some of that saving code needs a refactor.
    Note the similarity between lib/Instance.js#L121 and /lib/Instance.js#L217.

    It's effectively a duplicate (next is afterSave). I have some WIP changes to fix this, but will wait till all your stuff is merged.

  22. dresende commented on Aug 14, 2013

    @dresende
    Owner

    Yes, the difference is that line 217 is run after the db.update that it's what is being avoided by that (because there's nothing to save).

  23. dirkmc commented on Aug 14, 2013

    @dirkmc
    Author

    While you guys are refactoring that code, would it be possible to take a look at this:
    #278

  24. dresende commented on Aug 14, 2013

    @dresende
    Owner

    Don't worry, we'll look into it before posting a new version 😄

  25. dirkmc commented on Aug 14, 2013

    @dirkmc
    Author

    Great, thanks!

  26. petrovi4 commented on Aug 14, 2013

    @petrovi4

    Stack overflow fixed in current master, but other bug still exists:
    After run this code:

    tour.setTourist(tourists, function(err){
        tour.amount = 1000;
        tour.save();
    });

    I see in console:

    DELETE FROM "tour_tourist" WHERE "tour_id" = 5
    INSERT INTO "tour_tourist" ("tour_id", "tourist_id") VALUES (5, 5) RETURNING *
    INSERT INTO "tour_tourist" ("tour_id", "tourist_id") VALUES (5, 2) RETURNING *
    INSERT INTO "tour_tourist" ("tour_id", "tourist_id") VALUES (5, 3) RETURNING *
    
    DELETE FROM "tour_tourist" WHERE "tour_id" = 5
    INSERT INTO "tour_tourist" ("tour_id", "tourist_id") VALUES (5, 5) RETURNING *
    INSERT INTO "tour_tourist" ("tour_id", "tourist_id") VALUES (5, 3) RETURNING *
    

    So, second save always override changes made with first.

    p.s. With cache enabled after

    setTourist
    

    property tour.tourist still contains previous list of tourists.

  27. SamuelBolduc commented on Aug 14, 2013

    @SamuelBolduc

    So, second save always override changes made with first.

    Looks like I'm having the same issue here #298

  28. dresende commented on Aug 20, 2013

    @dresende
    Owner

    This should be fixed now. About this other issue of multiple INSERT let's discuss it in #298 .

  29. Gromina commented on Sep 16, 2013

    @Gromina

    I still have stack overflow problem on save in 2.1.1:

        @Thing =  db.define 'thing',
          title: {type:'text'}
        ,
          autoFetch: true
        @Model =  db.define 'model',
          filename: {type: 'text'}
        ,
          cache: false
        @Model.hasOne 'thing', @Thing,
          reverse: 'models'
          autoFetch: true

    When I save Thing instance, it gives me stack overflow. What's wrong with my code?

    Upd: I've made a fork with failing test #338

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions