What about multiple requests executed concurrently?

Recently I was writing a Node.js program that downloads resources on a page and gets a list of links to page resources first, such as:

[
  'https://xxx.com/img/logo.jpg',
  'https://xxx.com/img/bg.jpg',
  'https://xxx.com/css/main.css',
  'https://xxx.com/css/animate.css',
  'https://xxx.com/js/jquery.js',
  'https://xxx.com/js/form.js',
  ...
]

Requirements are parallel downloads of resources, notifications after all downloads are complete, and incorrect download links are collected.

If it is a traditional practice to traverse an array to send requests, declare a variable to record the number of requests, whether successful or unsuccessful, and at the end, give the variable + 1 and call a function that determines if the current variable is equal to the length of the array, and equality means that all requests have been completed.

// pseudo code
var count = 0
var errs = []
var data = [...]
function request(url) {
  ajax({url: url})
    .success(function () {
       count++
       callback()
    })
    .fail(function () {
      count++
      errs.push(...)
      callback()
    })
}

function callback() {
  if (count === data.length) {
    console.log('done!')
  }
}

data.forEach(request) 

Since requests are asynchronous, we cannot determine how long each request will take, so we can only process them in callbacks.Now that we have Promise, async-await, which supports synchronous writing, what can we do?

We use setTimeout to simulate requests, data = [500, 400, 300, 200, 100] is both the data returned by each request and the time required for each request.

If it is a secondary request (one after the request ends), it should be printed sequentially, theoretically the total time of all requests equals the sum of time spent per request, approximately 1500ms; if it is a concurrent request (assuming the number of requests is not too large, the limit is not exceeded), the order is from smallest to largest in timeFor printing, the total time for all requests is theoretically equal to the longest time, approximately 500 ms.

First look at how parallel requests and end-of-request determination work

// Simulate Request
function request(param) {
  return new Promise(resolve => {
    setTimeout(() => {
       console.log(param)
       resolve()
    }, param)
  })
}
const items = [500, 400, 300, 200, 100]

_Direct for loop

(() => {
  for (let item of items) {
    request(item)
  }
  console.log('end')
})()
// Output: end, 100, 200, 300, 400, 500

The output above shows that the request is parallel, but it is not possible to determine what ends

_for loop, using async-await

(async () => {
  for (let item of items) {
    await request(item)
  }
  console.log('end')
})()
// Output: 100, 200, 300, 400, 500, end

The code above shows that although the end is determined, the request is secondary

_Use Promise.all

(() => {
  Promise.all(items.map(request)).then(res => {
    console.log('end')
  })
})()
// Output: 100, 200, 300, 400, 500, end

The code above shows that the request is concurrent and that the end is printed after all requests are completed, satisfying the criteria

We can't guarantee that all requests are normal. Next, let's see what to do if something goes wrong, assuming that 200 requests go wrong

function request(param) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (param === 200) {
        // console.log(param, ' failed')
        return reject({
          status: 'error',
          data: param
        })
      }
      // console.log(param, ' success')
      resolve({
        status: 'success',
        data: param
      })
    }, param)
  })
}
const items = [500, 400, 300, 200, 100]

Promise has catch method to catch errors, recently added finally method can be executed at the end

(() => {
  Promise.all(items.map(request))
    .then(res => {
      console.log(res)
    })
    .catch (err => {
      console.log(err)
    })
    .finally(res => {
      console.log('end', res)
    })
})()
// Output {status:'error', data: 200}, end, undefined

The output above shows that if there is an error, it does not go into then, but into catch and then finally, but finally does not accept the parameter and only tells you that it is over.If you remove the console.log(...) comment for the above simulated request, you will also find that finally executes after the catch, and requests after 200 have not yet finished.

Next we revamped the simulation request and catch the error after the request went wrong

function request(param) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (param === 200) {
        // console.log(param, ' failed')
        return reject({
          status: 'error',
          data: param
        })
      }
      // console.log(param, ' success')
      resolve({
        status: 'success',
        data: param
      })
    }, param)
  }).catch(err => err)
}

(() => {
  Promise.all(items.map(request))
    .then(res => {
      console.log(res, 'end')
    })
})()
// Output [{...}, {...}, {...}, {stauts:'error', data: 200}, {...}], end

This gives you all the results in the then, and if you want to use the for loop, you can

(async () => {
  const temp = []
  // This for loop acts like the map above
  for (let item of items) {
    temp.push(request(item))
  }

  const result = []
  for (let t of temp) {
    result.push(await t)
  }
  console.log(result, 'end')
})()
// Output is consistent with above

The first for loop guarantees concurrent requests, saves Promise, and the second loop joins await to guarantee sequential execution.

Okay, that's all. Do you have a better way of writing?

Keywords: Javascript JQuery

Added by atlanta on Sat, 27 Jul 2019 23:04:44 +0300