由于 Laravel 11 队列现在也可以有速率限制(https://laravel.com/docs/11.x/queues#rate-limiting)。我的 Laravel 应用程序正在向 Shopify API 发出一些请求以获取新产品、向某些订单添加注释以及向订单添加发货信息(例如跟踪号码)。
我有一个 Shopify Basic 计划,允许每秒最多发出 2 个请求,但每分钟不得超过 40 个请求。
现在我想编写一个通用作业,我可以利用它来调用 Shopify API。因此,无论是获取产品还是更新产品,我都希望有一个作业类来处理这一问题,这样我就可以确保我没有超过 Shopify API 速率限制。
但是,我无法完成这项工作。我已经在我的
AppServiceProvider.php
中定义了速率限制,如 Laravel 文档中所述:
public function boot(): void
{
// Rate limit Shopify API requests set to 2 per second and 40 per minute
RateLimiter::for('shopify-api-requests', function (object $job) {
return [
Limit::perSecond(2),
Limit::perMinute(40),
];
});
}
这是我的可重用作业类,我想对我向 Shopify API 发出的每个请求重用它:
class ShopifyApiRequestJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $endpoint;
public $method;
public $data;
public function __construct(string $endpoint, string $method = 'GET', array $data = null)
{
$this->endpoint = $endpoint;
$this->method = $method;
$this->data = $data;
}
public function backoff(): array
{
return [1, 5, 10];
}
public function tries(): int
{
return 3;
}
public function middleware(): array
{
return [
new RateLimited('shopify-api-requests'),
//new WithoutOverlapping('shopify-api-requests')
];
}
public function handle()
{
// Construct the full URL
$url = 'https://' . config('settings.SHOPIFY_API_DOMAIN') . '/admin/api/' . config('settings.SHOPIFY_API_VERSION') . '/' . $this->endpoint;
$response = Http::withHeaders([
'X-Shopify-Access-Token' => config('settings.SHOPIFY_API_KEY'),
'Content-Type' => 'application/json',
])->{$this->method}($url, $this->data);
// Handle the response as needed (e.g., log it, store it, etc.)
if ($response->failed()) {
// Handle failure (e.g., retry the job, log the error, etc.)
Log::error("Shopify API Request Failed (" . $response->status() . "): " . $response->body() . " " . $url);
} else {
// Handle success (e.g., process the response, store it, etc.)
Log::info("Shopify API Request Successful");
}
}
}
当我测试我的工作类别时,它的行为不符合预期。我创建了一个
foreach
循环并已调度我的作业 10 次。
我试图存档的预期结果是,分派的每个作业不与同一类的另一个作业 (ShopifyApiRequestJob) 重叠,并且每秒最多仅处理 2 个作业,每分钟最多处理 30 个作业。
但是,我最终得到了这样的日志:
[2024-08-24 19:29:11] local.INFO: Shopify API Request Successful
[2024-08-24 19:29:17] local.INFO: Shopify API Request Successful
[2024-08-24 19:29:20] local.INFO: Shopify API Request Successful
[2024-08-24 19:29:26] local.ERROR: App\Jobs\ShopifyApiRequestJob has been attempted too many times. {"exception":"[object] (Illuminate\\Queue\\MaxAttemptsExceededException(code: 0): [...]
[2024-08-24 19:29:26] local.ERROR: App\Jobs\ShopifyApiRequestJob has been attempted too many times. {"exception":"[object] (Illuminate\\Queue\\MaxAttemptsExceededException(code: 0): [...]
[2024-08-24 19:29:26] local.ERROR: App\Jobs\ShopifyApiRequestJob has been attempted too many times. {"exception":"[object] (Illuminate\\Queue\\MaxAttemptsExceededException(code: 0): [...]
[2024-08-24 19:29:26] local.ERROR: App\Jobs\ShopifyApiRequestJob has been attempted too many times. {"exception":"[object] (Illuminate\\Queue\\MaxAttemptsExceededException(code: 0): [...]
[2024-08-24 19:29:26] local.ERROR: App\Jobs\ShopifyApiRequestJob has been attempted too many times. {"exception":"[object] (Illuminate\\Queue\\MaxAttemptsExceededException(code: 0): [...]
[2024-08-24 19:29:26] local.ERROR: App\Jobs\ShopifyApiRequestJob has been attempted too many times. {"exception":"[object] (Illuminate\\Queue\\MaxAttemptsExceededException(code: 0): [...]
[2024-08-24 19:29:26] local.ERROR: App\Jobs\ShopifyApiRequestJob has been attempted too many times. {"exception":"[object] (Illuminate\\Queue\\MaxAttemptsExceededException(code: 0): [...]
三个作业正在成功处理,但所有其他 7 个作业由于 MaxAttemptsExceededException 而失败。我故意增加了
$backOff
时间来调试它,但没有成功。
我不明白我做错了什么,配置我的工作。我已遵循文档。 有人可以给我建议如何解决这个问题吗?
此外,如果作业的所有重试都失败(而不是每次重试),我希望收到通知。
有人知道如何存档此行为吗?
亲切的问候
要使用 Laravel 的队列系统有效处理 Shopify API 速率限制,您需要解决实施中的几个关键点。
请按照以下步骤操作,希望问题能得到解决:
第一个限速配置:
AppServiceProvider
配置
use Illuminate\Support\Facades\RateLimiter;
public function boot(): void
{
RateLimiter::for('shopify-api-requests', function () {
return [
Limit::perSecond(2)->by('shopify-api'),
Limit::perMinute(40)->by('shopify-api'),
];
});
}
第二个
Job
配置
工作类别
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Redis\Limit;
class ShopifyApiRequestJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $endpoint;
public $method;
public $data;
public function __construct(string $endpoint, string $method = 'GET', array $data = null)
{
$this->endpoint = $endpoint;
$this->method = $method;
$this->data = $data;
}
public function backoff(): array
{
return [2, 10, 20]; // Adjusted backoff times
}
public function tries(): int
{
return 5; // Increase tries to accommodate retries within the rate limits
}
public function middleware(): array
{
return [
new RateLimited('shopify-api-requests'),
];
}
public function handle()
{
// Construct the full URL
$url = 'https://' . config('settings.SHOPIFY_API_DOMAIN') . '/admin/api/' . config('settings.SHOPIFY_API_VERSION') . '/' . $this->endpoint;
$response = Http::withHeaders([
'X-Shopify-Access-Token' => config('settings.SHOPIFY_API_KEY'),
'Content-Type' => 'application/json',
])->{$this->method}($url, $this->data);
// Handle the response
if ($response->failed()) {
// Log failure
Log::error("Shopify API Request Failed (" . $response->status() . "): " . $response->body() . " " . $url);
// If all retries fail, log a separate error
if ($this->attempts() >= $this->tries()) {
Log::critical("All retries for Shopify API Request Failed: " . $url);
}
// Re-throw the exception to allow Laravel to handle retries
throw new \Exception("Shopify API request failed.");
} else {
// Log success
Log::info("Shopify API Request Successful");
}
}
}
第三次处理
Retries
和 Notifications
作业失败处理
use Illuminate\Support\Facades\Notification;
use App\Notifications\JobFailedNotification;
public function failed(\Exception $exception)
{
// Notify the admin when all retries have failed
Notification::route('mail', config('settings.ADMIN_EMAIL'))
->notify(new JobFailedNotification($this->endpoint, $exception));
}