Backbone-relational
Backbone-relational copied to clipboard
Race condition causes "Duplicate id" error
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.
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();
How would that generate a duplicate id exactly? I'm assuming user.save() will generate a new one?
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.
@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 nestedactivities) - 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.
@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?
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.
@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 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 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.
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.
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 did you ever come to a reasonable solution for this? I have a pretty identical situation.
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.
Thanks Phil. That's more or less what I was afraid of.