ES6 - 有条件地在Promise中发出重试信号,直到达到最大重试次数

时间:2022-01-20 21:26:30

I have seen a number of questions around retrying Promises, however what I'm looking to do is slightly different in that I'd like to manage the retrying/rejecting of promises conditionally until the max retries have been reached.

我已经看到了许多关于重试Promise的问题,但是我想要做的是略有不同,因为我想有条理地管理重试/拒绝承诺,直到达到最大重试次数。

To give a simple example, imagine we wrap a promise around an XMLHttpRequest. When the request loads with a status of...

举一个简单的例子,假设我们围绕XMLHttpRequest包装一个promise。当请求加载状态为...

  • 200: resolve the Promise
  • 200:解决承诺
  • 299: retry immediately
  • 299:立即重试
  • 399: reject immediately
  • 399:立即拒绝
  • 499: fetch something from server, then retry
  • 499:从服务器获取内容,然后重试

Notice that there is scope here for asynchronous behavior to be executed before retries.

请注意,此处有一个范围可以在重试之前执行异步行为。

The solution I have been looking into involves two Promises.

我一直在研究的解决方案涉及两个Promises。

  • The first is a wrapper around each attempt and does a simple resolve/reject based on the result of that attempt.
  • 第一个是围绕每次尝试的包装器,并根据该尝试的结果进行简单的解析/拒绝。
  • The second is a wrapper around the set of attempts, which handles rejections of the individual Promises conditionally.
  • 第二个是围绕这组尝试的包装器,它有条件地处理各个Promise的拒绝。

Bringing this back to the example I mentioned...

把这回到我提到的例子......

  • The first Promise manages each XmlHttpRequest, resolving on status 200 and rejecting otherwise.
  • 第一个Promise管理每个XmlHttpRequest,解析状态200并拒绝否则。
  • The second Promise resolves itself when any of the attempts are resolved. Whenever an attempt is rejected, it decides on the next action (retry, reject, fetch then retry etc.) based on that attempt's status code.
  • 当任何尝试得到解决时,第二个Promise会自行解决。每当尝试被拒绝时,它将根据该尝试的状态代码决定下一个动作(重试,拒绝,获取然后重试等)。

I think I'm going in the right direction with this, but can't seem to get a concrete solution in place. I'm looking to create a generic wrapper for this kind of 'conditionally retrying promise.'

我认为我正朝着正确的方向前进,但似乎无法找到具体的解决方案。我正在寻找为这种“有条件的重试承诺”创建一个通用的包装器。


Edit:

编辑:

Here is a solution in progress:

这是一个正在进行的解决方案:

async function tryAtMost(maxAttempts, asyncCall, handleError)
{
    for (let i = 0; i < maxAttempts; i++)
    {
        try 
        { 
            return await asyncCall(); 
        }
        catch (error)
        {
            const nextAction = await handleError(error); // await some async request (if available) before proceeding
            const actionError = new Error(nextAction.error);

            switch (nextAction.type)
            {
                case ACTIONS.ABORT:
                    throw actionError;
                case ACTIONS.RETRY:
                    if (i === maxAttempts - 1) { throw actionError; }
                    else { continue; }
            }
        }
    }
}

3 个解决方案

#1


2  

There are a few ways to do this, as the other post shows. Personally I find the usage of class unnecessary. I'd approach it using something like

正如另一篇文章所示,有几种方法可以做到这一点。我个人认为不必使用类。我会用类似的东西接近它

async function fetchWithRetries(theURL, remainingRetries = 5) {
  const response = await fetch(theURL);

  switch (response.status) {
    case 200:
      return await response.json(); // or whatever you need
    case 299:
      if (remainingRetries === 0) {
        throw new Error();
      }
      return await fetchWithRetries(theURL, remainingRetries - 1);
    case 399:
      throw new Error();
    case 499:
      if (remainingRetries === 0) {
        throw new Error();
      }

      const otherData = await fetchOtherData();

      return await fetchWithRetries(theURL, remainingRetries - 1);

    default:
      // TODO: You didn't specify other codes?
  }
}

#2


1  

I would simply create a Class that returns an async function (which returns a Promise).

我只想创建一个返回异步函数的类(返回一个Promise)。

  • The Class instance keeps track of the attempts.
  • Class实例跟踪尝试。
  • The async function attempts to fetch something x number of times, equal to the number of maxAttempts.
  • 异步函数尝试获取x次数,等于maxAttempts的数量。
  • If the request responds properly without any errors just return the result.
  • 如果请求正确响应而没有任何错误,则返回结果。
  • Otherwise keep trying until you exhaust the number of maxAttempts.
  • 否则继续尝试,直到耗尽maxAttempts的数量。

An example for Node.js using request-promise-native:

const rp = require('request-promise-native')

class RetryableFetch {
  constructor({ url, maxAttempts = 3 }) {
    this.url = url
    this.maxAttempts = maxAttempts    
    this.attempts = 0

    return this.generateRequest()
  }

  async generateRequest() {
    for (let i = 0; i < this.maxAttempts; i++) {
      try {
        return await rp(this.url)
      } catch(err) {
        switch (err.statusCode) {
          // Add more cases here as you see fit.
          case 399:
            throw err
            break;
          default:
            if (++this.attempts === this.maxAttempts) throw err
        }
      }
    }
  }
}

Usage:

new RetryableFetch({
  url: 'https://www.google.com'
})
.then(result => {
  console.log(result)
})
.catch(err => {
  console.error(err)
})

You can of course substitute rp with Fetch if you want this to work in the browser since both use a Promise-based API.

如果您希望在浏览器中使用rp,则可以使用Fetch替换rp,因为它们都使用基于Promise的API。

#3


