Покращений контроль за завданнями в черзі за допомогою методу failWhen() у ThrottlesExceptions Laravel.

Перекладено ШІ
Оригінал: Laravel News
Оновлено: 02 вересня, 2025
Ви знаєте, як важливо контролювати обробку виключень у ваших проектах на Laravel? Ознайомтеся з новим методом `failWhen()`, який дозволяє вам не лише зупиняти виконання ланцюгів завдань, але й зберігати інформацію про помилки для подальшого аналізу. Читайте далі, щоб дізнатися, як ця функція може змінити вашу стратегію обробки помилок у складних сценаріях!

У middleware Laravel ThrottlesExceptions нещодавно з'явилася корисна можливість — метод failWhen(). Цей додаток забезпечує розробникам точний контроль над поведінкою при невдачі завдань, що особливо важливо для управління ланцюгами виконання завдань.

Раніше обробка винятків у завданнях обмежувалася вибором між продовженням виконання та повним видаленням завдання. Існуючий метод deleteWhen() видаляє завдання з черги, що зручно для певних випадків, але не враховує складні робочі процеси:

public function middleware(): array
{
    return [
        (new ThrottlesExceptions(2, 10 * 60))
            ->deleteWhen(CustomerNotFoundException::class)
    ];
}

Новий метод failWhen() долає це обмеження, дозволяючи відзначити завдання як невдало виконане, зберігаючи інформацію про помилку та зупиняючи виконання ланцюга завдань:

public function middleware(): array
{
    return [
        (new ThrottlesExceptions(2, 10 * 60))
            ->deleteWhen(CustomerNotFoundException::class)
            ->failWhen(fn (\Throwable $e) => $e instanceof SystemCriticalException)
    ];
}

Розглянемо систему управління запасами в електронній комерції, яка обробляє оновлення продуктів через ланцюги завдань. При виникненні критичних виключень потрібно зупинити весь ланцюг, а не продовжувати з потенційно неконсистентними даними:

<?php
namespace App\Jobs;

use App\Exceptions\DatabaseConnectionException;
use App\Exceptions\InventoryLockedException;
use App\Exceptions\ProductDiscontinuedException;
use App\Exceptions\TemporaryNetworkException;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;

class UpdateProductInventory implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $tries = 3;

    public function __construct(
        public Product $product,
        public int $quantity
    ) {}

    public function handle(InventoryService $service): void
    {
        Log::info('Updating product inventory', [
            'product_id' => $this->product->id,
            'quantity' => $this->quantity
        ]);

        try {
            $result = $service->updateStock($this->product, $this->quantity);

            $this->product->update([
                'stock_quantity' => $result->newQuantity,
                'last_updated' => now()
            ]);

        } catch (TemporaryNetworkException $e) {
            Log::warning('Network issue during inventory update', [
                'product_id' => $this->product->id,
                'error' => $e->getMessage()
            ]);
            throw $e;

        } catch (DatabaseConnectionException $e) {
            Log::critical('Database connection failed', [
                'product_id' => $this->product->id,
                'error' => $e->getMessage()
            ]);
            throw $e;

        } catch (InventoryLockedException $e) {
            Log::error('Product inventory locked', [
                'product_id' => $this->product->id,
                'locked_until' => $e->getLockedUntil()
            ]);
            throw $e;
        }
    }

    public function middleware(): array
    {
        return [
            (new ThrottlesExceptions(3, 5 * 60))
                ->deleteWhen(ProductDiscontinuedException::class)
                ->failWhen(function (\Throwable $e) {
                    return $e instanceof DatabaseConnectionException ||
                           $e instanceof InventoryLockedException ||
                           ($e instanceof TemporaryNetworkException && $e->isPermanent());
                })
                ->when(fn (\Throwable $e) => $e instanceof TemporaryNetworkException)
                ->report(function (\Throwable $e) {
                    return $e instanceof DatabaseConnectionException ||
                           $e instanceof InventoryLockedException;
                })
        ];
    }

    public function failed(\Throwable $exception): void
    {
        Log::error('Product inventory update failed permanently', [
            'product_id' => $this->product->id,
            'exception' => $exception->getMessage(),
            'chain_halted' => true
        ]);

        $this->product->update([
            'update_status' => 'failed',
            'last_error' => $exception->getMessage()
        ]);

        NotifyInventoryTeam::dispatch($this->product, $exception);
    }
}

class InventoryController extends Controller
{
    public function updateProductStock(Product $product, UpdateStockRequest $request)
    {
        Bus::chain([
            new UpdateProductInventory($product, $request->quantity),
            new RecalculateProductMetrics($product),
            new UpdateSearchIndex($product),
            new NotifyStockChange($product),
        ])->dispatch();

        return response()->json([
            'message' => 'Inventory update initiated',
            'product_id' => $product->id
        ]);
    }
}

class TemporaryNetworkException extends Exception
{
    public function isPermanent(): bool
    {
        return str_contains($this->getMessage(), 'service permanently unavailable') ||
               str_contains($this->getMessage(), 'endpoint deprecated');
    }
}

class DatabaseConnectionException extends Exception {}
class InventoryLockedException extends Exception
{
    public function getLockedUntil(): Carbon
    {
        return now()->addMinutes(30);
    }
}
class ProductDiscontinuedException extends Exception {}

Ця реалізація ілюструє стратегічне використання обох методів. Метод deleteWhen() обробляє продукти, які слід повністю видалити з процесу, тоді як failWhen() управляє критичними системними помилками, що потребують зупинки ланцюга завдань та детального моніторингу помилок.

Головна відмінність у їхній поведінці:

Цей підхід забезпечує всебічну обробку помилок з належними треками аудиту. Невдалі завдання залишаються доступними для нових досліджень, а метод failed() дозволяє налаштувати процедури відновлення у разі невдач, при цьому гарантуючи належну зупинку ланцюгів завдань.

Метод failWhen() приймає як класи винятків, так і замикання, що надає змогу реалізувати складну логіку для визначення, які винятки повинні призупинити виконання ланцюга завдань, а які підлягають повторним спробам або видаленню.