Backbone-relational icon indicating copy to clipboard operation
Backbone-relational copied to clipboard

Race condition causes "Duplicate id" error

Open philfreo opened this issue 12 years ago • 15 comments

This test cause illustrates a pretty bad race condition bug if you're creating an object, and any GET request comes back (from a collection or from a related parent model) that contains the new object, before the POST request does. The "Cannot instantiate more than one Backbone.RelationalModel with the same id per type" error gets thrown :(

    test('ensure no "duplicate id" race condition on creating objects while polling', function() {
        // 1. create & save a new object (POST request starts)
        // 2. fetch collection of objects (GET response arrives from a request that started either before or after #1)
        // 3. POST response arrives
        // = make sure we don't get a 'duplicate id' error

        var Model = Backbone.RelationalModel.extend({ save: function() {} });
        var Models = Backbone.Collection.extend({ model: Model });
        var models = new Models();

        // step 1
        var model = new Model();
        model.save();

        // step 2
        models.set([ { id: 1 } ]);

        // step 3
        model.set(model.parse({ id: 1 }));
    });

The "step 1-3" portions are just to show what would happen in the real HTTP request scenario.

philfreo avatar May 09 '13 00:05 philfreo

A simpler way of saying it is that there's a race condition that can trigger the duplicate ID error to be thrown, in code as simple as:

users.fetch();
user = new User();
user.save();

philfreo avatar May 16 '13 11:05 philfreo

How would that generate a duplicate id exactly? I'm assuming user.save() will generate a new one?

PaulUithol avatar May 16 '13 12:05 PaulUithol

The problem arises when the POST (creating an object) http response arrives after another response (like a GET request) that already contains the newly created ID.

philfreo avatar May 16 '13 12:05 philfreo

@PaulUithol does this make sense? I'm seeing this error frequently in this real-world situation:

  • background polling of parent model (user.fetch() every 5 seconds, which has nested activities)
  • user creates a new child model (activity = new Activity({ user: user }); activity.save())
  • the error gets thrown if the HTTP responses arrive in the "wrong" order.

philfreo avatar May 20 '13 17:05 philfreo

@PaulUithol if you don't have time to look at this bug, do you have any suggestions on how it should be fixed in BBR?

philfreo avatar May 24 '13 20:05 philfreo

I hope no one minds if I weigh in; this comment is more along the lines of discussion than complaint or solution.

I also have this issue. My specific scenario was saving an object, then another piece of my app asking for that object like:

var model = new Model({name: "foobar"}) model.fetch();

Of course name is not the id, so this turned out to be a design flaw, or at least a incompatible design decision. I had to work around this by eliminating the extra fetch and just making the other part of my app directly aware off the model. app.trigger("hereIsYourNewModel", fetchedModel);

The only thing that would be nice is if I could have used findOrCreate({name: "foobar"}) without making the idAttribute name. While name is unique and identifying in my scenario, it is not the id. I want to be able to set the name the first time call save and get a POST, then have an id attribute populated so the name can be changed, via save() with PUTs.

Of course even if we could more broadly look up models, there would still be the opening for race conditions where the findOrCreate is run while the original save() is still out at the server. In other places I've hedged this with carefully ordered callbacks and a shared reference to the model.

maxwellmstein avatar May 24 '13 23:05 maxwellmstein

@philfreo Just a thought, what if you had your jqxhr defined first that always set it from the save. That way you could avoid calling the activities fetch until the save completed:

var jqxhr;

jqxhr = newActivity.save();

meanwhile:

setTimeout..... $.when(jqxhr).done(function{ activities.fetch() });

maxwellmstein avatar May 24 '13 23:05 maxwellmstein

@maxwellmstein while I don't doubt there's some way to workaround this issue, it's not that simple because there are separate & independent views (and the fetching is happening with a setInterval every few seconds)

philfreo avatar May 24 '13 23:05 philfreo

@philfreo Yeah, the only way I can see around this is to avoid the race condition all together by somehow skipping the 5 second poll if the save is out at the server.

You could try something where your have an event or something that 'turns-off' the timeout, then call save, and in the success of the timeout fire an event to 'turn-on' the poll again.

That or even just pass a whole function of the save work via event or function to the place where the poll happens, that way the polling logic can find a window to run the function while it's not polling.

maxwellmstein avatar May 24 '13 23:05 maxwellmstein

It's harder because the fetch could happen before we even know there's going to be a save(), but you still have a race condition if the fetch response comes back first.

philfreo avatar May 24 '13 23:05 philfreo

After thinking about this one for a while, it seems like a fundamental design issue and there's no real way to fix it since you can't help but have 2 separate instances of the model in the scenario I described.

The best workaround I can think of to avoid the situation would be to keep track of all your AJAX requests and abort any background fetching/polling xhr's of collections (or models with collections) anytime there's a POST (save with isNew=true), and make sure new fetches don't occur until the POST response is back.

philfreo avatar Jul 25 '13 01:07 philfreo

@philfreo did you ever come to a reasonable solution for this? I have a pretty identical situation.

andrewhubbs avatar Dec 27 '13 20:12 andrewhubbs

I implemented what I described above. Basically I just avoid/pause background polling on models/collections when there's a "pending POST" that would create a new nested model that would show up in both requests. Then I unpause polling after the POST completes.

philfreo avatar Dec 27 '13 20:12 philfreo

Thanks Phil. That's more or less what I was afraid of.

andrewhubbs avatar Dec 27 '13 22:12 andrewhubbs