У 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()
управляє критичними системними помилками, що потребують зупинки ланцюга завдань та детального моніторингу помилок.
Головна відмінність у їхній поведінці:
deleteWhen()
: Повністю видаляє завдання, дозволяючи ланцюгу продовжити виконання.failWhen()
: Позначає завдання як невдало виконане, зупиняє ланцюг, зберігаючи інформацію про помилку для аналізу.Цей підхід забезпечує всебічну обробку помилок з належними треками аудиту. Невдалі завдання залишаються доступними для нових досліджень, а метод failed()
дозволяє налаштувати процедури відновлення у разі невдач, при цьому гарантуючи належну зупинку ланцюгів завдань.
Метод failWhen()
приймає як класи винятків, так і замикання, що надає змогу реалізувати складну логіку для визначення, які винятки повинні призупинити виконання ланцюга завдань, а які підлягають повторним спробам або видаленню.