仔细重试 XMLHttpRequest

363 阅读4分钟

很少看到一个web应用程序不使用XMLHttpRequest(或取回,具有类似功能的新API)。 XMLHttpRequest(简称为XHR)非常方便,但它对鼓励健壮和有弹性的编程实践没有太大作用。 在现实世界中运行的任何应用程序有时都会遇到短暂的网络中断和服务器中断。 我们应该通过自动重试请求来优雅地从两者中恢复。

  • 当我们遇到错误时重新请求 XHR。
  • 在重试之前暂停适当的时间。
  • 随机暂停持续时间。

自动重试

我们可能会通过它的error事件或timeout事件发现我们的XHR有一些问题,或者我们可能会注意到onload事件处理函数的状态表明了一个错误。


let request = new XMLHttpRequest()
request.onerror = function (event) {
  openAndSend(request)
}
request.onload = function (event) {
  if (request.status < 200 || request.status >= 300)
    openAndSend(request)
}
request.ontimeout = function (event) {
  openAndSend(request)
}
openAndSend(request);
function openAndSend(request) {
  request.open("GET", "http://example.com/")
  request.send()
}

用这种幼稚的策略,我们可能是在通过制造一个更大的问题来解决一个小问题。 即时重试的泛滥可能会产生超过服务器所能处理的流量,即使服务器状态良好。 这也不尊重客户的资源。 我们可以做得更好。

延迟重试

let request = new XMLHttpRequest()
request.onerror = function (event) {
  retry(request)
}
request.onload = function (event) {
  if (request.status < 200 || request.status >= 300)
    retry(request)
}
request.ontimeout = function (event) {
  retry(request)
}
openAndSend(request)
function retry(request) {
  setTimeout(function () {
    openAndSend(request)
  }, 1000)
}
function openAndSend(request) {
  request.open("GET", "http://example.com/")
  request.send()
}

第一步,使我们的自动重试对服务器更安全,对客户机的资源更容易。 初始请求立即发出,但我们在每次重试之前暂停一秒钟。 一秒钟是快速地进行前几次重试(以便快速恢复并提供良好的体验),但不要过于频繁地重试(以便对客户端和服务器资源放松)之间的折衷。 如果我们知道中断将持续多长时间,我们就可以调整这个延迟。 您可能已经和我一样注意到,短时间的中断通常会很快恢复,而长时间的中断则会持续一段时间。 换句话说:我们必须重试的次数越多,我们处理的长时间中断的可能性就越大。

指数后退

沿着这条思路,我们就会想到“指数后退”,这是一种策略,当我们进行更多尝试时,在每次重试之前以指数方式增加暂停时间。 我们可以用任何底数求指数,但我们用2因为我们是程序员程序员喜欢二进制。 使用二进制指数回退,如果我们在第一次重试之前的暂停是1秒,那么在第二次重试之前的暂停将是2秒,然后是第三次重试之前的4秒,以此类推。 我们还应该设置暂停时间的上限。 如果我们很好地选择了第一次暂停的持续时间和上限,那么我们应该可以从短时间的中断中快速恢复,而不会在较长时间的中断中给客户机或服务器带来过多的压力。

let errorCount = 0
let request = new XMLHttpRequest()
request.onerror = function (event) {
  openAndSend(request, ++errorCount)
}
request.onload = function (event) {
  if (request.status < 200 || request.status >= 300)
    openAndSend(request, ++errorCount)
}
request.ontimeout = function (event) {
  openAndSend(request, ++errorCount)
}
openAndSend(request, errorCount)
function openAndSend(request, errorCount) {
  let pause = errorCount ? Math.pow(2, errorCount - 1) * 1000 : 0
  pause = pause < 600000 ? pause : 600000
  setTimeout(function () {
    request.open("GET", "http://example.com/")
    request.send()
  }, pause)
}

这种截断的二进制指数后退策略的实现。到目前为止,我们构建的代码看起来相当可靠,但是如果许多客户机同时遇到错误,我们仍然可能会遇到麻烦。因为暂停的长度是确定的,所以任何同时遇到错误的客户端都将在同一时间重试。我们的服务器可能会优雅地恢复,但却会被大量的重试击倒。

let request = new XMLHttpRequest()
let errorCount = 0
request.onerror = function (event) {
  openAndSend(request, ++errorCount)
}
request.onload = function (event) {
  if (request.status < 200 || request.status >= 300)
    openAndSend(request, ++errorCount)
}
request.ontimeout = function (event) {
  openAndSend(request, ++errorCount)
}
openAndSend(request, errorCount)
function openAndSend(request, errorCount) {
  let limit = errorCount ? Math.pow(2, errorCount - 1) * 1000 : 0
  limit = limit > 600000 ? 600000 : limit
  var pause = Math.random() * limit
  setTimeout(function () {
    request.open("GET", "http://example.com/")
    request.send()
  }, pause)
}

为重试之前暂停的持续时间添加了一个随机性元素。暂停现在将有一个随机持续时间,对于第一次重试不超过1秒,对于第二次重试不超过2秒,以此类推。平均暂停时间现在是原来的一半,所以我们可能想要调整初始暂停时间和限制。

实现了我们设定的目标,但对于希望发送的每个请求都要输入,这有点冗长。我们看看能不能把它打包成一个方便的函数。

function xhr(url, callback, body = null, opts = {}) {
  // 默认选项
  opts.method = opts.method || "POST"
  opts.startLimit = opts.startLimit || 1000
  opts.maxLimit = opts.maxLimit || 600000
  // 准备请求
  let errorCount = 0
  let request = new XMLHttpRequest()
  request.onerror = function (event) {
    console.debug("xhr: %s error", url, event)
    xhr.do(url, body, opts, request, ++errorCount)
  }
  request.onload = function (event) {
    if (request.status >= 200 && request.status < 300) {
      console.debug("xhr: %s load success", url, event)
      callback(request.responseText)
    } else {
      console.debug("xhr: %s load error", url, event)
      xhr.do(url, body, opts, request, ++errorCount)
    }
  }
  request.ontimeout = function (event) {
    console.debug("xhr: %s timeout", url, event)
    xhr.do(url, body, opts, request, ++errorCount)
  }
  //执行请求
  xhr.do(url, body, opts, request, errorCount)
}
xhr.do = function (url, body, opts, request, errorCount) {
  // 计算发送请求倩暂停的时间
  let limit = errorCount
    ? Math.pow(2, errorCount - 1) * opts.startLimit : 0
  limit = limit < opts.maxLimit ? limit : opts.maxLimit
  let pause = Math.random() * limit
  console.debug("xhr: %s pause %d of %d ms limit, retry %d", url,
    pause, limit, errorCount)
  // 暂停后打开并发送请求
  setTimeout(function () {
    console.debug("xhr: %s send", url)
    request.open(opts.method, url)
    request.send(body)
  }, pause)
}

我发的思想汇集到一个方便的函数中,该函数封装了XMLHttpRequest。函数没有公开XMLHttpRequest的全部功能,只是我通常使用的功能。如果您需要更多的支持—例如设置请求头—在现有结构中添加您需要的内容应该是非常容易的。您可能还希望更改默认选项以适应您的环境。