미들웨어
미들웨어는 작업 실행을 양파형 파이프라인으로 감쌉니다. 각 미들웨어는 작업 전후에 로직을 실행하거나 실행을 조기에 종료하거나 예외를 처리할 수 있습니다. Queuety에는 기본 미들웨어 5개가 포함되어 있으며 Contracts\Middleware 인터페이스를 통해 사용자 정의 미들웨어를 지원합니다.
미들웨어는 디스패치 가능한 작업 클래스에 사용할 수 있습니다(Contracts\Job를 구현하고 Dispatchable 트레이트를 사용하는 클래스).
Contracts\Middleware 인터페이스
use Queuety\Contracts\Middleware;
interface Middleware {
public function handle( object $job, \Closure $next ): void;
}
$job 매개변수는 처리 중인 작업 인스턴스입니다. $next( $job )을 호출해 파이프라인의 다음 미들웨어로 실행을 넘기세요(마지막 미들웨어인 경우 작업의 handle() 메서드로 넘깁니다).
작업에 미들웨어 추가하기
작업 클래스에 미들웨어 인스턴스 배열을 반환하는 middleware() 메서드를 정의하세요.
use Queuety\Contracts\Job;
use Queuety\Dispatchable;
use Queuety\Middleware\RateLimited;
use Queuety\Middleware\Timeout;
class SyncInventoryJob implements Job {
use Dispatchable;
public function __construct(
public readonly string $sku,
) {}
public function handle(): void {
// Sync inventory for this SKU...
}
public function middleware(): array {
return [
new Timeout( seconds: 30 ),
new RateLimited( max: 100, window: 60 ),
];
}
}
파이프라인 실행 순서
미들웨어는 배열 순서대로 적용되며 첫 번째 미들웨어가 가장 바깥 계층이 됩니다. middleware() 메서드가 [A, B, C]을 반환하면 실행 순서는 다음과 같습니다.
A::handle() -> B::handle() -> C::handle() -> $job->handle()
각 미들웨어는 다음 미들웨어를 감쌉니다. A는 작업 전에 가장 먼저 실행되고 작업 후에 가장 마지막으로 실행되므로 시간 측정이나 try/catch 래퍼에 적합합니다.
사용자 정의 미들웨어 작성
Contracts\Middleware 인터페이스를 구현하세요.
use Queuety\Contracts\Middleware;
class LogExecution implements Middleware {
public function handle( object $job, \Closure $next ): void {
$class = get_class( $job );
error_log( "[Queuety] Starting: {$class}" );
$start = microtime( true );
try {
$next( $job );
} finally {
$elapsed = round( microtime( true ) - $start, 3 );
error_log( "[Queuety] Finished: {$class} in {$elapsed}s" );
}
}
}
작업에 적용하세요.
public function middleware(): array {
return [
new LogExecution(),
];
}
실행 조기 종료
$next()를 호출하지 않으면 미들웨어가 작업 실행을 완전히 건너뛸 수 있습니다.
class SkipOnMaintenanceMode implements Middleware {
public function handle( object $job, \Closure $next ): void {
if ( wp_is_maintenance_mode() ) {
return; // Job silently skipped.
}
$next( $job );
}
}
기본 제공 미들웨어
ThrottlesExceptions
작업 핸들러에서 발생하는 예외를 제한해 작업 폭주를 방지합니다. 일정 시간 동안 핸들러에서 예외가 너무 많이 발생하면 해당 핸들러의 후속 작업은 처리되지 않고 RuntimeException가 발생합니다. 이렇게 하면 실패한 외부 서비스 때문에 여러 작업의 재시도 횟수가 한꺼번에 모두 소진되는 일을 막을 수 있습니다.
use Queuety\Middleware\ThrottlesExceptions;
public function middleware(): array {
return [
new ThrottlesExceptions( max_attempts: 10, decay_minutes: 10 ),
];
}
| 매개변수 | 유형 | 기본값 | 설명 |
|---|---|---|---|
$max_attempts |
int |
10 |
감소 기간 내 허용되는 최대 예외 수 |
$decay_minutes |
int |
10 |
기간(분) |
예외 수는 Queuety의 잠금 테이블에서 핸들러 클래스별로 추적되며 모든 작업자 프로세스에서 공유됩니다. 임계값에 도달하면 미들웨어는 작업을 호출하지 않고 RuntimeException를 발생시킵니다. 그러면 작업자는 이를 일반 실패와 동일하게 처리합니다. 시도 횟수는 max_attempts에 반영되고, 작업은 일반적인 백오프 간격으로 재시도되며, 시도 횟수를 모두 소진하면 보관 상태가 됩니다.
예: 외부 API가 다운되면 처음 10번의 실패는 정상적으로 처리되며 매번 예외 카운터가 증가합니다. 새 예외가 발생하지 않은 채 10분이 지날 때까지 후속 시도는 API를 호출하지 않고 빠르게 실패합니다. 기록된 예외가 발생할 때마다 기간이 다시 시작됩니다.
RateLimited
일정 시간 내 실행 횟수의 최대값을 적용합니다. 미들웨어 내에서 한도를 초과하면 RateLimitExceededException가 발생하며, 작업자는 이를 일반적인 실패 시도처럼 처리해 일반 백오프 간격으로 재시도하고 max_attempts에 반영합니다. 미들웨어가 작업자의 속도 제한기에 한도를 등록하면 한도에 도달한 동안 이후 해당 핸들러의 작업도 실행 전에 지연 없이 pending로 되돌아갑니다(속도 제한 참고).
use Queuety\Middleware\RateLimited;
public function middleware(): array {
return [
new RateLimited( max: 60, window: 60 ), // 60 per minute
];
}
| 매개변수 | 유형 | 설명 |
|---|---|---|
$max |
int |
기간 내 허용되는 최대 실행 횟수 |
$window |
int |
기간(초) |
속도 제한기는 Queuety의 내부 RateLimiter 인스턴스를 사용하므로 모든 작업자 프로세스에 한도가 적용됩니다.
Timeout
pcntl_alarm를 사용해 작업별 실행 시간 제한을 설정하고 작업이 너무 오래 실행되면 TimeoutException를 발생시키도록 설계되었습니다. 현재는 시간 제한이 적용되지 않습니다(#10). Queuety가 비동기 신호를 활성화하지 않으므로 작업 실행 중에는 알람 핸들러가 작동하지 않습니다. 또한 미들웨어가 반환될 때 알람을 초기화해 작업자 자체의 작업 시간 제한 알람을 취소합니다.
use Queuety\Middleware\Timeout;
public function middleware(): array {
return [
new Timeout( seconds: 30 ),
];
}
| 매개변수 | 유형 | 설명 |
|---|---|---|
$seconds |
int |
최대 실행 시간(초) |
pcntl PHP 확장 기능이 필요합니다. pcntl를 사용할 수 없으면 미들웨어는 아무 동작 없이 넘어갑니다(시간 제한은 적용되지 않음).
UniqueJob
같은 키를 가진 작업이 동시에 실행되는 것을 방지합니다. 데이터베이스 잠금 테이블을 사용해 한 번에 하나의 인스턴스만 실행되도록 합니다. 잠금이 이미 설정된 경우 작업을 조용히 건너뛰며 재시도하지 않습니다.
use Queuety\Middleware\UniqueJob;
public function middleware(): array {
return [
new UniqueJob(), // Lock key defaults to the job class name
new UniqueJob( key: 'sync_inventory' ), // Custom lock key
];
}
| 매개변수 | 유형 | 기본값 | 설명 |
|---|---|---|---|
$key |
string|null |
null(클래스 이름 사용) |
고유성을 위한 잠금 키 |
WithoutOverlapping
같은 논리 키를 공유하는 작업의 중복 실행을 방지하며, 작업자 충돌로 인한 교착 상태를 막기 위해 잠금이 자동으로 만료됩니다. UniqueJob와 비슷하지만 시간 초과 후 잠금이 자동 해제되어야 하는 장시간 실행 작업을 위해 설계되었습니다.
use Queuety\Middleware\WithoutOverlapping;
public function middleware(): array {
return [
new WithoutOverlapping( key: 'import_products', release_after: 600 ),
];
}
| 매개변수 | 유형 | 기본값 | 설명 |
|---|---|---|---|
$key |
string |
(필수) | 논리 작업 키 |
$release_after |
int |
300 |
자동 해제 전 최대 잠금 시간(초) |
잠금이 이미 설정된 경우 작업을 조용히 건너뜁니다. 만료된 잠금은 잠금을 획득할 때마다 자동으로 정리됩니다.
예시: 미들웨어 조합
use Queuety\Contracts\Job;
use Queuety\Dispatchable;
use Queuety\Middleware\RateLimited;
use Queuety\Middleware\Timeout;
use Queuety\Middleware\WithoutOverlapping;
class ImportProductsJob implements Job {
use Dispatchable;
public function __construct(
public readonly int $supplier_id,
) {}
public function handle(): void {
// Fetch and import products from the supplier...
}
public function middleware(): array {
return [
new Timeout( seconds: 300 ),
new WithoutOverlapping(
key: "import_supplier_{$this->supplier_id}",
release_after: 600,
),
new RateLimited( max: 5, window: 60 ),
];
}
}
이 작업은 다음과 같이 동작합니다.
- 5분 넘게 실행되면 종료됩니다(Timeout)
- 같은 공급업체의 다른 가져오기가 이미 실행 중이면 건너뜁니다(WithoutOverlapping)
- 모든 공급업체를 통틀어 분당 가져오기를 5회로 제한합니다(RateLimited)