没有不值得去解决的问题,也没有不值得去学习的技术!

Implementing Exponential Backoff for Queue Job Retries in Laravel 6

Check the queue job execution status. After the first failure of ThemeAssetUploadJob, it retried 3 more times in total. The intervals were 3, 9, and 27 seconds, respectively. This matches expectations

1. Laravel 8 natively implemented exponential backoff in queue jobs. Reference: https://learnku.com/laravel/t/50086

2. Reference: http://snags88.github.io/backoff-strategy-for-laravel-jobs . Backoff Strategy for Laravel Jobs.

3. In modern applications, when something goes wrong, the application attempts to retry the code block that caused the problem. For example, if you are using Gmail and lose your internet connection, the site will try to reconnect to the Google servers. You might notice that on the first attempt, it tries to reconnect immediately. Then it waits a few seconds and retries. Then it waits 10-15 seconds and retries. Then it waits 30-40 seconds, and so on. What you are experiencing is called a backoff strategy for retrying failed tasks. As you can imagine, adopting this strategy for queued jobs would be great, especially for jobs that handle third-party APIs. If a third party experiences an outage, it might take some time to recover, so employing a backoff strategy is beneficial. Sadly, Laravel 6 jobs do not have a built-in backoff strategy. However, through the failed job handling it exposes to us, we can easily write our own retriable jobs. The specific backoff strategy we will implement is called an exponential backoff strategy, because we increase the backoff exponentially (i.e., 2, 4, 8, 16, 32, etc.).

4. There is an existing Job with a maximum of 3 attempts. I decided to implement exponential backoff on top of this. Existing code implementation:

PHP
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. Create RetriableJob.php

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. Edit ThemeAssetUploadJob.php to inherit from RetriableJob, and add logging in the handle() method to record the final number of executions.

PHP
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. Check the queue job execution status. After the first failure of ThemeAssetUploadJob, it retried 3 more times in total. The intervals were 3, 9, and 27 seconds, respectively. This matches expectations. See Figure 1.

Check the queue job execution status. After the first failure of ThemeAssetUploadJob, it retried 3 more times in total. The intervals were 3, 9, and 27 seconds, respectively. This matches expectations
Figure 1
Plaintext
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. Check the generated log records again for further confirmation. It executed 4 times in total. The intervals were 5, 9, and 27 seconds, respectively. This matches expectations. See Figure 2.

Check the generated log records again for further confirmation. It executed 4 times in total. The intervals were 5, 9, and 27 seconds, respectively. This matches expectations
Figure 2
Plaintext
[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. However, the expectation is that the $themeManager->themeInstallationJobFailed() method should only be executed after the 4th failure (i.e., when it truly fails). But I found that the $themeManager->themeInstallationJobFailed() method was executed after every failure. Edit RetriableJob.php. See Figure 3.

However, the expectation is that the $themeManager->themeInstallationJobFailed() method should only be executed after the 4th failure (i.e., when it truly fails). But it was found that the $themeManager->themeInstallationJobFailed() method was executed after every failure. Edit RetriableJob.php
Figure 3
PHP
<?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. Edit ThemeAssetUploadJob.php to inherit from RetriableJob.

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

11. The $themeManager->themeInstallationJobFailed() method is only executed after the 4th failure (i.e., when it truly fails). This matches expectations. See Figure 4.

Figure 4
Figure 4

关于作者 · 持续学习与技术实践

我是一名拥有 15 年以上开发经验的技术从业者,自 2013 年起持续运营个人技术博客,记录工作与个人项目中遇到的真实问题,以及解决问题过程中的探索、实践和思考。

除了技术写作,我也在持续开发和维护自己的独立项目,探索新技术和新工具在实际场景中的应用。我始终相信,没有不值得去解决的问题,也没有不值得去学习的技术。

如果这篇文章对你有所帮助,欢迎浏览博客中的其他文章,也欢迎交流技术经验与想法。

关于我  ·  GitHub  ·  邮件联系