0
votes

I have breezeJs running in an angular app on mobile device (cordova), which talks to .Net WebApi.

Everything works great, except once in a while the device will get PrimaryKey violations (from my SQL Server).

I think I narrowed it down to only happening when data connection is shakey on the device. The only way I can figure these primary key violations are happening is somehow the server is Saving Changes, but the mobile connection drops out before the response can come back from server that everything saved OK.

What is supposed to happen when BreezeJS doesn't hear back from server after calling SaveChanges? Anyone familiar with BreezeJS know of a way to handle this scenario?

1
This is a challenging problem. Can be very hard to know what actually happened after a timeout. Retry is an option but not as easy as it looks. This isn't just a Breeze issue either; imagine your options without it. I'll give this some more thought. - Ward
@Ward I know this is not specific to Breeze. It would be super awesome if Breeze was able to handle or detect these situations though. (or is that outside scope of what breeze should be doing?) What I would like to know regarding breeze is if I do have a hack similar to Jeremy's, what about my cached breeze data after the retry fails and I handle the PK violation...how do I tell breeze to pretend the SaveChanges was successful? Should I iterate each entity with PK violation and change entityAspect to unchanged? - Rastographics
You can look for the PK error and call acceptChanges if you want to assume the save succeeded. Subsequent saves will fail in systems where the server sets concurrency properties during inserts/updates because the client's concurrency prop values don't match the server's. If none of your props have concurrency mode = fixed OR concurrency mode fixed props are assigned on the client you should be fine. - Jeremy Danyow
Again, beware of store generated keys. Jeremy always uses client-generated keys (e.g, GUIDs). If you're not so lucky, then you've got a real problem here. You won't know what the permanent key is (you'll be locking in temp keys like "-1") and you won't get foreign key fixup. - Ward
Are you in control of your server? I have an idea that might work if you can take over the save behavior of your server. - Ward

1 Answers

1
votes

I've had to handle the same scenario in my project. The approach I took was two part:

  1. Add automatic retries to failed ajax requests. I'm using breeze with jQuery, so I googled "jQuery retry ajax". There's many different implementations, mine is somewhat custom, all center around hijacking the onerror callback as well as the deferred's fail handler to inject retry logic. I'm sure Angular will have similar means of retrying dropped requests.

  2. In the saveChanges fail handler, add logic like this:

...

function isConcurrencyException(reason: any) {
    return reason && reason.message && /Store update, insert, or delete statement affected an unexpected number of rows/.test(reason.message);
}

function isConnectionFailure(reason: any): boolean {
    return reason && reason.hasOwnProperty('status') && reason.status === 0
}

entityManager.saveChanges()
   .then(... yay ...)
   .fail(function(reason) {
       if (isConnectionFailure(reason)) {
           // retry attempts failed to reach server.
           // notify user and save to local storage....
           return;
       }

       if (isConcurrencyException(reason)) {
           // EF is not letting me save the entities again because my previous save (or another user's save) moved the concurrency stamps on the record.  There's also the possibility that a record I'm try to save was deleted by another user.

           // recover... in my case I kept it simple and simply attempt to reload the entity.  If nothing is returned I know the entity was deleted.  Otherwise I now have the latest version.  In either case a message is shown to the user.  
           return; 
       }

       if (reason.entityErrors) {
           // We have an "entityErrors" property... this means the saved failed due to server-side validation errors.
           // do whatever you do to handle validation errors...
           return;           
       }

       // an unexpected exception.  let it bubble up.
       throw reason;    
   })
   .done();  // terminate the promise chain (may not be an equivalent in Angular, not sure).

One of the ways you can test spotty connections is to use Fiddler's AutoResponder tab. Set up a *.drop rule with a regex that matches your breeze route and check the "Enable Automatic Responses" box when you want to simulate dropped requests.

Fiddler AutoResponder


This is a somewhat messy problem to solve- no one size fits all answer, hope this helps.


NOTE

Ward makes a good point in the comments below. This approach is not suitable in situations where the entity's primary key is generated on the server (which would be the case if your db uses identity columns for PKs) because the retry logic could cause duplicate inserts.