在 Laravel 6 中,在队列任务中,重试时的指数回退(exponential backoff)的实现

1、在 Laravel 8 中,才原生实现了在队列任务中的指数回退。参考:https://learnku.com/laravel/t/50086

2、参考:http://snags88.github.io/backoff-strategy-for-laravel-jobs 。Backoff Strategy for Laravel Jobs。

3、在现代应用程序中,当出现问题时,会尝试重试导致问题的代码块。例如,如果您在使用 GMail 并且失去了互联网连接,该网站将尝试重新连接到 Google 服务器。您可能会注意到,在第一次尝试时,它会立即尝试重新连接。然后它会等待几秒钟,然后重试。然后等待 10-15 秒,然后重试。然后再等待 30-40 秒,等等。您所经历的称为重新尝试失败任务的退避策略。可以想象,对于排队的作业,采用这种策略会很棒,尤其是对于处理第三方 API 的作业。如果第三方出现故障,那么它可能需要一些时间才能恢复正常,因此采用退避策略是有益的。可悲的是,Laravel 6 的作业中没有内置退避策略。然而,通过它暴露给我们的失败作业处理,我们可以轻松编写自己的可重试作业。我们将实施的特定退避策略称为指数退避策略,因为我们以指数方式增加退避(即 2、4、8、16、32 等)。

4、现有一个 Job,可尝试的最大次数为 3 次。决定在此基础上实现指数回退。现有代码实现

class ThemeAssetUploadJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ThemeAssetGlobPatternTrait;

    /**
     * 任务可以尝试的最大次数。
     *
     * @var int
     */    public $tries = 3;

 /**
     * 任务失败的处理过程
     *
     * @param  Exception  $exception
     * @return void
     */    public function failed(Exception $exception)
    {
        $themeManager = new ThemeManager();
        $themeManager->themeInstallationJobFailed($exception, $this->themeInstallationTask, self::$absolutePath, self::$destination);
    }
}

5、新建 RetriableJob.php

<?php

namespace Modules\ThemeStoreDb\Jobs;

use Exception;
use Illuminate\Foundation\Bus\PendingDispatch;

class RetriableJob
{
    public $tries = 1; // 覆盖默认值并确保我们不会自动重新排队
    public $currentRetryCount = 1;
    public $maxRetries = 3; // 3, 9, 27 seconds
    public $backoffFactor = 3;

    /**
     * 任务失败的处理过程
     *
     * @param Exception $exception
     * @return PendingDispatch|void
     */    public function failed(Exception $exception)
    {
        if ($this->currentRetryCount <= $this->maxRetries) {
            $this->delay(now()->addSeconds($this->backoffFactor ** $this->currentRetryCount));
            $this->currentRetryCount += 1;

            return dispatch($this);
        }
    }
}

6、编辑 ThemeAssetUploadJob.php,继承至 RetriableJob,在 handle() 方法添加日志,以记录最终的执行次数

class ThemeAssetUploadJob extends RetriableJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ThemeAssetGlobPatternTrait;

    /**
     * 任务可以尝试的最大次数。
     *
     * @var int
     */    public $tries = 1;

 /**
     * Execute the job.
     *
     * @return void
     * @throws OverflowException
     */    public function handle()
    {
  Log::info(date("Y-m-d H:i:s"));
        aa();
 }

 /**
     * 任务失败的处理过程
     *
     * @param  Exception  $exception
     * @return void
     */    public function failed(Exception $exception)
    {
        parent::failed($exception);
        $themeManager = new ThemeManager();
        $themeManager->themeInstallationJobFailed($exception, $this->themeInstallationTask, self::$absolutePath, self::$destination);
    }
}

7、查看队列任务执行情况。ThemeAssetUploadJob 在第 1 次失败后,总计又重试了 3 次。间隔时间分别为:3、9、27。符合预期。如图1

图1

