You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I faced a tough bug in my App (Rails + Ember) that happens in an edge case of race condition between regular HTTP requests to the backend and async WebSocket messages updating the records on the store.
Summarized Description
Given the following piece of code:
store.createRecord('item').save();
Ember breaks if a request to create a new record on the backend returns a server-side generated id which has already been used by another (equivalent) record on the store, raising the following errors:
Assertion Failed: 'item' was saved to the server, but the response returned the new id '5cf17edc141edc9828ac0039', which has already been used with another record.
Assertion Failed: You can only unload a record which is not inFlight. <item:null>
Reproduction
Now, I'll describe how it is possible to achieve the above scenario in an App based on WebSocket.
Suppose the App has two models:
Project - with a hasMany relationship with items
Item - with a belongsTo relationship with project
Also, consider that the App has a WebSocket (WS) connection with the backend. Whenever a record is created or updated, the backend asynchronously streams a JSON-API message with the record to the frontend clients through the WS connection. The frontend simply adds/updates the store with the streamed resource through the store's pushPayload method.
Now, consider the following two concurrent flows indicated by sequential, italic numbers (HTTP-based flow) and bold letters (WS flow):
Ember runs let project = await store.createRecord('project').save();
Ember creates a POST request to the backend
Backend stores the new project and generates a new id for the project
Backend returns the resource as JSON-API payload and a 201 HTTP status code to Ember
Ember updates the store with the returned data and id
a. The backend schedules a job to stream WS messages asynchronously with the created project
Ember runs let item = await store.createRecord('item', {project: project}).save();
Ember creates a POST request to the backend
Backend stores the new item with a new id and a relationship with the project
b. The backend's WS worker get the project from the database and streams it as JSON-API to the frontend in response for step a. Since the new item has been saved to the database in step 8, it will be included in the items relationship of the project
c. Ember WS handler receives the message with the updated project resource and updates the store with pushPayload. Consequently, not only the project entry is updated with the relationship to the new item, but also a new 'item' entry is created on the store corresponding to the same record created in step 6 with the server-side generated id.
Backend responds to the request started at step 7 with the item resource in a JSON-API payload and a 201 HTTP status code to Ember
Ember tries to update the store with the returned data and id but it raises the following error:
Assertion Failed: 'item' was saved to the server, but the response returned the new id '5cf17edc141edc9828ac0039', which has already been used with another record.
Moreover, Ember does not change the state of the 'item' object declared in step 2 which remains in the inFlight state forever. Later, if you try to unload the item from the store, you'll get the following error:
Assertion Failed: You can only unload a record which is not inFlight. `<item:null>`
Discussion
Notice that the above-described race condition problem could also happen with slow clients or if the backend streamed the WS events before responding to the HTTP request.
IMHO, for successful create requests, Ember should replace any existing mapped internalModel for the returned ID by a new internalModel with the resource returned by the backend. Or, it should at least change the state of the invalid entry to enable its unloading from the store.
Workaround
I've implemented the following workaround for my test environment since the described race condition happens more often in my acceptance tests. Please, tell me if you have a better idea/workaround.
I overrode the application adapter's createRecord method to remove any previously existing record in the store equivalent to the returned resource in app/adapters/application.js:
createRecord(store,type){letajaxRequest=this._super(...arguments);if(ENV.environment=='test'){ajaxRequest.then((response)=>{letid=response.data.id;// From JSON-API payloadletmap=store._internalModelsFor(type.modelName);letinternalModel=map.get(id);map.remove(internalModel,id);});}returnajaxRequest;},
For applications exhibiting this form of race condition the API needs to reflect back a local identifier. This is achievable today without changes to ember-data but will be easier to resolve in the near future with the implementation of Identifiers.
I faced a tough bug in my App (Rails + Ember) that happens in an edge case of race condition between regular HTTP requests to the backend and async WebSocket messages updating the records on the store.
Summarized Description
Given the following piece of code:
Ember breaks if a request to create a new record on the backend returns a server-side generated id which has already been used by another (equivalent) record on the store, raising the following errors:
<item:null>
Reproduction
Now, I'll describe how it is possible to achieve the above scenario in an App based on WebSocket.
Suppose the App has two models:
Also, consider that the App has a WebSocket (WS) connection with the backend. Whenever a record is created or updated, the backend asynchronously streams a JSON-API message with the record to the frontend clients through the WS connection. The frontend simply adds/updates the store with the streamed resource through the store's pushPayload method.
Now, consider the following two concurrent flows indicated by sequential, italic numbers (HTTP-based flow) and bold letters (WS flow):
let project = await store.createRecord('project').save();
a. The backend schedules a job to stream WS messages asynchronously with the created project
let item = await store.createRecord('item', {project: project}).save();
b. The backend's WS worker get the project from the database and streams it as JSON-API to the frontend in response for step a. Since the new item has been saved to the database in step 8, it will be included in the items relationship of the project
c. Ember WS handler receives the message with the updated project resource and updates the store with pushPayload. Consequently, not only the project entry is updated with the relationship to the new item, but also a new 'item' entry is created on the store corresponding to the same record created in step 6 with the server-side generated id.
Discussion
Notice that the above-described race condition problem could also happen with slow clients or if the backend streamed the WS events before responding to the HTTP request.
IMHO, for successful create requests, Ember should replace any existing mapped internalModel for the returned ID by a new internalModel with the resource returned by the backend. Or, it should at least change the state of the invalid entry to enable its unloading from the store.
Workaround
I've implemented the following workaround for my test environment since the described race condition happens more often in my acceptance tests. Please, tell me if you have a better idea/workaround.
I overrode the application adapter's
createRecord
method to remove any previously existing record in the store equivalent to the returned resource inapp/adapters/application.js
:Related Issues
This issue may be related to #4972 and #4262
Versions
The text was updated successfully, but these errors were encountered: