0

Problem: I have a list of users to delete. Not only the user himself is deleted, but also a significant amount of data associated with him. Some tables that are involved in deleting user information contain millions of records. All internal processes are started asynchronously when deleted. And if, when deleting one user, there are no problems, and all processes in the background are successful (that is, the client does not expect a response), then if you run several of them, this takes all the server resources, which naturally negatively affects the work of the site as long as it goes removal.

for (let i = 0; i < users.length; i++) {
    const user = users[i];
    await this.removeInstance(user, removedBy);
}

this.removeInstance contains many other asynchronous operations.

What I want: start an asynchronous operation this.removeInstance at 3 minute intervals. The speed of data deletion is not important to me, but according to my observations 1-2 minutes is exactly enough to "completely finish" with one user.

Note: this.removeInstance return removed user, but I cannot use this, since a dozen processes are already running in the background to clear his information.

Thanks for any help!

Roman Nozhenko
  • 698
  • 8
  • 21

4 Answers4

1

Sorry to bother you, but a solution has been found. How do I add a delay in a JavaScript loop?

Final:

public async removeBatchUsers(usersEmailsList: string[], removedBy: IIdentity) {
    const users: UserSchema[] = await User.find({userEmail: {$in: usersEmailsList}});

    const timer = ms => new Promise(res => setTimeout(res, ms));

    async function load (that) {
        for (let i = 0; i < users.length; i++) {
            const user = users[i];
            await that.removeInstance(user, removedBy);
            await timer(10000);
        }
    }

    load(this);
}

The solution above using Promise.allSettled is the most correct and safest. Tested it, in my case I had to additionally install the "promise.allsettled" library (23Kb)

Roman Nozhenko
  • 698
  • 8
  • 21
  • naturally, the timer should be set less, it was just necessary to make sure that they would not be executed at the same time – Roman Nozhenko Mar 02 '21 at 09:08
1

Hi you can fake a sleep function and use it

function sleep(time = 0){
  return new Promise(resolve=>{
    setTimeout(()=>{
      resolve()
    },time)
  })
}

for (let i = 0; i < users.length; i++) {
    const user = users[i];
    await this.removeInstance(user, removedBy);
    await sleep(1000*60*3)
}
Amit Wagner
  • 3,134
  • 3
  • 19
  • 35
  • Thank you, I posted this answer below, which I found at the link. Thank you for your concern and for your time, I note that your answer also works. Have a nice day! – Roman Nozhenko Mar 02 '21 at 09:12
1

You can create a wrapper function that delays an async task completion to a minimum:

const wait = ms =>
  new Promise(resolve => setTimeout(resolve, ms));

const delayCompletion = minTime => task =>
  Promise.allSettled([task(), wait(minTime)])
    .then(([result]) => result.status === "fulfilled"  
      ? result.value 
      : Promise.reject(result.reason)
    );

Example:

const wait = ms =>
  new Promise(resolve => setTimeout(resolve, ms));

const delayCompletion = minTime => task =>
  Promise.allSettled([task(), wait(minTime)])
    .then(([result]) => result.status === "fulfilled"  
      ? result.value 
      : Promise.reject(result.reason)
    );
      

//showcase:

async function main() {
  console.log("-- start loop --");
  for(let i = 0; i < 3; i++) {
    console.log(`start iteration ${i}`);
    
    const delayAtLeastTwoSeconds = delayCompletion(2000);
    const oneSecondtask = () => wait(1000);
    
    await delayAtLeastTwoSeconds(oneSecondtask);
    
    console.log(`finish iteration ${i}`);
  }
  console.log("-- finish loop --");
}

main();

Promise.allSettled() waits until all promises are no longer pending. We give it one asynchronous task an it adds another for a minimum delay, so if task finishes early, then it still has to wait until the delay timer is finished. Conversely, if the delay timer finishes first but task has not, then that will still be awaited.

Promise.allSettled resolves to an array of promises where each has a status "fulfilled" or "rejected". We are only interested in the first item of the array because it's task - the second one is the delay. We just return its value if it was successful or reject with the original rejection reason, if it wasn't. That way delayCompletion still preserves the same semantics as the original promise and any code that consumed the original promise can transparently consume the promise from delayCompletion.

In some respects, this is the opposite of Promise.race() which instead resolves to the promise that resolves first. Instead we wait for the last one but we do have a fixed return value.

With this helper function, your code can be transformed like this:

const safeDelay = delayCompletion(3 * 60 * 1000); //3 minutes

for (let i = 0; i < users.length; i++) {
    const user = users[i];
    await safeDelay(() => this.removeInstance(user, removedBy));
}
VLAZ
  • 26,331
  • 9
  • 49
  • 67
  • Interesting but... UnhandledPromiseRejectionWarning: TypeError: Promise.allSettled is not a function. Node version? – Roman Nozhenko Mar 02 '21 at 09:30
  • @RomanKovalevsky [according to MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled#browser_compatibility), this will work with Node 12.9.0 and above. EDIT: it seems [there is an npm package](https://www.npmjs.com/package/promise.allsettled) that can be used for older environments. – VLAZ Mar 02 '21 at 09:44
  • Thank you. My version "@types/node": "^10.7.1". I think your solution is the best, but unfortunately I cannot apply it. And I'm not ready to upgrade Node yet :) – Roman Nozhenko Mar 02 '21 at 09:47
  • @RomanKovalevsky I've edited my comment. There is a package you can import to use `allSettled` with if `Promise.allSettled` is not available. – VLAZ Mar 02 '21 at 09:48
  • Yes, it does work. As I understand it, I can now set a delay of 1 minute and not worry if suddenly the operation and all its child operations are not completed within the allotted time? – Roman Nozhenko Mar 02 '21 at 10:06
  • @RomanKovalevsky if you set a delay of one minute then, that's the minimum completion time. So, if `this.removeInstance(user, removedBy)` takes 40 seconds, the next iteration will still wait another 20 seconds. If the `this.removeInstance(user, removedBy)` takes 2 minutes, then the next iteration will start immediately after it finishes. – VLAZ Mar 02 '21 at 10:10
  • Thank you so much. Your solution is the most reliable and fully corresponds to the task that was before me. Have a nice day! – Roman Nozhenko Mar 02 '21 at 10:12
0

If you're not too worried about about how long the whole process takes you can use a for..of loop with await to run this.removeInstance in series over the array:

for (let user of users) {
  await this.removeInstance(user)
}

This will wait for each user to be removed before starting the next - you could then return a http 202 to the client to indicate the message is received and is being processed, perhaps also implementing a polling endpoint to check in on the status of the work.

Jack Dunleavy
  • 249
  • 1
  • 7
  • OP *is* worried how long it would take. Or rather, how *short* it would take and wants to impose a minimum delay between iterations. – VLAZ Mar 02 '21 at 09:00
  • This will start a lot of nested asynchronous operations running concurrently, I wrote about this question – Roman Nozhenko Mar 02 '21 at 09:07
  • 1
    Perhaps I misunderstood the question - I took 'The speed of data deletion is not important to me' to mean that the process could take a while provided it didn't eat all of the resources of the server (i.e. kicking off millions of concurrent javascript operations). I think there's a more general point to be made here about architecting your application - If a large amount of background work is harming your webserver's performance you may wish to split that out into a seperate service that can be scaled independently. – Jack Dunleavy Mar 02 '21 at 09:12
  • Yes, in the future I planned so, since all accompanying operations when deleting a user are clearly separated and independent, and therefore their execution can be processed separately. – Roman Nozhenko Mar 02 '21 at 09:14