Zatvobor/ember-couchdb-kit

Race condition with multiple updates leads to 409 Conflict

Closed this issue · 4 comments

The following code will lead to 409 Conflict on the second update:

    window.Application = Ember.Application.create();
    Application.ApplicationAdapter = EmberCouchDBKit.DocumentAdapter.extend({
        host: "http://localhost:5984", db: "test"
    });
    Application.ApplicationSerializer = EmberCouchDBKit.DocumentSerializer.extend();
    Application.Document = DS.Model.extend({
        state: DS.attr('string')
    });

    $(function () {
        var store = Application.__container__.lookup('store:main');
        var record = store.createRecord('document', {state: "created"});
        record.save().then(function () {
            console.log("Created: " + record.id);
            setTimeout(function () {
                record.set('state', "update 1");
                record.save().then(function () {
                    console.log("Update 1: " + record.id);
                }).catch(window.alert);
            }, 100);
            setTimeout(function () {
                record.set('state', "update 2");
                record.save().then(function () {
                    console.log("Update 2: " + record.id);
                }).catch(window.alert);
            }, 100);
        });
    })

I've tested this with 1.0.0 and 1.0.3... here's the console output from my Chrome/Mac:

DEBUG: -------------------------------
DEBUG: Ember      : 1.8.1
DEBUG: Ember Data : 1.0.0-beta.15
DEBUG: Handlebars : 1.3.0
DEBUG: jQuery     : 1.11.1
DEBUG: -------------------------------
Created: c367797acff303e8b596f02b31011cf9
Update 1: c367797acff303e8b596f02b31011cf9
PUT http://localhost:5984/test/c367797acff303e8b596f02b31011cf9 409 (Conflict)

I believe this is because both updates are submitted concurrently, and each has the "_rev" from the initial document version, so one update is bound to fail. I'm not sure whether a similar race applies with multiple creates, create/update, or update/delete, but I would suspect so.

It seems like there must be something serializing updates to CouchDB on a document-by-document basis.

FWIW -- this bug has caused me to lose data in production, especially when the CouchDB server is under load, since my application auto-saves as changes are made.

That's sucks. I will check this one asap

Short facts (based on your snippet).

[info] [<0.149.0>] 127.0.0.1 - - POST /test 201
[info] [<0.149.0>] 127.0.0.1 - - PUT /test/64800e0d92bbb5cfd9fd772a7a00120e 201
[info] [<0.157.0>] 127.0.0.1 - - PUT /test/64800e0d92bbb5cfd9fd772a7a00120e 409
curl http://127.0.0.1:5984/test/64800e0d92bbb5cfd9fd772a7a00120e
{"_id":"64800e0d92bbb5cfd9fd772a7a00120e","_rev":"2-28c8c3bb7f2549f8958a826ca42a12a5","state":"update 2"}

As you can see here that state is "_rev":"2-x", "state":"update 2" which means that 'update 2' fulfilled before 'update 1' and it is completely obvious as for me.
The much better way to prevent case like this is based on promises (then) instead of using setTimeout. I will show you:

record.save().then(function () {
    console.log("Created: " + record.id);
    setTimeout(function () {
        record.set('state', "update 1");
        record.save().then(function () {
            console.log("Update 1: " + record.id);
        }).then(function() {
          record.set('state', "update 2");
          record.save().then(function () {
              console.log("Update 2: " + record.id);
          }).catch(window.alert);
        }).catch(window.alert);
    }, 100);

Works well as you expect.

url http://127.0.0.1:5984/test/64800e0d92bbb5cfd9fd772a7a00313c
{"_id":"64800e0d92bbb5cfd9fd772a7a00313c","_rev":"3-fec06efaf08d138935391e7b77c6e5b6","state":"update 2"}

Sure, the situation can be avoided if the application code serializes all calls to each record's save() method, but that seems like it's kicking the serialization concern up to layers that shouldn't have to care about it. I was simply using setTimeout as a simple illustration of the issue.

In fact, my application is hitting this issue when the user clicks two independent buttons in my application more quickly that the CouchDB server is able to successfully return. AFAIK there is no supported way to determine if an Ember data store record is in the process of saving, so to workaround this problem as you have suggested requires me to maintain a global lookup table containing the latest unfulfilled promise for each record, which is something I'd rather not do.

Please consider how you might address this concern within the adapter itself, or please demonstrate how I can avoid this situation while retaining the independent functions.

I'll help you. Firstly, let me know why you have decided to go with CouchDB (seems like a most common issue when a CouchDB is using in the wrong way). Next point, is there any reason to have an UI that enables saving after each click on the same page? If so, you should think about 'disable' all buttons during an Ember data store record is in the process of saving, that is weird, however, could be a right way in long term perspective.