поскольку очереди Laravel 11 теперь также могут иметь ограничение скорости (https://laravel.com/docs/11.x/queues#rate-limiting). Мое приложение Laravel выполняет несколько запросов к API Shopify для получения новых продуктов, добавления примечаний к некоторым заказам, а также добавления к заказу информации о доставке, такой как номер отслеживания.
У меня есть план Shopify Basic, по которому разрешено делать максимум 2 запроса в секунду, но не более 40 запросов в минуту.
Теперь я хочу написать универсальное задание, которое я смогу использовать для вызовов API Shopify. Итак, либо я получаю продукты, либо обновляю продукты, я хочу иметь один класс заданий, который позаботится об этом, чтобы я мог быть уверен, что не превышаю ограничение скорости 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
для его отладки, но безуспешно.
Я не понимаю, что я сделал не так, настраивая свою работу. Я следил за документацией. Кто-нибудь может дать мне совет, как решить эту проблему?
Кроме того, я хотел бы получать уведомление, если все повторные попытки задания оказались неудачными, а не при каждой повторной попытке.
Кто-нибудь знает, как заархивировать такое поведение?
С уважением
🤔 А знаете ли вы, что...
PHP предоставляет множество инструментов для отладки кода, таких как Xdebug.
Чтобы эффективно обрабатывать ограничения скорости Shopify API с помощью системы очередей Laravel, вам необходимо учесть несколько ключевых моментов в вашей реализации.
Пожалуйста, выполните следующие действия и надеюсь, что проблема решена:
Конфигурация ограничения первой скорости:
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));
}
Я столкнулся с аналогичной проблемой, связанной с ограничением скорости с внешним API.
Можете ли вы удалить метод tries
и добавить метод retryUntil
?
public function retryUntil(): \DateTime
{
return now()->addMinutes(Illuminate\Support\Carbon::MINUTES_PER_HOUR * 2);
}
Вот реализация My Job
<?php
namespace App\Jobs\Report\PrtgReport;
use App\Models\User;
use DateTime;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
class SendExternalRequest implements ShouldQueue
{
use Batchable;
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/**
* The number of seconds the job can run before timing out.
*
* @var int
*/
public $timeout = 120;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(User $user)
{
//
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$response = Http::baseUrl('mybaseurl.com')
->get('test.php', []);
if ($response->successful()) {
//logic Goes here
} else {
$this->release(10);
}
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [
new APIRateLimiterMiddleware(),
(new WithoutOverlapping(User::class.':HIT:'.$this->user->id))
->releaseAfter(10),
];
}
/**
* Determine the time at which the job should timeout.
*/
public function retryUntil(): DateTime
{
return now()->addMinutes(Carbon::MINUTES_PER_HOUR * 2);
}
}
И вот промежуточное программное обеспечение, которое я использую
<?php
namespace App\Jobs\Middleware;
use App\Models\User;
use Closure;
use Illuminate\Support\Facades\Redis;
class APIRateLimiterMiddleware
{
/**
* Process the queued job.
*
* @param \Closure(object): void $next
*/
public function handle(object $job, Closure $next): void
{
Redis::throttle('API'.':Screenshot')
->block(0)
->allow(1)
->every(3) // Allow 1 job every 3 seconds
->then(function () use ($job, $next) {
// Lock obtained...
$next($job);
}, function () use ($job) {
// Could not obtain lock...
$job->release(3); // Release the job back to the queue after 3 seconds
});
}
}
Предоставленный мной код - это всего лишь образец моей реализации, пожалуйста. обязательно приспособьтесь к вашим требованиям.