1  

Based off your comment:

根据你的评论:

I'm looking to create a generic wrapper for this kind of "conditionally" retrying promise.

我正在寻找为这种“有条件”重试承诺创建一个通用的包装器。

Here's a more generalised wrapper for this:

这是一个更通用的包装器:

  • It allows you to specify the number of max attempts.
  • 它允许您指定最大尝试次数。
  • You pass it your own Promise.
  • 你传递了自己的承诺。
  • You specify, at the construction site, what should happen if the promise rejects and the max attempts have not yet been reached.
  • 您在施工现场指定如果承诺拒绝且尚未达到最大尝试将会发生什么。

// Class Retryable

class Retryable {
  constructor({ promise, maxAttempts = 1, attemptRetry }) {
    this.promise = promise
    this.maxAttempts = maxAttempts
    this.attemptRetry = attemptRetry

    this.attempts = 0
  }

  generateTry() {
    console.info('generating request')

    return this.promise().catch(err => {
      if (++this.attempts === this.maxAttempts) throw err

      return this.attemptRetry(err, () => this.generateTry() , () => {
        throw err
      })
    })
  }
}

// Usage

const retryable = new Retryable({
  maxAttempts: 4,
  promise: () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        reject({ status: 500 })
        // If you `resolve` here instead you will trigger `.then()`
      }, 200)
    })
  },

  attemptRetry: function(err, yes, no) {
    switch (err.status) {
      case 500:
        return yes()
        break;
      default:
        return no()
    }
  }
})

retryable.generateTry().then(result => {
  console.log(result)
}).catch(err => {
  console.error(err)
})

#1


2  

There are a few ways to do this, as the other post shows. Personally I find the usage of class unnecessary. I'd approach it using something like

正如另一篇文章所示,有几种方法可以做到这一点。我个人认为不必使用类。我会用类似的东西接近它

async function fetchWithRetries(theURL, remainingRetries = 5) {
  const response = await fetch(theURL);

  switch (response.status) {
    case 200:
      return await response.json(); // or whatever you need
    case 299:
      if (remainingRetries === 0) {
        throw new Error();
      }
      return await fetchWithRetries(theURL, remainingRetries - 1);
    case 399:
      throw new Error();
    case 499:
      if (remainingRetries === 0) {
        throw new Error();
      }

      const otherData = await fetchOtherData();

      return await fetchWithRetries(theURL, remainingRetries - 1);

    default:
      // TODO: You didn't specify other codes?
  }
}

#2


1  

I would simply create a Class that returns an async function (which returns a Promise).

我只想创建一个返回异步函数的类(返回一个Promise)。

  • The Class instance keeps track of the attempts.
  • Class实例跟踪尝试。
  • The async function attempts to fetch something x number of times, equal to the number of maxAttempts.
  • 异步函数尝试获取x次数,等于maxAttempts的数量。
  • If the request responds properly without any errors just return the result.
  • 如果请求正确响应而没有任何错误,则返回结果。
  • Otherwise keep trying until you exhaust the number of maxAttempts.
  • 否则继续尝试,直到耗尽maxAttempts的数量。

An example for Node.js using request-promise-native:

const rp = require('request-promise-native')

class RetryableFetch {
  constructor({ url, maxAttempts = 3 }) {
    this.url = url
    this.maxAttempts = maxAttempts    
    this.attempts = 0

    return this.generateRequest()
  }

  async generateRequest() {
    for (let i = 0; i < this.maxAttempts; i++) {
      try {
        return await rp(this.url)
      } catch(err) {
        switch (err.statusCode) {
          // Add more cases here as you see fit.
          case 399:
            throw err
            break;
          default:
            if (++this.attempts === this.maxAttempts) throw err
        }
      }
    }
  }
}

Usage:

new RetryableFetch({
  url: 'https://www.google.com'
})
.then(result => {
  console.log(result)
})
.catch(err => {
  console.error(err)
})

You can of course substitute rp with Fetch if you want this to work in the browser since both use a Promise-based API.

如果您希望在浏览器中使用rp,则可以使用Fetch替换rp,因为它们都使用基于Promise的API。

#3


1  

Based off your comment:

根据你的评论:

I'm looking to create a generic wrapper for this kind of "conditionally" retrying promise.

我正在寻找为这种“有条件”重试承诺创建一个通用的包装器。

Here's a more generalised wrapper for this:

这是一个更通用的包装器:

  • It allows you to specify the number of max attempts.
  • 它允许您指定最大尝试次数。
  • You pass it your own Promise.
  • 你传递了自己的承诺。
  • You specify, at the construction site, what should happen if the promise rejects and the max attempts have not yet been reached.
  • 您在施工现场指定如果承诺拒绝且尚未达到最大尝试将会发生什么。

// Class Retryable

class Retryable {
  constructor({ promise, maxAttempts = 1, attemptRetry }) {
    this.promise = promise
    this.maxAttempts = maxAttempts
    this.attemptRetry = attemptRetry

    this.attempts = 0
  }

  generateTry() {
    console.info('generating request')

    return this.promise().catch(err => {
      if (++this.attempts === this.maxAttempts) throw err

      return this.attemptRetry(err, () => this.generateTry() , () => {
        throw err
      })
    })
  }
}

// Usage

const retryable = new Retryable({
  maxAttempts: 4,
  promise: () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        reject({ status: 500 })
        // If you `resolve` here instead you will trigger `.then()`
      }, 200)
    })
  },

  attemptRetry: function(err, yes, no) {
    switch (err.status) {
      case 500:
        return yes()
        break;
      default:
        return no()
    }
  }
})

retryable.generateTry().then(result => {
  console.log(result)
}).catch(err => {
  console.error(err)
})