PS E:\wwwroot\object> php artisan queue:work
[2023-04-07 11:26:38][9Eq40nHhxmivDgMMdlFWue9CPLVtgXlB] Processing: Modules\ThemeStoreDb\Jobs\ThemeInstallationJob
[2023-04-07 11:27:20][9Eq40nHhxmivDgMMdlFWue9CPLVtgXlB] Processed:  Modules\ThemeStoreDb\Jobs\ThemeInstallationJob
[2023-04-07 11:27:20][QxjbrKqslQ31DHW4feCXGmFGwg2LNU1t] Processing: Modules\ThemeStoreDb\Jobs\ThemeAssetUploadJob
[2023-04-07 11:27:22][QxjbrKqslQ31DHW4feCXGmFGwg2LNU1t] Failed:     Modules\ThemeStoreDb\Jobs\ThemeAssetUploadJob
[2023-04-07 11:27:25][lbE1431gMXnH5W5gXXmfRbI3fnlumTFt] Processing: Modules\ThemeStoreDb\Jobs\ThemeAssetUploadJob
[2023-04-07 11:27:25][lbE1431gMXnH5W5gXXmfRbI3fnlumTFt] Failed:     Modules\ThemeStoreDb\Jobs\ThemeAssetUploadJob
[2023-04-07 11:27:34][WCq0aET7JOPjiHGxtbJEEt5p4BGrEpko] Processing: Modules\ThemeStoreDb\Jobs\ThemeAssetUploadJob
[2023-04-07 11:27:34][WCq0aET7JOPjiHGxtbJEEt5p4BGrEpko] Failed:     Modules\ThemeStoreDb\Jobs\ThemeAssetUploadJob
[2023-04-07 11:28:01][NhYilL7Uus8oBRNgsxFzrB0x1ZKkl40o] Processing: Modules\ThemeStoreDb\Jobs\ThemeAssetUploadJob
[2023-04-07 11:28:01][NhYilL7Uus8oBRNgsxFzrB0x1ZKkl40o] Failed:     Modules\ThemeStoreDb\Jobs\ThemeAssetUploadJob

8、再次查看生成的日志记录,以再次确认。总计执行了 4 次。间隔时间分别为:5、9、27。符合预期。如图2

图2

[2023-04-07 11:27:20] local.INFO: 2023-04-07 11:27:20  
[2023-04-07 11:27:25] local.INFO: 2023-04-07 11:27:25  
[2023-04-07 11:27:34] local.INFO: 2023-04-07 11:27:34
[2023-04-07 11:28:01] local.INFO: 2023-04-07 11:28:01

9、但是,预期是当第 4 次失败后(即真正失败时),才执行 $themeManager->themeInstallationJobFailed() 方法。但是,发现在每次失败后,皆执行了 $themeManager->themeInstallationJobFailed() 方法。编辑 RetriableJob.php 。如图3

图3

<?php

namespace Modules\ThemeStoreDb\Jobs;

use Exception;
use Illuminate\Foundation\Bus\PendingDispatch;

class Retriable
{
    public $tries = 1; // 覆盖默认值并确保我们不会自动重新排队
    public $currentRetryCount = 1;
    public $maxRetries = 3; // 3, 9, 27 seconds
    public $backoffFactor = 3;

    /**
     * 任务失败的处理过程
     *
     * @param Exception $exception
     * @return PendingDispatch|false
     */    public function failed(Exception $exception)
    {
        if ($this->currentRetryCount <= $this->maxRetries) {
            $this->delay(now()->addSeconds($this->backoffFactor ** $this->currentRetryCount));
            $this->currentRetryCount += 1;

            return dispatch($this);
        }

        return false;
    }
}

10、编辑 ThemeAssetUploadJob.php,继承至 RetriableJob。

    /**
     * 任务失败的处理过程
     *
     * @param Exception $exception
     * @return void
     */    public function failed(Exception $exception)
    {
        if (!parent::failed($exception)) {
            $themeManager = new ThemeManager();
            $themeManager->themeInstallationJobFailed($exception, $this->themeInstallationTask);
        }
    }

11、仅当第 4 次失败后(即真正失败时),才执行 $themeManager->themeInstallationJobFailed() 方法。符合预期。如图4

图4

永夜