Ajay S
Ajay S

Reputation: 48602

How to handle concurrency issue gracefully

I am working on app where I need to implement offline architecture. My implementation is I am saving the requests whatever user is performing like update order status, update delivery status.

It has to be sequentially like Req1(Shipped)->Req2(Out For Delivery) -> Req3(Delivered) I have a method (uploadRequests ) where I am doing sync with the server. Whenever internet is back, If user refresh the order list by swipe up then I am uploading the offline data then calling order list api. It is like this.

OrderList.js

<List onRefresh=(() => { uploadRequests()) />

offline-request.js

export async function uploadRequests() {
  for (let index = 0; index < sortedRequests.length; index++) {
    const keyName = "REQUEST_" + sortedRequests[index];

    const { consigmentID, userID, type, request, sync } = await load(keyName);

    // Update sync request
    const payload = {
      type: TYPE.UPDATE_LINE_ITEM,
      request: request,
      sync: SYNC.IN_PROCESS,
      userID: userID,
      consigmentID: consigmentID,
    };
    
    await save(keyName, payload);
    // update sync status to SYNC.IN_PROCESS in the storage

    if (sync === SYNC.TO_BE_UPLOAD) {
      // make API call to upload this request to server
    }
    // delete the request from storage once api hit is success
    await remove(keyName);
  }
}

Problem-

Here initially all offline payload sync status is SYNC.TO_BE_UPLOAD, once sync start for this request, then I am updating the status to sync to SYNC.IN_PROCESS one by one after request has been hit to server so that if user swipe multiple times then this request wont be picked up.

So if I saved 30 requests in the storage to sync with server. 1st time if user swipe up and internet is back then suppose it has processed 1st to 5th requests and set the sync status to IN_PROCESS so other thread wont process it and if meanwhile user swipe up list again then this uploadRequests method will call again. Here it will pick up 6th to 30th requests and first thread also it will process 6th to 30th requests so both threads will process 6th to 30th requests twice times.

How I can handle this problem gracefully in javascript without variable ? Main thing all other areas where I am calling API has to wait first to clear this storage.

Problem with variable is Suppose I am on order list page, i swipe up and it start uploading requests, if swipe up then i can ignore this api hit using variable but If i tap on one order and mark it delivered then it will also skip offline requests and make delivered api call but here i want offline requests should clear from storage then make new call when internet is available. This is the reason I don't want using variable like isOfflineRunning=true/false.

Any suggestions to solve this problem ?

Upvotes: 0

Views: 162

Answers (1)

Avinash Thakur
Avinash Thakur

Reputation: 2029

You can wrap your uploadRequests function with this singleCallOnly function like

const singleCallOnly = fn => {
  let lastPromise = null;
  return async (...args) => {
    if(lastPromise) await lastPromise;
    lastPromise = fn(...args);
    return lastPromise;
  }
}
export const uploadRequests = singleCallOnly(_uploadRequests)

You can run the attached snippet for a sample response.


However, on a sidenote, I'm doubtful about the way you're consuming sortedRequests. This way, you should ensure that no element is added/removed from sortedRequests while that function is running.

If sortedRequests is like a queue of pending requests, you should consume it like

while(sortedRequests.length) {
  const key = sortedRequests.shift();
  const keyName = "REQUEST_" + key;
  // ... rest of code
}

// Helper functions start
const sleep = async (ms) => new Promise((res) => setTimeout(() => res(ms), ms));
const simulateLatency = () => sleep(50 + Math.floor(Math.random() * 500));

const SYNC = {
  TO_BE_UPLOAD: "TO_BE_UPLOAD",
  IN_PROCESS: "IN_PROCESS",
};
const TYPE = {
  UPDATE_LINE_ITEM: "UPDATE_LINE_ITEM",
};
const requests = Array(10)
  .fill()
  .map((_, i) => ({
    consigmentID: `c${i}`,
    userID: `u${i}`,
    type: TYPE.UPDATE_LINE_ITEM,
    request: "some-request",
    sync: SYNC.TO_BE_UPLOAD,
    key: `REQUEST_K${i}`,
  }));

const load = async (key) => {
  // simulate latency
  await simulateLatency();
  return requests.find((r) => r.key === key);
};
const save = async (keyname, payload) => {
  await simulateLatency();
  requests.find((r) => r.key === keyname).sync = SYNC.IN_PROCESS;
};
const remove = async (keyname) => {
  await simulateLatency();
  const idx = requests.findIndex((r) => r.key === keyname);
  if (idx < 0) return;
  requests.splice(idx, 1);
};
const sortedRequests = Array(5)
  .fill()
  .map((_, i) => `K${i}`);
// Helper functions end

//  YOUR CODE STARTS
async function _uploadRequests() {
  while(sortedRequests.length) {
    const key = sortedRequests.shift()
    const keyName = "REQUEST_" + key;
    console.log('start', keyName);
    const { consigmentID, userID, type, request, sync } = await load(keyName);
    
    // Update sync request
    const payload = {
      type: TYPE.UPDATE_LINE_ITEM,
      request: request,
      sync: SYNC.IN_PROCESS,
      userID: userID,
      consigmentID: consigmentID,
    };
    
    await save(keyName, payload);
    // update sync status to SYNC.IN_PROCESS in the storage

    if (sync === SYNC.TO_BE_UPLOAD) {
      // make API call to upload this request to server
    }
    // delete the request from storage once api hit is success
    await remove(keyName);
    console.log('done', keyName);
  }
}

const singleCallOnly = fn => {
  let lastPromise = null;
  return async (...args) => {
    if(lastPromise) await lastPromise;
    lastPromise = fn(...args);
    return lastPromise;
  }
}

(async () => {
  const uploadRequests = singleCallOnly(_uploadRequests);
  uploadRequests();
  await sleep(1000);
  uploadRequests();
  await sleep(1000);
  uploadRequests();
})();

Upvotes: 1

Related Questions