From 260658c7a0fbda5041fb1aef50e3d978b17197cc Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Tue, 25 Aug 2026 11:27:07 +0100 Subject: [PATCH 1/5] setup --- README.md | 4 +- composer.json | 3 +- src/CallQueuedClosure.php | 112 +++ src/Compatibility/SignalTrait.php | 402 ++++++++++ src/Config/queue.php | 56 ++ src/Drivers/ConnectorInterface.php | 14 + src/Drivers/DatabaseDriver.php | 468 +++++++++++ src/Enums/WorkerStopReason.php | 15 + src/Events/QueueEvent.php | 216 +++++ src/Events/QueueEventManager.php | 250 ++++++ src/Exceptions/InvalidPayloadException.php | 23 + src/Exceptions/ManuallyFailedException.php | 10 + .../MaxAttemptsExceededException.php | 24 + src/Exceptions/QueueException.php | 64 ++ src/Exceptions/TimeoutExceededException.php | 18 + src/Jobs/DatabaseJob.php | 75 ++ src/Jobs/DatabaseJobRecord.php | 47 ++ src/Jobs/FakeJob.php | 81 ++ src/Jobs/InspectedJob.php | 42 + src/Jobs/Job.php | 321 ++++++++ src/Jobs/JobName.php | 42 + src/Manager.php | 333 ++++++++ src/Queue.php | 404 ++++++++++ src/Worker.php | 759 ++++++++++++++++++ src/WorkerOptions.php | 36 + 25 files changed, 3816 insertions(+), 3 deletions(-) create mode 100644 src/CallQueuedClosure.php create mode 100644 src/Compatibility/SignalTrait.php create mode 100644 src/Config/queue.php create mode 100644 src/Drivers/ConnectorInterface.php create mode 100644 src/Drivers/DatabaseDriver.php create mode 100644 src/Enums/WorkerStopReason.php create mode 100644 src/Events/QueueEvent.php create mode 100644 src/Events/QueueEventManager.php create mode 100644 src/Exceptions/InvalidPayloadException.php create mode 100644 src/Exceptions/ManuallyFailedException.php create mode 100644 src/Exceptions/MaxAttemptsExceededException.php create mode 100644 src/Exceptions/QueueException.php create mode 100644 src/Exceptions/TimeoutExceededException.php create mode 100644 src/Jobs/DatabaseJob.php create mode 100644 src/Jobs/DatabaseJobRecord.php create mode 100644 src/Jobs/FakeJob.php create mode 100644 src/Jobs/InspectedJob.php create mode 100644 src/Jobs/Job.php create mode 100644 src/Jobs/JobName.php create mode 100644 src/Manager.php create mode 100644 src/Queue.php create mode 100644 src/Worker.php create mode 100644 src/WorkerOptions.php diff --git a/README.md b/README.md index ec296bd..386f3bf 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Créez votre premier Job via la commande: php klinge queue:job Example ``` -Et ajoutez-le au tableau des gestionnaires (`handlers`) dans le fichier `app\Config\queue.php`: +Et ajoutez-le au tableau des gestionnaires (`jobs`) dans le fichier `app\Config\queue.php`: ```php // ... @@ -62,7 +62,7 @@ use App\Jobs\Example; return [ // --- - 'handlers' => [ + 'jobs' => [ 'my-example' => Example::class ], diff --git a/composer.json b/composer.json index afd678e..aeccb99 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ } ], "require": { - "php": "^8.1" + "php": "^8.1", + "blitz-php/database": "^0.8.3" }, "require-dev": { "blitz-php/coding-standard": "^1.4", diff --git a/src/CallQueuedClosure.php b/src/CallQueuedClosure.php new file mode 100644 index 0000000..bc29cc9 --- /dev/null +++ b/src/CallQueuedClosure.php @@ -0,0 +1,112 @@ +closure = $closure; + } + + /** + * Create a new job instance. + */ + public static function create(Closure $job): self + { + return new self(new SerializableClosure($job)); + } + + /** + * Execute the job. + */ + public function handle(ContainerInterface $container): void + { + $container->call($this->closure->getClosure(), ['job' => $this]); + } + + /** + * Add a callback to be executed if the job fails. + */ + public function onFailure(callable $callback): self + { + $this->failureCallbacks[] = $callback instanceof Closure + ? new SerializableClosure($callback) + : $callback; + + return $this; + } + + /** + * Handle a job failure. + */ + public function failed(Throwable $e):void + { + foreach ($this->failureCallbacks as $callback) { + $callback($e); + } + } + + /** + * Get the display name for the queued job. + */ + public function displayName(): string + { + $closure = $this->closure instanceof SerializableClosure + ? $this->closure->getClosure() + : $this->closure; + + $reflection = new ReflectionFunction($closure); + + $prefix = is_null($this->name) ? '' : "{$this->name} - "; + + return $prefix.'Closure ('.basename($reflection->getFileName()).':'.$reflection->getStartLine().')'; + } + + /** + * Assign a name to the job. + */ + public function name(string $name): self + { + $this->name = $name; + + return $this; + } +} diff --git a/src/Compatibility/SignalTrait.php b/src/Compatibility/SignalTrait.php new file mode 100644 index 0000000..c724ff4 --- /dev/null +++ b/src/Compatibility/SignalTrait.php @@ -0,0 +1,402 @@ + + */ + private array $registeredSignals = []; + + /** + * Signal-to-method mapping. + * + * @var array + */ + private array $signalMethodMap = []; + + /** + * Cached result of PCNTL extension availability. + */ + private static ?bool $isPcntlAvailable = null; + + /** + * Cached result of POSIX extension availability. + */ + private static ?bool $isPosixAvailable = null; + + /** + * Check if PCNTL extension is available (cached). + */ + protected function isPcntlAvailable(): bool + { + if (self::$isPcntlAvailable === null) { + if (is_windows()) { + self::$isPcntlAvailable = false; + } else { + self::$isPcntlAvailable = extension_loaded('pcntl'); + if (! self::$isPcntlAvailable) { + // CLI::write('PCNTL extension is not available. Signal handling will be disabled.', 'yellow'); + } + } + } + + return self::$isPcntlAvailable; + } + + /** + * Check if POSIX extension is available (cached). + */ + protected function isPosixAvailable(): bool + { + if (self::$isPosixAvailable === null) { + self::$isPosixAvailable = is_windows() ? false : extension_loaded('posix'); + } + + return self::$isPosixAvailable; + } + + /** + * Register signal handlers. + * + * @param list $signals List of signals to handle + * @param array $methodMap Optional signal-to-method mapping + */ + protected function registerSignals( + array $signals = [], + array $methodMap = [], + ): void { + if (! $this->isPcntlAvailable()) { + return; + } + + if ($signals === []) { + $signals = [SIGTERM, SIGINT, SIGHUP, SIGQUIT]; + } + + if (! $this->isPosixAvailable() && (in_array(SIGTSTP, $signals, true) || in_array(SIGCONT, $signals, true))) { + // CLI::write('POSIX extension is not available. SIGTSTP and SIGCONT signals will be disabled.', 'yellow'); + $signals = array_diff($signals, [SIGTSTP, SIGCONT]); + + // Remove from method map as well + unset($methodMap[SIGTSTP], $methodMap[SIGCONT]); + + if ($signals === []) { + return; + } + } + + // Enable async signals for immediate response + pcntl_async_signals(true); + + $this->signalMethodMap = $methodMap; + + foreach ($signals as $signal) { + if (pcntl_signal($signal, [$this, 'handleSignal'])) { + $this->registeredSignals[] = $signal; + } else { + $signal = $this->getSignalName($signal); + // CLI::write("Failed to register signal handler for {$signal}.", 'red'); + } + } + } + + /** + * Handle incoming signals. + */ + protected function handleSignal(int $signal): void + { + $this->callCustomHandler($signal); + + // Apply standard Unix signal behavior for registered signals + switch ($signal) { + case SIGTERM: + case SIGINT: + case SIGQUIT: + case SIGHUP: + $this->running = false; + break; + + case SIGTSTP: + // Restore default handler and re-send signal to actually suspend + pcntl_signal(SIGTSTP, SIG_DFL); + posix_kill(posix_getpid(), SIGTSTP); + break; + + case SIGCONT: + // Re-register SIGTSTP handler after resume + pcntl_signal(SIGTSTP, [$this, 'handleSignal']); + break; + } + } + + /** + * Call custom signal handler if one is mapped for this signal. + * Falls back to generic onInterruption() method if no explicit mapping exists. + */ + private function callCustomHandler(int $signal): void + { + // Check for explicit mapping first + $method = $this->signalMethodMap[$signal] ?? null; + + if ($method !== null && method_exists($this, $method)) { + $this->{$method}($signal); + + return; + } + + // If no explicit mapping, try generic catch-all method + if (method_exists($this, 'onInterruption')) { // @phpstan-ignore-line + $this->onInterruption($signal); + } + } + + /** + * Check if command should terminate. + */ + protected function shouldTerminate(): bool + { + return ! $this->running; + } + + /** + * Check if the process is currently running (not terminated). + */ + protected function isRunning(): bool + { + return $this->running; + } + + /** + * Request immediate termination. + */ + protected function requestTermination(): void + { + $this->running = false; + } + + /** + * Reset all states (for testing or restart scenarios). + */ + protected function resetState(): void + { + $this->running = true; + + // Unblock signals if they were blocked + if ($this->signalsBlocked) { + $this->unblockSignals(); + } + } + + /** + * Execute a callable with ALL signals blocked to prevent ANY interruption during critical operations. + * + * This blocks ALL interruptible signals including: + * - Termination signals (SIGTERM, SIGINT, etc.) + * - Pause/resume signals (SIGTSTP, SIGCONT) + * - Custom signals (SIGUSR1, SIGUSR2) + * + * Only SIGKILL (unblockable) can still terminate the process. + * Use this for database transactions, file operations, or any critical atomic operations. + * + * @template TReturn + * + * @param Closure():TReturn $operation + * + * @return TReturn + */ + protected function withSignalsBlocked(Closure $operation) + { + $this->blockSignals(); + + try { + return $operation(); + } finally { + $this->unblockSignals(); + } + } + + /** + * Block ALL interruptible signals during critical sections. + * Only SIGKILL (unblockable) can terminate the process. + */ + protected function blockSignals(): void + { + if (! $this->signalsBlocked && $this->isPcntlAvailable()) { + // Block ALL signals that could interrupt critical operations + pcntl_sigprocmask(SIG_BLOCK, [ + SIGTERM, SIGINT, SIGHUP, SIGQUIT, // Termination signals + SIGTSTP, SIGCONT, // Pause/resume signals + SIGUSR1, SIGUSR2, // Custom signals + SIGPIPE, SIGALRM, // Other common signals + ]); + $this->signalsBlocked = true; + } + } + + /** + * Unblock previously blocked signals. + */ + protected function unblockSignals(): void + { + if ($this->signalsBlocked && $this->isPcntlAvailable()) { + // Unblock the same signals we blocked + pcntl_sigprocmask(SIG_UNBLOCK, [ + SIGTERM, SIGINT, SIGHUP, SIGQUIT, // Termination signals + SIGTSTP, SIGCONT, // Pause/resume signals + SIGUSR1, SIGUSR2, // Custom signals + SIGPIPE, SIGALRM, // Other common signals + ]); + $this->signalsBlocked = false; + } + } + + /** + * Check if signals are currently blocked. + */ + protected function signalsBlocked(): bool + { + return $this->signalsBlocked; + } + + /** + * Add or update signal-to-method mapping at runtime. + */ + protected function mapSignal(int $signal, string $method): void + { + $this->signalMethodMap[$signal] = $method; + } + + /** + * Get human-readable signal name. + */ + protected function getSignalName(int $signal): string + { + return match ($signal) { + SIGTERM => 'SIGTERM', + SIGINT => 'SIGINT', + SIGHUP => 'SIGHUP', + SIGQUIT => 'SIGQUIT', + SIGUSR1 => 'SIGUSR1', + SIGUSR2 => 'SIGUSR2', + SIGPIPE => 'SIGPIPE', + SIGALRM => 'SIGALRM', + SIGTSTP => 'SIGTSTP', + SIGCONT => 'SIGCONT', + default => "Signal {$signal}", + }; + } + + /** + * Unregister all signals (cleanup). + */ + protected function unregisterSignals(): void + { + if (! $this->isPcntlAvailable()) { + return; + } + + foreach ($this->registeredSignals as $signal) { + pcntl_signal($signal, SIG_DFL); + } + + $this->registeredSignals = []; + $this->signalMethodMap = []; + } + + /** + * Check if signals are registered. + */ + protected function hasSignals(): bool + { + return $this->registeredSignals !== []; + } + + /** + * Get list of registered signals. + * + * @return list + */ + protected function getSignals(): array + { + return $this->registeredSignals; + } + + /** + * Get comprehensive process state information. + * + * @return array{ + * pid: int, + * running: bool, + * pcntl_available: bool, + * registered_signals: int, + * registered_signals_names: array, + * signals_blocked: bool, + * explicit_mappings: int, + * memory_usage_mb: float, + * memory_peak_mb: float, + * session_id?: false|int, + * process_group?: false|int, + * has_controlling_terminal?: bool + * } + */ + protected function getProcessState(): array + { + $pid = getmypid(); + $state = [ + // Process identification + 'pid' => $pid, + 'running' => $this->running, + + // Signal handling status + 'pcntl_available' => $this->isPcntlAvailable(), + 'registered_signals' => count($this->registeredSignals), + 'registered_signals_names' => array_map([$this, 'getSignalName'], $this->registeredSignals), + 'signals_blocked' => $this->signalsBlocked, + 'explicit_mappings' => count($this->signalMethodMap), + + // System resources + 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2), + 'memory_peak_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2), + ]; + + // Add terminal control info if POSIX extension is available + if ($this->isPosixAvailable()) { + $state['session_id'] = posix_getsid($pid); + $state['process_group'] = posix_getpgid($pid); + $state['has_controlling_terminal'] = posix_isatty(STDIN); + } + + return $state; + } + } +} diff --git a/src/Config/queue.php b/src/Config/queue.php new file mode 100644 index 0000000..e5a29c0 --- /dev/null +++ b/src/Config/queue.php @@ -0,0 +1,56 @@ + env('queue.connection', 'database'), + + 'connections' => [ + 'database' => [ + 'group' => env('queue.database.group', 'default'), + 'shared' => true, + 'skip_locked' => true, + 'table' => env('queue.database.table', 'jobs'), + ], + 'redis' => [ + 'driver' => 'redis', + 'host' => env('redis.host', '127.0.0.1'), + 'password' => env('redis.password', null), + 'port' => env('redis.port', 6379), + 'database' => env('redis.database', 0), + ], + 'predis' => [ + 'driver' => 'predis', + 'scheme' => 'tcp', + 'host' => env('redis.host', '127.0.0.1'), + 'password' => env('redis.password', null), + 'port' => env('redis.port', 6379), + 'database' => env('redis.database', 0), + ], + 'rabbitmq' => [ + 'driver' => 'rabbitmq', + 'host' => env('rabbitmq.host', '127.0.0.1'), + 'port' => env('rabbitmq.port', 5672), + 'user' => env('rabbitmq.user', 'guest'), + 'password' => env('rabbitmq.password', 'guest'), + 'vhost' => env('rabbitmq.vhost', '/'), + ], + ], + + 'drivers' => [ + 'database' => \BlitzPHP\Queue\Drivers\Database::class, + 'redis' => \BlitzPHP\Queue\Drivers\Redis::class, + 'predis' => \BlitzPHP\Queue\Drivers\Predis::class, + 'rabbitmq' => \BlitzPHP\Queue\Drivers\RabbitMQ::class, + ], + + 'keep_failed_jobs' => true, + + 'failed' => [ + 'driver' => env('queue.failed_driver', 'database-uuids'), + 'database' => env('db.connection', 'default'), + 'table' => 'failed_jobs', + ], + + 'batching' => [ + 'database' => env('db.connection', 'default'), + 'table' => 'job_batches', + ], +]; diff --git a/src/Drivers/ConnectorInterface.php b/src/Drivers/ConnectorInterface.php new file mode 100644 index 0000000..fdebdc6 --- /dev/null +++ b/src/Drivers/ConnectorInterface.php @@ -0,0 +1,14 @@ +dispatchAfterCommit = $dispatchAfterCommit; + } + + /** + * Establish a queue connection. + * + * @param array $config + */ + public function connect(ContainerInterface $container, array $config): QueueContract + { + return new self( + $container->get(ConnectionResolverInterface::class)->connection($config['connection'] ?? null), + $config['table'], + $config['queue'], + $config['retry_after'] ?? 60, + $config['after_commit'] ?? null + ); + } + + /** + * Get the size of the queue. + */ + public function size(?string $queue = null): int + { + return $this->database->table($this->table) + ->where('queue', $this->getQueue($queue)) + ->count(); + } + + /** + * Get the number of pending jobs. + */ + public function pendingSize(?string $queue = null): int + { + return $this->database->table($this->table) + ->where('queue', $this->getQueue($queue)) + ->where('available_at <=', $this->currentTime()) + ->whereNull('reserved_at') + ->count(); + } + + /** + * Get the number of delayed jobs. + */ + public function delayedSize(?string $queue = null): int + { + return $this->database->table($this->table) + ->where('queue', $this->getQueue($queue)) + ->where('available_at >', $this->currentTime()) + ->whereNull('reserved_at') + ->count(); + } + + /** + * Get the number of reserved jobs. + */ + public function reservedSize(?string $queue = null): int + { + return $this->database->table($this->table) + ->where('queue', $this->getQueue($queue)) + ->whereNotNull('reserved_at') + ->count(); + } + + /** + * Get the pending jobs for the given queue. + * + * @return Collection + */ + public function pendingJobs(?string $queue = null): Collection + { + $data = $this->database->table($this->table) + ->where('queue', $this->getQueue($queue)) + ->where('available_at <=', $this->currentTime()) + ->whereNull('reserved_at') + ->all(); + + return collect($data)->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); + } + + /** + * Get the delayed jobs for the given queue. + * + * @return Collection + */ + public function delayedJobs(?string $queue = null): Collection + { + $data = $this->database->table($this->table) + ->where('queue', $this->getQueue($queue)) + ->where('available_at >', $this->currentTime()) + ->whereNull('reserved_at') + ->all(); + + return collect($data) + ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); + } + + /** + * Get the reserved jobs for the given queue. + * + * @return Collection + */ + public function reservedJobs(?string $queue = null): Collection + { + $data = $this->database->table($this->table) + ->where('queue', $this->getQueue($queue)) + ->whereNotNull('reserved_at') + ->all(); + + return collect($data) + ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); + } + + /** + * Get the creation timestamp of the oldest pending job, excluding delayed jobs. + */ + public function creationTimeOfOldestPendingJob(?string $queue = null): ?int + { + return $this->database->table($this->table) + ->where('queue', $this->getQueue($queue)) + ->where('available_at <=', $this->currentTime()) + ->whereNull('reserved_at') + ->sortAsc('available_at') + ->value('available_at'); + } + + /** + * Push a new job onto the queue. + */ + public function push(string|Job $job, mixed $data = '', ?string $queue = null): mixed + { + return $this->enqueueUsing( + $job, + $this->createPayload($job, $this->getQueue($queue), $data), + $queue, + null, + fn ($payload, $queue) => $this->pushToDatabase($queue, $payload), + ); + } + + /** + * Push a raw payload onto the queue. + */ + public function pushRaw(string $payload, ?string $queue = null, array $options = []): mixed + { + return $this->pushToDatabase($queue, $payload); + } + + /** + * Push a new job onto the queue after (n) seconds. + */ + public function later(DateTimeInterface|DateInterval|int $delay, string|Job $job, mixed $data = '', ?string $queue = null): mixed + { + return $this->enqueueUsing( + $job, + $this->createPayload($job, $this->getQueue($queue), $data, $delay), + $queue, + $delay, + fn ($payload, $queue, $delay) => $this->pushToDatabase($queue, $payload, $delay), + ); + } + + /** + * Push an array of jobs onto the queue. + */ + public function bulk(array $jobs, mixed $data = '', ?string $queue = null): mixed + { + $queue = $this->getQueue($queue); + + $now = $this->availableAt(); + + $this->database->table($this->table)->insert((new Collection((array) $jobs))->map( + function ($job) use ($queue, $data, $now) { + return $this->buildDatabaseRecord( + $queue, + $this->createPayload($job, $this->getQueue($queue), $data), + isset($job->delay) ? $this->availableAt($job->delay) : $now, + ); + } + )->all()); + + return null; + } + + /** + * Release a reserved job back onto the queue after (n) seconds. + */ + public function release(string $queue, DatabaseJobRecord $job, int $delay): mixed + { + return $this->pushToDatabase($queue, $job->payload, $delay, $job->attempts); + } + + /** + * Push a raw payload to the database with a given delay of (n) seconds. + */ + protected function pushToDatabase(?string $queue, string $payload, DateTimeInterface|DateInterval|int $delay = 0, int $attempts = 0): mixed + { + $builder = $this->database->table($this->table); + + $builder->insert($this->buildDatabaseRecord( + $this->getQueue($queue), + $payload, + $this->availableAt($delay), + $attempts + )); + + return $builder->lastId(); + } + + /** + * Create an array to insert for the given job. + */ + protected function buildDatabaseRecord(?string $queue, string $payload, int $availableAt, int $attempts = 0): array + { + return [ + 'queue' => $queue, + 'attempts' => $attempts, + 'reserved_at' => null, + 'available_at' => $availableAt, + 'created_at' => $this->currentTime(), + 'payload' => $payload, + ]; + } + + /** + * Pop the next job off of the queue. + * + * @throws Throwable + */ + public function pop(string $queue = null): ?Job + { + $queue = $this->getQueue($queue); + + $jobRecord = null; + + try { + return $this->database->transaction(function () use ($queue, &$jobRecord) { + if ($jobRecord = $this->getNextAvailableJob($queue)) { + return $this->marshalJob($queue, $jobRecord); + } + }); + } catch (Throwable $e) { + // Potentially invalid job that we need to fail (#58978)... + if ($jobRecord) { + try { + (new DatabaseJob( + $this->container, $this, $jobRecord, $this->connectionName, $queue + ))->fail($e); + } catch (Throwable) { + // Ignore and throw the original exception... + } + } + + throw $e; + } + } + + /** + * Get the next available job for the queue. + */ + protected function getNextAvailableJob(?string $queue): ?DatabaseJobRecord + { + $job = $this->database->table($this->table) + // ->lock($this->getLockForPopping()) available only in blitz-php/database > 1.2 + ->where('queue', $this->getQueue($queue)) + ->where(function ($query) { + $this->isAvailable($query); + $this->isReservedButExpired($query); + }) + ->orderBy('id', 'asc') + ->first(); + + return $job ? new DatabaseJobRecord((object) $job) : null; + } + + /** + * Get the lock required for popping the next job. + * + * @return string|bool + */ + protected function getLockForPopping() + { + if ($this->lockForPopping !== null) { + return $this->lockForPopping; + } + + $databaseEngine = $this->database->getConnection()->getAttribute(PDO::ATTR_DRIVER_NAME); + $databaseVersion = $this->database->getConnection()->getAttribute(PDO::ATTR_SERVER_VERSION); + + if ((new Stringable($databaseVersion))->contains('MariaDB')) { + $databaseEngine = 'mariadb'; + $databaseVersion = Text::before(Text::after($databaseVersion, '5.5.5-'), '-'); + } elseif ((new Stringable($databaseVersion))->contains(['vitess', 'PlanetScale'])) { + $databaseEngine = 'vitess'; + $databaseVersion = Text::before($databaseVersion, '-'); + } + + if (($databaseEngine === 'mysql' && version_compare($databaseVersion, '8.0.1', '>=')) || + ($databaseEngine === 'mariadb' && version_compare($databaseVersion, '10.6.0', '>=')) || + ($databaseEngine === 'pgsql' && version_compare($databaseVersion, '9.5', '>=')) || + ($databaseEngine === 'vitess' && version_compare($databaseVersion, '19.0', '>=')) + ) { + return $this->lockForPopping = 'FOR UPDATE SKIP LOCKED'; + } + + if ($databaseEngine === 'sqlsrv') { + return $this->lockForPopping = 'with(rowlock,updlock,readpast)'; + } + + return $this->lockForPopping = true; + } + + /** + * Modify the query to check for available jobs. + */ + protected function isAvailable(BuilderInterface $query): void + { + $query->where(function ($query) { + $query->whereNull('reserved_at') + ->where('available_at <=', $this->currentTime()); + }); + } + + /** + * Modify the query to check for jobs that are reserved but have expired. + */ + protected function isReservedButExpired(BuilderInterface $query): void + { + $expiration = Date::now()->subSeconds($this->retryAfter)->getTimestamp(); + + $query->orWhere(function ($query) use ($expiration) { + $query->where('reserved_at', '<=', $expiration); + }); + } + + /** + * Marshal the reserved job into a DatabaseJob instance. + */ + protected function marshalJob(string $queue, DatabaseJobRecord $job): DatabaseJob + { + return new DatabaseJob( + $this->container, + $this, + $this->markJobAsReserved($job), + $this->connectionName, + $queue, + ); + } + + /** + * Mark the given job ID as reserved. + */ + protected function markJobAsReserved(DatabaseJobRecord $job): DatabaseJobRecord + { + $this->database->table($this->table)->where('id', $job->id)->update([ + 'reserved_at' => $job->touch(), + 'attempts' => $job->increment(), + ]); + + return $job; + } + + /** + * Delete a reserved job from the queue. + * + * @throws Throwable + */ + public function deleteReserved(string $queue, string $id): void + { + $this->database->transaction(function () use ($id) { + if ($this->database->table($this->table)/*->lockForUpdate()*/->where('id', $id)->first()) { + $this->database->table($this->table)->where('id', $id)->delete(); + } + }); + } + + /** + * Delete a reserved job from the reserved queue and release it. + */ + public function deleteAndRelease(string $queue, DatabaseJob $job, int $delay): void + { + $this->database->transaction(function () use ($queue, $job, $delay) { + $where = ['id' => $job->getJobId()]; + + if ($this->database->table($this->table)/*->lockForUpdate()*/->where($where)->first()) { + $this->database->table($this->table)->where($where)->delete(); + } + + $this->release($queue, $job->getJobRecord(), $delay); + }); + } + + /** + * Delete all of the jobs from the queue. + */ + public function clear(int $queue): bool + { + $this->database->table($this->table) + ->where('queue', $this->getQueue($queue)) + ->delete(); + + return true; + } + + /** + * Get the queue or return the default. + */ + public function getQueue(?string $queue): string + { + return $queue ?: $this->default; + } + + /** + * Get the underlying database instance. + */ + public function getDatabase(): ConnectionInterface + { + return $this->database; + } +} diff --git a/src/Enums/WorkerStopReason.php b/src/Enums/WorkerStopReason.php new file mode 100644 index 0000000..063feb3 --- /dev/null +++ b/src/Enums/WorkerStopReason.php @@ -0,0 +1,15 @@ +type); + + $this->timestamp = $timestamp ?? Date::now(); + } + + /** + * Get event type + */ + public function getType(): string + { + return $this->type; + } + + /** + * Get connection name + */ + public function getConnection(): string + { + return $this->connection; + } + + /** + * Get queue name + */ + public function getQueue(): ?string + { + return $this->queue; + } + + /** + * Get timestamp + */ + public function getTimestamp(): Date + { + return $this->timestamp; + } + + /** + * Get all metadata + */ + public function getAllMetadata(): array + { + return $this->metadata; + } + + /** + * Get metadata value by key + */ + public function getMetadata(string $key, mixed $default = null): mixed + { + return $this->metadata[$key] ?? $default; + } + + /** + * Check if this is a job-related event + */ + public function isJobEvent(): bool + { + return str_starts_with($this->type, 'queue.job.'); + } + + /** + * Check if this is a worker-related event + */ + public function isWorkerEvent(): bool + { + return str_starts_with($this->type, 'queue.worker.'); + } + + /** + * Check if this is an operation event (like queue.cleared) + */ + public function isOperationEvent(): bool + { + return str_contains($this->type, 'queue.') + && ! $this->isJobEvent() + && ! $this->isWorkerEvent() + && ! $this->isConnectionEvent(); + } + + /** + * Check if this is a connection event + */ + public function isConnectionEvent(): bool + { + return str_starts_with($this->type, 'queue.connection.'); + } + + // Job-related convenience methods (metadata-based) + + /** + * Get job ID (for job events) + */ + public function getJobId(): ?int + { + $job = $this->getMetadata('job'); + + return $job instanceof Job ? $job->getJobId() : $this->getMetadata('job_id'); + } + + /** + * Get job priority (for job events) + */ + public function getPriority(): ?string + { + $job = $this->getMetadata('job'); + + return $job instanceof Job ? $job->priority : $this->getMetadata('priority'); + } + + /** + * Get number of attempts (for job events) + */ + public function getAttempts(): ?int + { + $job = $this->getMetadata('job'); + + return $job instanceof Job ? $job->attempts() : $this->getMetadata('attempts'); + } + + /** + * Get job status (for job events) + */ + public function getStatus(): ?int + { + $job = $this->getMetadata('job'); + + return $job instanceof Job ? $job->status : $this->getMetadata('status'); + } + + /** + * Get job class name (for job events) + */ + public function getJobClass(): ?string + { + return $this->getMetadata('job_class'); + } + + /** + * Get processing time in seconds (for job events) + */ + public function getProcessingTime(): float + { + return (float) $this->getMetadata('processing_time', 0.0); + } + + /** + * Get processing time in milliseconds (for job events) + */ + public function getProcessingTimeMs(): int + { + return (int) ($this->getProcessingTime() * 1000); + } + + /** + * Get exception (for failed events) + */ + public function getException(): ?Throwable + { + return $this->getMetadata('exception'); + } + + /** + * Get exception message (for failed events) + */ + public function getExceptionMessage(): ?string + { + $exception = $this->getException(); + + return $exception?->getMessage(); + } + + /** + * Check if event has failed + */ + public function hasFailed(): bool + { + $job = $this->getMetadata('job'); + + return $job instanceof Job ? $job->hasFailed() : $this->getException() !== null; + } + + /** + * Convert to array for serialization + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'connection' => $this->connection, + 'queue' => $this->queue, + 'metadata' => $this->metadata, + 'timestamp' => $this->timestamp->toDateTimeString(), + ]; + } +} diff --git a/src/Events/QueueEventManager.php b/src/Events/QueueEventManager.php new file mode 100644 index 0000000..186ec50 --- /dev/null +++ b/src/Events/QueueEventManager.php @@ -0,0 +1,250 @@ +events->emit(new QueueEvent( + type : self::JOB_ATTEMPTED, + connection: $connection, + queue : $job->getQueue(), + metadata : compact('job', 'e'), + )); + } + + /** + * Emit job failed event + */ + public function jobFailed(string $connection, Job $job, ?Throwable $e): void + { + $this->events->emit(new QueueEvent( + type : self::JOB_FAILED, + connection: $connection, + queue : $job->getQueue(), + metadata : compact('job', 'e'), + )); + } + + /** + * Emit job exception-occurent event + */ + public function jobExceptionOccured(string $connection, Job $job, Throwable $e): void + { + $this->events->emit(new QueueEvent( + type : self::JOB_EXCEPTION_OCCURED, + connection: $connection, + queue : $job->getQueue(), + metadata : compact('job', 'e'), + )); + } + + /** + * Emit job popping event + */ + public function jobPopping(string $connection, ?string $queue = null): void + { + $this->events->emit(new QueueEvent( + type : self::JOB_POPPING, + connection: $connection, + queue : $queue, + )); + } + + /** + * Emit job popped event + */ + public function jobPopped(string $connection, ?Job $job = null): void + { + $this->events->emit(new QueueEvent( + type : self::JOB_POPPED, + connection: $connection, + queue : $job?->getQueue(), + metadata : compact('job') + )); + } + + /** + * Emit job processing event + */ + public function jobProcessing(string $connection, Job $job): void + { + $this->events->emit(new QueueEvent( + type : self::JOB_PROCESSING, + connection: $connection, + queue : $job->getQueue(), + metadata : compact('job'), + )); + } + + /** + * Emit job processed event + */ + public function jobProcessed(string $connection, Job $job): void + { + $this->events->emit(new QueueEvent( + type : self::JOB_PROCESSED, + connection: $connection, + queue : $job->getQueue(), + metadata : compact('job'), + )); + } + + /** + * Emit job processed event + */ + public function jobQueued(string $connection, ?string $queue, string|int|null $jobId, string|object $job, string $payload, DateTimeInterface|DateInterval|int|null $delay): void + { + $this->events->emit(new QueueEvent( + type : self::JOB_QUEUED, + connection: $connection, + queue : $queue, + metadata : compact('jobId', 'job', 'payload', 'delay'), + )); + } + + /** + * Emit job processed event + */ + public function jobQueuing(string $connection, ?string $queue, string|object $job, string $payload, DateTimeInterface|DateInterval|int|null $delay): void + { + $this->events->emit(new QueueEvent( + type : self::JOB_QUEUEING, + connection: $connection, + queue : $queue, + metadata : compact('job', 'payload', 'delay'), + )); + } + + /** + * Emit job released-after-exception started event + */ + public function jobReleasedAfterException(string $connection, Job $job, int $backoff): void + { + $this->events->emit(new QueueEvent( + type : self::JOB_RELEASED_AFTER_EXCEPTION, + connection: $connection, + queue : $job->getQueue(), + metadata : compact('job', 'backoff'), + )); + } + + /** + * Emit job timeout event + */ + public function jobTimeout(string $connection, string $queue, Job $job, array $metadata = []): void + { + $this->events->emit(new QueueEvent( + type : self::JOB_TIMEOUT, + connection: $connection, + queue : $queue, + metadata : array_merge([ + 'job_class' => $job->payload['job'], + 'job' => $job, + ], $metadata), + )); + } + + /** + * Emit queue cleared event + */ + public function queueCleared(string $connection, ?string $queue = null): void + { + $this->events->emit(new QueueEvent( + type : self::QUEUE_CLEARED, + connection: $connection, + queue : $queue, + )); + } + + /** + * Emit queue paused event + */ + public function queuePaused(string $connection, string $queue, DateTimeInterface|DateInterval|int|null $ttl = null): void + { + $this->events->emit(new QueueEvent( + type : self::QUEUE_PAUSED, + connection: $connection, + queue : $queue, + metadata : compact('ttl'), + )); + } + + /** + * Emit queue resumed event + */ + public function queueResumed(string $connection, string $queue): void + { + $this->events->emit(new QueueEvent( + type : self::QUEUE_RESUMED, + connection: $connection, + queue : $queue, + )); + } + + /** + * Emit worker started event + */ + public function workerStarting(string $connection, string $queue, WorkerOptions $options): void + { + $this->events->emit(new QueueEvent( + type : self::WORKER_STARTING, + connection: $connection, + queue : $queue, + metadata : compact('options') + )); + } + + /** + * Emit worker stopped event + */ + public function workerStopping(string $connection, int $status, ?WorkerOptions $options = null, ?WorkerStopReason $reason = null): void + { + $this->events->emit(new QueueEvent( + type : self::WORKER_STOPPING, + connection: $connection, + metadata : compact('status', 'options', 'reason') + )); + } +} diff --git a/src/Exceptions/InvalidPayloadException.php b/src/Exceptions/InvalidPayloadException.php new file mode 100644 index 0000000..e99d2d7 --- /dev/null +++ b/src/Exceptions/InvalidPayloadException.php @@ -0,0 +1,23 @@ +value = $value; + } +} diff --git a/src/Exceptions/ManuallyFailedException.php b/src/Exceptions/ManuallyFailedException.php new file mode 100644 index 0000000..345651d --- /dev/null +++ b/src/Exceptions/ManuallyFailedException.php @@ -0,0 +1,10 @@ +resolveName().' has been attempted too many times.'), function ($e) use ($job) { + $e->job = $job; + }); + } +} diff --git a/src/Exceptions/QueueException.php b/src/Exceptions/QueueException.php new file mode 100644 index 0000000..321fd2d --- /dev/null +++ b/src/Exceptions/QueueException.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Queue\Exceptions; + +use RuntimeException; + +final class QueueException extends RuntimeException +{ + public static function forIncorrectHandler(): static + { + return new self(lang('Queue.incorrectHandler')); + } + + public static function forIncorrectQueueFormat(): static + { + return new self(lang('Queue.incorrectQueueFormat')); + } + + public static function forTooLongQueueName(): static + { + return new self(lang('Queue.tooLongQueueName')); + } + + public static function forIncorrectJobHandler(): static + { + return new self(lang('Queue.incorrectJobHandler')); + } + + public static function forIncorrectPriorityFormat(): static + { + return new self(lang('Queue.incorrectPriorityFormat')); + } + + public static function forTooLongPriorityName(): static + { + return new self(lang('Queue.tooLongPriorityName')); + } + + public static function forIncorrectQueuePriority(string $priority, string $queue): static + { + return new self(lang('Queue.incorrectQueuePriority', [$priority, $queue])); + } + + public static function forIncorrectDelayValue(): static + { + return new self(lang('Queue.incorrectDelayValue')); + } + + public static function forFailedJsonEncode(string $error): static + { + return new self(lang('Queue.failedToJsonEncode', [$error])); + } +} diff --git a/src/Exceptions/TimeoutExceededException.php b/src/Exceptions/TimeoutExceededException.php new file mode 100644 index 0000000..1f5ed47 --- /dev/null +++ b/src/Exceptions/TimeoutExceededException.php @@ -0,0 +1,18 @@ +resolveName().' has timed out.'), function ($e) use ($job) { + $e->job = $job; + }); + } +} diff --git a/src/Jobs/DatabaseJob.php b/src/Jobs/DatabaseJob.php new file mode 100644 index 0000000..9e29934 --- /dev/null +++ b/src/Jobs/DatabaseJob.php @@ -0,0 +1,75 @@ +queue = $queue; + $this->container = $container; + $this->connectionName = $connectionName; + } + + /** + * Release the job back into the queue after (n) seconds. + */ + public function release(int $delay = 0): void + { + parent::release($delay); + + $this->database->deleteAndRelease($this->queue, $this, $delay); + } + + /** + * Delete the job from the queue. + */ + public function delete(): void + { + parent::delete(); + + $this->database->deleteReserved($this->queue, $this->job->id); + } + + /** + * Get the number of times the job has been attempted. + */ + public function attempts(): int + { + return (int) $this->job->attempts; + } + + /** + * Get the job identifier. + */ + public function getJobId(): string|int + { + return $this->job->id; + } + + /** + * Get the raw body string for the job. + */ + public function getRawBody(): string + { + return $this->job->payload; + } + + /** + * Get the database job record. + */ + public function getJobRecord(): DatabaseJobRecord + { + return $this->job; + } +} diff --git a/src/Jobs/DatabaseJobRecord.php b/src/Jobs/DatabaseJobRecord.php new file mode 100644 index 0000000..d1436e2 --- /dev/null +++ b/src/Jobs/DatabaseJobRecord.php @@ -0,0 +1,47 @@ +record->attempts++; + + return $this->record->attempts; + } + + /** + * Update the "reserved at" timestamp of the job. + */ + public function touch(): int + { + $this->record->reserved_at = $this->currentTime(); + + return $this->record->reserved_at; + } + + /** + * Dynamically access the underlying job information. + */ + public function __get(string $key): mixed + { + return $this->record->{$key}; + } +} diff --git a/src/Jobs/FakeJob.php b/src/Jobs/FakeJob.php new file mode 100644 index 0000000..74eb2bd --- /dev/null +++ b/src/Jobs/FakeJob.php @@ -0,0 +1,81 @@ +released = true; + $this->releaseDelay = $delay; + } + + /** + * Get the number of times the job has been attempted. + */ + public function attempts(): int + { + return $this->attempts; + } + + /** + * Delete the job from the queue. + */ + public function delete(): void + { + $this->deleted = true; + } + + /** + * Delete the job, call the "failed" method, and raise the failed job event. + */ + public function fail(?Throwable $e = null): void + { + $this->failed = true; + $this->failedWith = $e; + } +} diff --git a/src/Jobs/InspectedJob.php b/src/Jobs/InspectedJob.php new file mode 100644 index 0000000..093d895 --- /dev/null +++ b/src/Jobs/InspectedJob.php @@ -0,0 +1,42 @@ +payload()['uuid'] ?? null; + } + + /** + * Fire the job. + */ + public function fire(): void + { + $payload = $this->payload(); + + [$class, $method] = JobName::parse($payload['job']); + + ($this->instance = $this->resolve($class))->{$method}($this, $payload['data']); + } + + /** + * Delete the job from the queue. + */ + public function delete(): void + { + $this->deleted = true; + } + + /** + * Determine if the job has been deleted. + */ + public function isDeleted(): bool + { + return $this->deleted; + } + + /** + * Release the job back into the queue after (n) seconds. + */ + public function release(int $delay = 0): void + { + $this->released = true; + } + + /** + * Determine if the job was released back into the queue. + */ + public function isReleased(): bool + { + return $this->released; + } + + /** + * Determine if the job has been deleted or released. + */ + public function isDeletedOrReleased(): bool + { + return $this->isDeleted() || $this->isReleased(); + } + + /** + * Determine if the job has been marked as a failure. + */ + public function hasFailed(): bool + { + return $this->failed; + } + + /** + * Mark the job as "failed" + */ + public function markAsFailed(): void + { + $this->failed = true; + } + + /** + * Delete the job, call the "failed" method, and raise the failed job event. + */ + public function fail(?Throwable $e = null): void + { + $this->markAsFailed(); + + if ($this->isDeleted()) { + return; + } + + if ($this->shouldRollBackDatabaseTransaction($e)) { + $this->container->get(ConnectionResolverInterface::class) + ->connection(config('queue.failed.database')) + ->rollBack(); + } + + try { + // If the job has failed, we will delete it, call the "failed" method and then call + // an event indicating the job has failed so it can be logged if needed. This is + // to allow every developer to better keep monitor of their failed queue jobs. + $this->delete(); + + $this->failed($e); + } finally { + $this->resolve(QueueEventManager::class)->jobFailed($this->connectionName, $this, $e ?: new ManuallyFailedException); + } + } + + /** + * Determine if the current database transaction should be rolled back to level zero. + */ + protected function shouldRollBackDatabaseTransaction(Throwable $e): bool + { + $config = config('queue.failed'); + + return $e instanceof TimeoutExceededException && + $config['database'] && + in_array($config['driver'], ['database', 'database-uuids']) && + $this->container->bound(ConnectionResolverInterface::class); + } + + /** + * Process an exception that caused the job to fail. + */ + protected function failed(?Throwable $e): void + { + $payload = $this->payload(); + + [$class] = JobName::parse($payload['job']); + + if (method_exists($this->instance = $this->resolve($class), 'failed')) { + $this->instance->failed($payload['data'], $e, $payload['uuid'] ?? '', $this); + } + } + + /** + * Resolve the given class. + */ + protected function resolve(string $class): mixed + { + return $this->container->make($class); + } + + /** + * Get the resolved job handler instance. + */ + public function getResolvedJob(): mixed + { + return $this->instance; + } + + /** + * Get the decoded body of the job. + */ + public function payload(): array + { + return json_decode($this->getRawBody(), true); + } + + /** + * Get the number of times to attempt a job. + */ + public function maxTries(): ?int + { + return $this->payload()['maxTries'] ?? null; + } + + /** + * Get the number of times to attempt a job after an exception. + */ + public function maxExceptions(): ?int + { + return $this->payload()['maxExceptions'] ?? null; + } + + /** + * Determine if the job should fail when it timeouts. + */ + public function shouldFailOnTimeout(): bool + { + return $this->payload()['failOnTimeout'] ?? false; + } + + /** + * The number of seconds to wait before retrying a job that encountered an uncaught exception. + * + * @return int|int[]|null + */ + public function backoff() + { + return $this->payload()['backoff'] ?? $this->payload()['delay'] ?? null; + } + + /** + * Get the number of seconds the job can run. + */ + public function timeout(): ?int + { + return $this->payload()['timeout'] ?? null; + } + + /** + * Get the timestamp indicating when the job should timeout. + */ + public function retryUntil(): ?int + { + return $this->payload()['retryUntil'] ?? null; + } + + /** + * Get the name of the queued job class. + */ + public function getName(): string + { + return $this->payload()['job']; + } + + /** + * Get the resolved display name of the queued job class. + * + * Resolves the name of "wrapped" jobs such as class-based handlers. + */ + public function resolveName(): string + { + return JobName::resolve($this->getName(), $this->payload()); + } + + /** + * Get the class of the queued job. + * + * Resolves the class of "wrapped" jobs such as class-based handlers. + */ + public function resolveQueuedJobClass(): string + { + return JobName::resolveClassName($this->getName(), $this->payload()); + } + + /** + * Get the name of the connection the job belongs to. + */ + public function getConnectionName(): string + { + return $this->connectionName; + } + + /** + * Get the name of the queue the job belongs to. + */ + public function getQueue(): string + { + return $this->queue; + } + + /** + * Get the service container instance. + */ + public function getContainer(): ContainerInterface + { + return $this->container; + } +} diff --git a/src/Jobs/JobName.php b/src/Jobs/JobName.php new file mode 100644 index 0000000..4720bd0 --- /dev/null +++ b/src/Jobs/JobName.php @@ -0,0 +1,42 @@ + $payload + */ + public static function resolveClassName(string $name, array $payload): string + { + if (is_string($payload['data']['commandName'] ?? null)) { + return $payload['data']['commandName']; + } + + return $name; + } +} diff --git a/src/Manager.php b/src/Manager.php new file mode 100644 index 0000000..22f249d --- /dev/null +++ b/src/Manager.php @@ -0,0 +1,333 @@ + + */ + protected array $connections = []; + + /** + * The array of resolved queue connectors. + */ + protected array $connectors = []; + + protected QueueEventManager $queueEventManager; + + /** + * Create a new queue manager instance. + */ + public function __construct(protected ContainerInterface $container) + { + } + + /** + * Register an event listener for the before job event. + */ + public function before(callable $callback): void + { + $this->container->get(EventManagerInterface::class)->on( + QueueEventManager::JOB_PROCESSING, + $callback + ); + } + + /** + * Register an event listener for the after job event. + */ + public function after(callable $callback): void + { + $this->container->get(EventManagerInterface::class)->on( + QueueEventManager::JOB_PROCESSED, + $callback + ); + } + + /** + * Register an event listener for the exception occurred job event. + */ + public function exceptionOccurred(callable $callback): void + { + $this->container->get(EventManagerInterface::class)->on( + QueueEventManager::JOB_EXCEPTION_OCCURED, + $callback + ); + } + + /** + * Register an event listener for the daemon queue loop. + */ + public function looping(callable $callback): void + { + $this->container->get(EventManagerInterface::class)->on( + QueueEventManager::JOB_LOOPING, + $callback + ); + } + + /** + * Register an event listener for the failed job event. + */ + public function failing(callable $callback): void + { + $this->container->get(EventManagerInterface::class)->on( + QueueEventManager::JOB_FAILED, + $callback + ); + } + + /** + * Register an event listener for the daemon queue starting. + */ + public function starting(callable $callback): void + { + $this->container->get(EventManagerInterface::class)->on( + QueueEventManager::WORKER_STARTING, + $callback + ); + } + + /** + * Register an event listener for the daemon queue stopping. + */ + public function stopping(callable $callback): void + { + $this->container->get(EventManagerInterface::class)->on( + QueueEventManager::WORKER_STOPPING, + $callback + ); + } + + protected function queueEventManager(): QueueEventManager + { + if (! $this->queueEventManager) { + $this->queueEventManager = $this->container->make(QueueEventManager::class); + } + + return $this->queueEventManager; + } + + /** + * Determine if the driver is connected. + */ + public function connected(UnitEnum|string|null $name = null): bool + { + return isset($this->connections[Helpers::enumValue($name) ?: $this->getDefaultDriver()]); + } + + /** + * Resolve a queue connection instance. + */ + public function connection(UnitEnum|string|null $name = null): QueueContract + { + $name = Helpers::enumValue($name) ?: $this->getDefaultDriver(); + + // If the connection has not been resolved yet we will resolve it now as all + // of the connections are resolved when they are actually needed so we do + // not make any unnecessary connection to the various queue end-points. + if (! isset($this->connections[$name])) { + $this->connections[$name] = $this->resolve($name); + + $this->connections[$name]->setContainer($this->container); + } + + return $this->connections[$name]; + } + + /** + * Resolve a queue connection. + * + * @throws InvalidArgumentException + */ + protected function resolve(string $name): Queue + { + $config = $this->getConfig($name); + + if (is_null($config)) { + throw new InvalidArgumentException("The [{$name}] queue connection has not been configured."); + } + + $queue = $this->getConnector($config['driver']) + ->connect($this->container, $config) + ->setConnectionName($name); + + if (method_exists($queue, 'setConfig')) { + $queue->setConfig($config); + } + + return $queue; + } + + /** + * Get the connector for a given driver. + * + * @throws InvalidArgumentException + */ + protected function getConnector(string $driver): ConnectorInterface + { + if (! isset($this->connectors[$driver])) { + throw new InvalidArgumentException("No connector for [$driver]."); + } + + return call_user_func($this->connectors[$driver]); + } + + /** + * Pause a queue by its connection and name. + */ + public function pause(string $connection, string $queue): void + { + $this->container->get(Cache::class) + ->forever("blitzphp:queue:paused:{$connection}:{$queue}", true); + + $this->queueEventManager()->queuePaused($connection, $queue); + } + + /** + * Pause a queue by its connection and name for a given amount of time. + */ + public function pauseFor(string $connection, string $queue, DateTimeInterface|DateInterval|int $ttl): void + { + $convertedTtl = $ttl instanceof DateTimeInterface ? $ttl->getTimestamp() : $ttl; + + $this->container->get(Cache::class) + ->set("blitzphp:queue:paused:{$connection}:{$queue}", true, $convertedTtl); + + $this->queueEventManager()->queuePaused($connection, $queue, $ttl); + } + + /** + * Resume a paused queue by its connection and name. + */ + public function resume(string $connection, string $queue): void + { + $this->container->get(Cache::class) + ->delete("blitzphp:queue:paused:{$connection}:{$queue}"); + + $this->queueEventManager()->queueResumed($connection, $queue); + } + + /** + * Determine if a queue is paused. + */ + public function isPaused(string $connection, string $queue): bool + { + return (bool) $this->container->get(Cache::class) + ->get("blitzphp:queue:paused:{$connection}:{$queue}", false); + } + + /** + * Indicate that queue workers should not poll for restart or pause signals. + * + * This prevents the workers from hitting the application cache to determine if they need to pause or restart. + */ + public function withoutInterruptionPolling(): void + { + Worker::$restartable = false; + Worker::$pausable = false; + } + + /** + * Add a queue connection resolver. + */ + public function extend(string $driver, Closure $resolver): void + { + $this->addConnector($driver, $resolver); + } + + /** + * Add a queue connection resolver. + */ + public function addConnector(string $driver, Closure $resolver): void + { + $this->connectors[$driver] = $resolver; + } + + /** + * Get the queue connection configuration. + */ + protected function getConfig(string $name): ?array + { + if (! is_null($name) && $name !== 'null') { + return config("queue.connections.{$name}"); + } + + return ['driver' => 'null']; + } + + /** + * Get the name of the default queue connection. + */ + public function getDefaultDriver(): string + { + return config('queue.default'); + } + + /** + * Set the name of the default queue connection. + */ + public function setDefaultDriver(string $name): void + { + config()->set('queue.default', $name); + } + + /** + * Get the full name for the given connection. + */ + public function getName(?string $connection = null): string + { + return $connection ?: $this->getDefaultDriver(); + } + + /** + * Get the container instance used by the manager. + */ + public function getContainer(): ContainerInterface + { + return $this->container; + } + + /** + * Set the container instance used by the manager. + */ + public function setContainer(ContainerInterface $container) + { + $this->container = $container; + + foreach ($this->connections as $connection) { + $connection->setContainer($container); + } + + return $this; + } + + /** + * Dynamically pass calls to the default connection. + */ + public function __call(string $method, array $parameters = []): mixed + { + return $this->connection()->$method(...$parameters); + } +} diff --git a/src/Queue.php b/src/Queue.php new file mode 100644 index 0000000..d062cab --- /dev/null +++ b/src/Queue.php @@ -0,0 +1,404 @@ +push($job, $data, $queue); + } + + /** + * Push a new job onto a specific queue after (n) seconds. + */ + public function laterOn(string $queue, DateTimeInterface|DateInterval|int $delay, string|Job $job, mixed $data = ''): mixed + { + return $this->later($delay, $job, $data, $queue); + } + + /** + * Push an array of jobs onto the queue. + * + * @param array $jobs + * + * @return void + */ + public function bulk(array $jobs, mixed $data = '', ?string $queue = null) + { + foreach ($jobs as $job) { + $this->push($job, $data, $queue); + } + } + + /** + * Create a payload string from the given job and data. + * + * + * @throws InvalidPayloadException + */ + protected function createPayload(string|object $job, string $queue, mixed $data = '', DateTimeInterface|DateInterval|int|null $delay = null): ?string + { + if ($job instanceof Closure) { + $job = CallQueuedClosure::create($job); + } + + $value = $this->createPayloadArray($job, $queue, $data); + + $value['delay'] = isset($delay) + ? $this->secondsUntil($delay) + : null; + + $payload = json_encode($value, \JSON_UNESCAPED_UNICODE); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new InvalidPayloadException( + 'Unable to JSON encode payload. Error ('.json_last_error().'): '.json_last_error_msg(), $value + ); + } + + return $payload; + } + + /** + * Create a payload array from the given job and data. + */ + protected function createPayloadArray(string|object $job, string $queue, mixed $data = ''): array + { + return is_object($job) + ? $this->createObjectPayload($job, $queue) + : $this->createStringPayload($job, $queue, $data); + } + + /** + * Create a payload for an object-based queue handler. + * + * @throws RuntimeException + */ + protected function createObjectPayload(object $job, string $queue): array + { + $payload = $this->withCreatePayloadHooks($queue, [ + 'uuid' => (string) Text::uuid(), + 'displayName' => $this->getDisplayName($job), + 'job' => 'BlitzPHP\Queue\CallQueuedHandler@call', + 'maxTries' => $this->getJobTries($job), + 'maxExceptions' => $job->maxExceptions ?? null, + 'failOnTimeout' => $job->failOnTimeout ?? false, + 'backoff' => $this->getJobBackoff($job), + 'timeout' => $job->timeout ?? null, + 'retryUntil' => $this->getJobExpiration($job), + 'deleteWhenMissingModels' => $job->deleteWhenMissingModels ?? false, + 'data' => [ + 'commandName' => $job, + 'command' => $job, + 'batchId' => $job->batchId ?? null, + ], + 'createdAt' => Date::now()->getTimestamp(), + ]); + + try { + $command = $this->jobShouldBeEncrypted($job) && $this->container->bound(EncrypterInterface::class) + ? $this->container->get(EncrypterInterface::class)->encrypt(serialize(clone $job)) + : serialize(clone $job); + } catch (Throwable $e) { + throw new RuntimeException( + sprintf('Failed to serialize job of type [%s]: %s', get_class($job), $e->getMessage()), + 0, + $e + ); + } + + return array_merge($payload, [ + 'data' => array_merge($payload['data'], [ + 'commandName' => get_class($job), + 'command' => $command, + ]), + ]); + } + + /** + * Get the display name for the given job. + */ + protected function getDisplayName(object $job): string + { + return method_exists($job, 'displayName') + ? $job->displayName() + : get_class($job); + } + + /** + * Get the maximum number of attempts for an object-based queue handler. + */ + public function getJobTries(object $job): mixed + { + $tries = $job->tries ?? null; + + if (method_exists($job, 'tries')) { + $tries = $job->tries(); + } + + return $tries; + } + + /** + * Get the backoff for an object-based queue handler. + */ + public function getJobBackoff(object $job): mixed + { + $backoff = null; + + if (method_exists($job, 'backoff')) { + $backoff = $job->backoff(); + } else if (property_exists($job, 'backoff')) { + $backoff = $job->backoff ?? null; + } + + if (is_null($backoff)) { + return null; + } + + return Collection::wrap($backoff) + ->map(fn ($backoff) => $backoff instanceof DateTimeInterface ? $this->secondsUntil($backoff) : $backoff) + ->implode(','); + } + + /** + * Get the expiration timestamp for an object-based queue handler. + */ + public function getJobExpiration(object $job): mixed + { + if (! method_exists($job, 'retryUntil') && ! isset($job->retryUntil)) { + return null; + } + + $expiration = $job->retryUntil ?? $job->retryUntil(); + + return $expiration instanceof DateTimeInterface + ? $expiration->getTimestamp() + : $expiration; + } + + /** + * Determine if the job should be encrypted. + */ + protected function jobShouldBeEncrypted(object $job): bool + { + return isset($job->shouldBeEncrypted) && $job->shouldBeEncrypted; + } + + /** + * Create a typical, string based queue payload array. + */ + protected function createStringPayload(string $job, string $queue, mixed $data): array + { + return $this->withCreatePayloadHooks($queue, [ + 'uuid' => (string) Text::uuid(), + 'displayName' => is_string($job) ? explode('@', $job)[0] : null, + 'job' => $job, + 'maxTries' => null, + 'maxExceptions' => null, + 'failOnTimeout' => false, + 'backoff' => null, + 'timeout' => null, + 'data' => $data, + 'createdAt' => Date::now()->getTimestamp(), + ]); + } + + /** + * Register a callback to be executed when creating job payloads. + */ + public static function createPayloadUsing(?callable $callback = null): void + { + if (is_null($callback)) { + static::$createPayloadCallbacks = []; + } else { + static::$createPayloadCallbacks[] = $callback; + } + } + + /** + * Create the given payload using any registered payload hooks. + */ + protected function withCreatePayloadHooks(string $queue, array $payload): array + { + if (! empty(static::$createPayloadCallbacks)) { + foreach (static::$createPayloadCallbacks as $callback) { + $payload = array_merge($payload, $callback($this->getConnectionName(), $queue, $payload)); + } + } + + return $payload; + } + + /** + * Enqueue a job using the given callback. + */ + protected function enqueueUsing(string|object $job, string $payload, ?string $queue, DateTimeInterface|DateInterval|int|null $delay, callable $callback): mixed + { + /* + if ($this->shouldDispatchAfterCommit($job) && $this->container->bound('db.transactions')) { + if ($job->shouldBeUnique) { + $this->container->make('db.transactions')->addCallbackForRollback( + function () use ($job) { + (new UniqueLock($this->container->make(Cache::class)))->release($job); + } + ); + } + + return $this->container->make('db.transactions')->addCallback( + function () use ($queue, $job, $payload, $delay, $callback) { + $this->raiseJobQueueingEvent($queue, $job, $payload, $delay); + + return tap($callback($payload, $queue, $delay), function ($jobId) use ($queue, $job, $payload, $delay) { + $this->raiseJobQueuedEvent($queue, $jobId, $job, $payload, $delay); + }); + } + ); + } + */ + + $this->raiseJobQueueingEvent($queue, $job, $payload, $delay); + + return tap($callback($payload, $queue, $delay), function ($jobId) use ($queue, $job, $payload, $delay) { + $this->raiseJobQueuedEvent($queue, $jobId, $job, $payload, $delay); + }); + } + + /** + * Determine if the job should be dispatched after all database transactions have committed. + */ + protected function shouldDispatchAfterCommit(string|object $job): bool + { + if (! $job instanceof Closure && is_object($job) && isset($job->afterCommit)) { + return $job->afterCommit; + } + + return $this->dispatchAfterCommit ?? false; + } + + /** + * Raise the job queueing event. + */ + protected function raiseJobQueueingEvent(string $queue, string|object $job, string $payload, DateTimeInterface|DateInterval|int|null $delay): void + { + if ($this->container->bound(EventManagerInterface::class)) { + $delay = ! is_null($delay) ? $this->secondsUntil($delay) : $delay; + + $this->container->get(QueueEventManager::class)->jobQueueing($this->connectionName, $queue, $job, $payload, $delay); + } + } + + /** + * Raise the job queued event. + */ + protected function raiseJobQueuedEvent(?string $queue, string|int|null $jobId, string|object $job, string $payload, DateTimeInterface|DateInterval|int|null $delay) + { + if ($this->container->bound(EventManagerInterface::class)) { + $delay = ! is_null($delay) ? $this->secondsUntil($delay) : $delay; + + $this->container->get(QueueEventManager::class)->jobQueued($this->connectionName, $queue, $jobId, $job, $payload, $delay); + } + } + + /** + * Get the connection name for the queue. + */ + public function getConnectionName(): string + { + return $this->connectionName; + } + + /** + * Set the connection name for the queue. + */ + public function setConnectionName(string $name): self + { + $this->connectionName = $name; + + return $this; + } + + /** + * Get the queue configuration array. + */ + public function getConfig(): array + { + return $this->config; + } + + /** + * Set the queue configuration array. + */ + public function setConfig(array $config): self + { + $this->config = $config; + + return $this; + } + + /** + * Get the container instance being used by the connection. + */ + public function getContainer(): ContainerInterface + { + return $this->container; + } + + /** + * Set the IoC container instance. + */ + public function setContainer(ContainerInterface $container): void + { + $this->container = $container; + } +} diff --git a/src/Worker.php b/src/Worker.php new file mode 100644 index 0000000..6c1d9cb --- /dev/null +++ b/src/Worker.php @@ -0,0 +1,759 @@ +exceptions = $exceptions; + $this->isDownForMaintenance = $isDownForMaintenance; + $this->resetScope = $resetScope; + } + + /** + * Listen to the given queue in a loop. + */ + public function daemon(string $connectionName, string $queue, WorkerOptions $options): int + { + if ($supportsAsyncSignals = $this->supportsAsyncSignals()) { + $this->listenForSignals(); + } + + $lastRestart = $this->getTimestampOfLastQueueRestart(); + + [$startTime, $jobsProcessed] = [hrtime(true) / 1e9, 0]; + + $this->raiseWorkerStartingEvent($connectionName, $queue, $options); + + while (true) { + // Before reserving any jobs, we will make sure this queue is not paused and + // if it is we will just pause this worker for a given amount of time and + // make sure we do not need to kill this worker process off completely. + if (! $this->daemonShouldRun($options, $connectionName, $queue)) { + [$status, $reason] = $this->pauseWorker($options, $lastRestart); + + if (! is_null($status)) { + return $this->stop($status, $options, $reason); + } + + continue; + } + + if (isset($this->resetScope)) { + ($this->resetScope)(); + } + + // First, we will attempt to get the next job off of the queue. We will also + // register the timeout handler and reset the alarm for this job so it is + // not stuck in a frozen state forever. Then, we can fire off this job. + $job = $this->getNextJob( + $this->manager->connection($connectionName), $queue + ); + + if ($supportsAsyncSignals) { + $this->registerTimeoutHandler($job, $options); + } + + // If the daemon should run (not in maintenance mode, etc.), then we can run + // fire off this job for processing. Otherwise, we will need to sleep the + // worker so no more jobs are processed until they should be processed. + if ($job) { + $jobsProcessed++; + + $this->runJob($job, $connectionName, $options); + + if ($options->rest > 0) { + $this->sleep($options->rest); + } + } else { + $this->sleep($options->sleep); + } + + if ($supportsAsyncSignals) { + $this->resetTimeoutHandler(); + } + + // Finally, we will check to see if we have exceeded our memory limits or if + // the queue should restart based on other indications. If so, we'll stop + // this worker and let whatever is "monitoring" it restart the process. + [$status, $reason] = $this->stopIfNecessary( + $options, $lastRestart, $startTime, $jobsProcessed, $job + ); + + if (! is_null($status)) { + return $this->stop($status, $options, $reason); + } + } + } + + /** + * Register the worker timeout handler. + */ + protected function registerTimeoutHandler(Job $job, WorkerOptions $options): void + { + // We will register a signal handler for the alarm signal so that we can kill this + // process if it is running too long because it has frozen. This uses the async + // signals supported in recent versions of PHP to accomplish it conveniently. + pcntl_signal(SIGALRM, function () use ($job, $options) { + if ($job) { + $this->markJobAsFailedIfWillExceedMaxAttempts( + $job->getConnectionName(), $job, (int) $options->maxTries, $e = $this->timeoutExceededException($job) + ); + + $this->markJobAsFailedIfWillExceedMaxExceptions( + $job->getConnectionName(), $job, $e + ); + + $this->markJobAsFailedIfItShouldFailOnTimeout( + $job->getConnectionName(), $job, $e + ); + + $this->events->jobTimeout($job->getConnectionName(), $job->getQueue(), $job); + } + + $this->kill(static::EXIT_ERROR, $options, WorkerStopReason::TimedOut); + }, true); + + pcntl_alarm( + max($this->timeoutForJob($job, $options), 0) + ); + } + + /** + * Reset the worker timeout handler. + */ + protected function resetTimeoutHandler(): void + { + pcntl_alarm(0); + } + + /** + * Get the appropriate timeout for the given job. + */ + protected function timeoutForJob(Job $job, WorkerOptions $options): int + { + return $job && ! is_null($job->timeout()) ? $job->timeout() : $options->timeout; + } + + /** + * Determine if the daemon should process on this iteration. + */ + protected function daemonShouldRun(WorkerOptions $options, string $connectionName, string $queue): bool + { + return ! (($this->isDownForMaintenance)() && ! $options->force) || + $this->paused; + } + + /** + * Pause the worker for the current loop. + */ + protected function pauseWorker(WorkerOptions $options, int $lastRestart): ?array + { + $this->sleep($options->sleep > 0 ? $options->sleep : 1); + + return $this->stopIfNecessary($options, $lastRestart); + } + + /** + * Determine the exit code to stop the process if necessary. + */ + protected function stopIfNecessary(WorkerOptions $options, int $lastRestart, int $startTime = 0, int $jobsProcessed = 0, mixed $job = null): ?array + { + return match (true) { + $this->lostConnection => [static::EXIT_SUCCESS, WorkerStopReason::LostConnection], + $this->shouldQuit => [static::EXIT_SUCCESS, WorkerStopReason::Interrupted], + $this->memoryExceeded($options->memory) => [static::$memoryExceededExitCode ?? static::EXIT_MEMORY_LIMIT, WorkerStopReason::MaxMemoryExceeded], + $this->queueShouldRestart($lastRestart) => [static::EXIT_SUCCESS, WorkerStopReason::ReceivedRestartSignal], + $options->stopWhenEmpty && is_null($job) => [static::EXIT_SUCCESS, WorkerStopReason::QueueEmpty], + $options->maxTime && hrtime(true) / 1e9 - $startTime >= $options->maxTime => [static::EXIT_SUCCESS, WorkerStopReason::MaxTimeExceeded], + $options->maxJobs && $jobsProcessed >= $options->maxJobs => [static::EXIT_SUCCESS, WorkerStopReason::MaxJobsExceeded], + default => null + }; + } + + /** + * Process the next job on the queue. + */ + public function runNextJob(string $connectionName, string $queue, WorkerOptions $options): void + { + $job = $this->getNextJob( + $this->manager->connection($connectionName), $queue + ); + + // If we're able to pull a job off of the stack, we will process it and then return + // from this method. If there is no job on the queue, we will "sleep" the worker + // for the specified number of seconds, then keep processing jobs after sleep. + if ($job) { + return $this->runJob($job, $connectionName, $options); + } + + $this->sleep($options->sleep); + } + + /** + * Get the next job from the queue connection. + */ + protected function getNextJob(Queue $connection, string $queue): ?Job + { + $popJobCallback = function ($queue, $index = 0) use ($connection) { + return $connection->pop($queue, $index); + }; + + $this->raiseBeforeJobPopEvent($connection->getConnectionName(), $queue); + + try { + if (isset(static::$popCallbacks[$this->name ?? ''])) { + if (! is_null($job = (static::$popCallbacks[$this->name ?? ''])($popJobCallback, $queue))) { + $this->raiseAfterJobPopEvent($connection->getConnectionName(), $job); + } + + return $job; + } + + foreach (explode(',', $queue) as $index => $queue) { + if ($this->queuePaused($connection->getConnectionName(), $queue)) { + continue; + } + + if (! is_null($job = $popJobCallback($queue, $index))) { + $this->raiseAfterJobPopEvent($connection->getConnectionName(), $job); + + return $job; + } + } + } catch (Throwable $e) { + logger()->error($e->getMessage()); + // $this->exceptions->report($e); + + $this->stopWorkerIfLostConnection($e); + + $this->sleep(1); + } + + return null; + } + + /** + * Determine if a given connection and queue is paused. + */ + protected function queuePaused(string $connectionName, string $queue): bool + { + if (! static::$pausable) { + return false; + } + + return $this->cache && $this->manager->isPaused($connectionName, $queue); + } + + /** + * Process the given job. + */ + protected function runJob(Job $job, string $connectionName, WorkerOptions $options): void + { + try { + return $this->process($connectionName, $job, $options); + } catch (Throwable $e) { + if (static::$reportJobExceptions) { + logger()->error($e->getMessage()); + // $this->exceptions->report($e); + } + + $this->stopWorkerIfLostConnection($e); + } + } + + /** + * Stop the worker if we have lost connection to a database. + */ + protected function stopWorkerIfLostConnection(Throwable $e): void + { + /* + if ($this->causedByLostConnection($e)) { + $this->lostConnection = true; + } + */ + } + + /** + * Process the given job from the queue. + * + * @throws Throwable + */ + public function process(string $connectionName, Job $job, WorkerOptions $options): void + { + try { + // First we will raise the before job event and determine if the job has already run + // over its maximum attempt limits, which could primarily happen when this job is + // continually timing out and not actually throwing any exceptions from itself. + $this->raiseBeforeJobEvent($connectionName, $job); + + $this->markJobAsFailedIfAlreadyExceedsMaxAttempts( + $connectionName, $job, (int) $options->maxTries + ); + + if ($job->isDeleted()) { + return $this->raiseAfterJobEvent($connectionName, $job); + } + + // Here we will fire off the job and let it process. We will catch any exceptions, so + // they can be reported to the developer's logs, etc. Once the job is finished the + // proper events will be fired to let any listeners know this job has completed. + $job->fire(); + + $this->raiseAfterJobEvent($connectionName, $job); + } catch (Throwable $e) { + $exceptionOccurred = $e; + + $this->handleJobException($connectionName, $job, $options, $e); + } finally { + $this->events->jobAttempted($connectionName, $job, $exceptionOccurred ?? null); + } + } + + /** + * Handle an exception that occurred while the job was running. + * + * @throws Throwable + */ + protected function handleJobException(string $connectionName, Job $job, WorkerOptions $options, Throwable $e): void + { + try { + // First, we will go ahead and mark the job as failed if it will exceed the maximum + // attempts it is allowed to run the next time we process it. If so we will just + // go ahead and mark it as failed now so we do not have to release this again. + if (! $job->hasFailed()) { + $this->markJobAsFailedIfWillExceedMaxAttempts( + $connectionName, $job, (int) $options->maxTries, $e + ); + + $this->markJobAsFailedIfWillExceedMaxExceptions( + $connectionName, $job, $e + ); + } + + $this->raiseExceptionOccurredJobEvent( + $connectionName, $job, $e + ); + } finally { + // If we catch an exception, we will attempt to release the job back onto the queue + // so it is not lost entirely. This'll let the job be retried at a later time by + // another listener (or this same one). We will re-throw this exception after. + if (! $job->isDeleted() && ! $job->isReleased() && ! $job->hasFailed()) { + $backoff = $this->calculateBackoff($job, $options); + + $job->release($backoff); + + $this->events->jobReleasedAfterException($connectionName, $job, $backoff); + } + } + + throw $e; + } + + /** + * Mark the given job as failed if it has exceeded the maximum allowed attempts. + * + * This will likely be because the job previously exceeded a timeout. + * + * @throws Throwable + */ + protected function markJobAsFailedIfAlreadyExceedsMaxAttempts(string $connectionName, Job $job, int $maxTries): void + { + $maxTries = ! is_null($job->maxTries()) ? $job->maxTries() : $maxTries; + + $retryUntil = $job->retryUntil(); + + if ($retryUntil && Date::now()->getTimestamp() <= $retryUntil) { + return; + } + + if (! $retryUntil && ($maxTries === 0 || $job->attempts() <= $maxTries)) { + return; + } + + $this->failJob($job, $e = $this->maxAttemptsExceededException($job)); + + throw $e; + } + + /** + * Mark the given job as failed if it has exceeded the maximum allowed attempts. + */ + protected function markJobAsFailedIfWillExceedMaxAttempts(string $connectionName, Job $job, int $maxTries, Throwable $e): void + { + $maxTries = ! is_null($job->maxTries()) ? $job->maxTries() : $maxTries; + + if ($job->retryUntil() && $job->retryUntil() <= Date::now()->getTimestamp()) { + $this->failJob($job, $e); + } + + if (! $job->retryUntil() && $maxTries > 0 && $job->attempts() >= $maxTries) { + $this->failJob($job, $e); + } + } + + /** + * Mark the given job as failed if it has exceeded the maximum allowed attempts. + */ + protected function markJobAsFailedIfWillExceedMaxExceptions(string $connectionName, Job $job, Throwable $e): void + { + if (! $this->cache || is_null($uuid = $job->uuid()) || + is_null($maxExceptions = $job->maxExceptions())) { + return; + } + + if (! $this->cache->get('job-exceptions:'.$uuid)) { + $this->cache->set('job-exceptions:'.$uuid, 0, Date::now()->addDay()->getTimestamp()); + } + + if ($maxExceptions <= $this->cache->increment('job-exceptions:'.$uuid)) { + $this->cache->delete('job-exceptions:'.$uuid); + + $this->failJob($job, $e); + } + } + + /** + * Mark the given job as failed if it should fail on timeouts. + */ + protected function markJobAsFailedIfItShouldFailOnTimeout(string $connectionName, Job $job, Throwable $e): void + { + if (method_exists($job, 'shouldFailOnTimeout') ? $job->shouldFailOnTimeout() : false) { + $this->failJob($job, $e); + } + } + + /** + * Mark the given job as failed and raise the relevant event. + */ + protected function failJob(Job $job, Throwable $e): void + { + $job->fail($e); + } + + /** + * Calculate the backoff for the given job. + */ + protected function calculateBackoff(Job $job, WorkerOptions $options): int + { + $backoff = explode( + ',', + method_exists($job, 'backoff') && ! is_null($job->backoff()) + ? $job->backoff() + : $options->backoff + ); + + return (int) ($backoff[$job->attempts() - 1] ?? last($backoff)); + } + + /** + * Raise an event indicating the worker is starting. + */ + protected function raiseWorkerStartingEvent(string $connectionName, string $queue, WorkerOptions $options): void + { + $this->events->workerStarting($connectionName, $queue, $options); + } + + /** + * Raise an event indicating a job is being popped from the queue. + */ + protected function raiseBeforeJobPopEvent(string $connectionName, ?string $queue = null): void + { + $this->events->jobPopping($connectionName, $queue); + } + + /** + * Raise an event indicating a job has been popped from the queue. + */ + protected function raiseAfterJobPopEvent(string $connectionName, ?Job $job): void + { + $this->events->jobPopped($connectionName, $job); + } + + /** + * Raise an event indicating a job is being processed. + */ + protected function raiseBeforeJobEvent(string $connectionName, Job $job): void + { + $this->events->jobProcessing($connectionName, $job); + } + + /** + * Raise an event indicating a job has been processed. + */ + protected function raiseAfterJobEvent(string $connectionName, Job $job): void + { + $this->events->jobProcessed($connectionName, $job); + } + + /** + * Raise the exception occurred queue job event. + */ + protected function raiseExceptionOccurredJobEvent(string $connectionName, Job $job, Throwable $e): void + { + $this->events->jobExceptionOccured($connectionName, $job, $e); + } + + /** + * Determine if the queue worker should restart. + */ + protected function queueShouldRestart(?int $lastRestart): bool + { + if (! static::$restartable) { + return false; + } + + return $this->getTimestampOfLastQueueRestart() != $lastRestart; + } + + /** + * Get the last queue restart timestamp, or null. + */ + protected function getTimestampOfLastQueueRestart(): ?int + { + if (! static::$restartable) { + return null; + } + + if ($this->cache) { + return (int) $this->cache->get('blitzphp:queue:restart'); + } + + return null; + } + + /** + * Enable async signals for the process. + */ + protected function listenForSignals(): void + { + pcntl_async_signals(true); + + pcntl_signal(SIGQUIT, fn () => $this->shouldQuit = true); + pcntl_signal(SIGTERM, fn () => $this->shouldQuit = true); + pcntl_signal(SIGINT, fn () => $this->shouldQuit = true); + pcntl_signal(SIGUSR2, fn () => $this->paused = true); + pcntl_signal(SIGCONT, fn () => $this->paused = false); + } + + /** + * Determine if "async" signals are supported. + */ + protected function supportsAsyncSignals(): bool + { + return extension_loaded('pcntl'); + } + + /** + * Determine if the memory limit has been exceeded. + */ + public function memoryExceeded(int $memoryLimit): bool + { + return ((int) $memoryLimit) > 0 && (memory_get_usage(true) / 1024 / 1024) >= ((int) $memoryLimit); + } + + /** + * Stop listening and bail out of the script. + */ + public function stop(int $status = 0, ?WorkerOptions $options = null, ?WorkerStopReason $reason = null): int + { + $this->events->workerStopping($this->manager->getName(), $status, $options, $reason); + + return $status; + } + + /** + * Kill the process. + */ + public function kill(int $status = 0, ?WorkerOptions $options = null, ?WorkerStopReason $reason = null): never + { + $status = $this->stop($status, $options, $reason); + + if (extension_loaded('posix')) { + posix_kill(getmypid(), SIGKILL); + } + + exit($status); + } + + /** + * Create an instance of MaxAttemptsExceededException. + */ + protected function maxAttemptsExceededException(Job $job): MaxAttemptsExceededException + { + return MaxAttemptsExceededException::forJob($job); + } + + /** + * Create an instance of TimeoutExceededException. + */ + protected function timeoutExceededException(Job $job): TimeoutExceededException + { + return TimeoutExceededException::forJob($job); + } + + /** + * Sleep the script for a given number of seconds. + */ + public function sleep(int|float $seconds): void + { + if ($seconds < 1) { + usleep($seconds * 1_000_000); + } else { + sleep($seconds); + } + } + + /** + * Set the cache repository implementation. + */ + public function setCache(Cache $cache): self + { + $this->cache = $cache; + + return $this; + } + + /** + * Set the name of the worker. + */ + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + /** + * Register a callback to be executed to pick jobs. + */ + public static function popUsing(string $workerName, callable $callback): void + { + if (is_null($callback)) { + unset(static::$popCallbacks[$workerName]); + } else { + static::$popCallbacks[$workerName] = $callback; + } + } + + /** + * Get the queue manager instance. + */ + public function getManager(): Manager + { + return $this->manager; + } + + /** + * Set the queue manager instance. + */ + public function setManager(Manager $manager): void + { + $this->manager = $manager; + } +} diff --git a/src/WorkerOptions.php b/src/WorkerOptions.php new file mode 100644 index 0000000..170ca6c --- /dev/null +++ b/src/WorkerOptions.php @@ -0,0 +1,36 @@ + Date: Thu, 27 Aug 2026 18:45:10 +0100 Subject: [PATCH 2/5] =?UTF-8?q?chore:=20architecture=20de=20base=20(ready-?= =?UTF-8?q?to-use)=20effectu=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 4 +- src/CallQueuedClosure.php | 11 +- src/CallQueuedHandler.php | 386 ++++++++++++++++++ src/Commands/Work.php | 367 +++++++++++++++++ src/Config/Services.php | 108 +++++ src/Config/queue.php | 14 +- src/DTO/Config.php | 114 ++++++ src/{ => DTO}/WorkerOptions.php | 2 +- .../2026-08-26-061438_CreateQueueTables.php | 42 ++ src/Drivers/ConnectorInterface.php | 2 +- src/Drivers/DatabaseDriver.php | 189 +++------ src/Drivers/FailoverDriver.php | 173 ++++++++ src/Drivers/NullDriver.php | 118 ++++++ src/Drivers/SyncDriver.php | 230 +++++++++++ src/Events/QueueEvent.php | 88 ++-- src/Events/QueueEventManager.php | 49 ++- src/Failed/CountableFailedJobProvider.php | 10 + src/Failed/DatabaseFailedJobProvider.php | 133 ++++++ src/Failed/DatabaseUuidFailedJobProvider.php | 136 ++++++ src/Failed/FailedJobProviderInterface.php | 43 ++ src/Failed/FileFailedJobProvider.php | 181 ++++++++ src/Failed/NullFailedJobProvider.php | 63 +++ src/Failed/PrunableFailedJobProvider.php | 12 + src/Job.php | 33 ++ src/Jobs/DatabaseJob.php | 4 +- src/Jobs/InspectedJob.php | 2 +- src/Jobs/Job.php | 1 + src/Jobs/SyncJob.php | 68 +++ src/Manager.php | 163 +++----- src/Models/JobModel.php | 208 ++++++++++ src/Providers/QueueProvider.php | 25 ++ src/Queue.php | 45 +- src/Traits/Dispatchable.php | 59 +++ src/Traits/InteractsWithQueue.php | 254 ++++++++++++ .../SerializesAndRestoresModelIdentifiers.php | 120 ++++++ src/Traits/SerializesModels.php | 101 +++++ src/Worker.php | 49 ++- 37 files changed, 3246 insertions(+), 361 deletions(-) create mode 100644 src/CallQueuedHandler.php create mode 100644 src/Commands/Work.php create mode 100644 src/Config/Services.php create mode 100644 src/DTO/Config.php rename src/{ => DTO}/WorkerOptions.php (97%) create mode 100644 src/Database/Migrations/2026-08-26-061438_CreateQueueTables.php create mode 100644 src/Drivers/FailoverDriver.php create mode 100644 src/Drivers/NullDriver.php create mode 100644 src/Drivers/SyncDriver.php create mode 100644 src/Failed/CountableFailedJobProvider.php create mode 100644 src/Failed/DatabaseFailedJobProvider.php create mode 100644 src/Failed/DatabaseUuidFailedJobProvider.php create mode 100644 src/Failed/FailedJobProviderInterface.php create mode 100644 src/Failed/FileFailedJobProvider.php create mode 100644 src/Failed/NullFailedJobProvider.php create mode 100644 src/Failed/PrunableFailedJobProvider.php create mode 100644 src/Job.php create mode 100644 src/Jobs/SyncJob.php create mode 100644 src/Models/JobModel.php create mode 100644 src/Providers/QueueProvider.php create mode 100644 src/Traits/Dispatchable.php create mode 100644 src/Traits/InteractsWithQueue.php create mode 100644 src/Traits/SerializesAndRestoresModelIdentifiers.php create mode 100644 src/Traits/SerializesModels.php diff --git a/composer.json b/composer.json index aeccb99..4274556 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,8 @@ { "name": "blitz-php/queue", "description": "Gestionnaire de file d'attente pour BlitzPHP", - "keywords": ["blitz-php", "blitz php", "queue", "database", "redis", "predis" ], - "homepage": "https://github.com/blitz-php/tasks", + "keywords": ["blitz-php", "blitz php", "queue", "worker", "database", "redis", "predis", "file d'attente" ], + "homepage": "https://github.com/blitz-php/queue", "license": "MIT", "type": "library", "authors": [ diff --git a/src/CallQueuedClosure.php b/src/CallQueuedClosure.php index bc29cc9..ffd60ed 100644 --- a/src/CallQueuedClosure.php +++ b/src/CallQueuedClosure.php @@ -3,18 +3,17 @@ namespace BlitzPHP\Queue; use BlitzPHP\Contracts\Container\ContainerInterface; +use BlitzPHP\Queue\Traits\Dispatchable; +use BlitzPHP\Queue\Traits\InteractsWithQueue; +use BlitzPHP\Queue\Traits\SerializesModels; use Closure; -use Illuminate\Bus\Batchable; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Bus\Dispatchable; use Laravel\SerializableClosure\SerializableClosure; use ReflectionFunction; use Throwable; -class CallQueuedClosure implements ShouldQueue +class CallQueuedClosure { - use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, SerializesModels; /** * The serializable Closure instance. diff --git a/src/CallQueuedHandler.php b/src/CallQueuedHandler.php new file mode 100644 index 0000000..486a70e --- /dev/null +++ b/src/CallQueuedHandler.php @@ -0,0 +1,386 @@ +getCommand($data); + + // Vérifier si c'est une classe incomplète + if ($command instanceof \__PHP_Incomplete_Class) { + throw new Exception('Job is incomplete class: ' . json_encode($command)); + } + + // Injecter les dépendances + $command = $this->setJobInstanceIfNecessary($job, $command); + + // Si le job a déjà été supprimé, on arrête + if ($job->isDeleted()) { + return; + } + + // Exécuter le job + $this->executeCommand($command); + + // Si le job n'a pas été supprimé ou relâché, on le supprime + if (!$job->isDeletedOrReleased()) { + $job->delete(); + } + + } catch (ModelNotFoundException $e) { + // Gérer le cas où un modèle n'est pas trouvé + $this->handleModelNotFound($job, $e); + } catch (Throwable $e) { + // Gérer les autres exceptions + $this->handleException($job, $data, $e); + throw $e; + } + } + + /** + * Récupère la commande (le job utilisateur) depuis les données + */ + protected function getCommand(array $data): mixed + { + if (!isset($data['command'])) { + throw new RuntimeException('Job data missing "command" key.'); + } + + // Si c'est déjà un objet (pour les jobs sync) + if (is_object($data['command']) && !is_string($data['command'])) { + return $data['command']; + } + + // Si c'est une chaîne sérialisée + if (is_string($data['command'])) { + // Vérifier si c'est du sérialisé PHP + if (str_starts_with($data['command'], 'O:')) { + $command = unserialize($data['command']); + if ($command !== false) { + return $command; + } + } + + // Essayer de décrypter si c'est encrypté + if ($this->container->bound(EncrypterInterface::class)) { + try { + $decrypted = $this->container->get(EncrypterInterface::class)->decrypt($data['command']); + $command = unserialize($decrypted); + if ($command !== false) { + return $command; + } + } catch (Throwable $e) { + // Ignorer l'erreur de décryptage + } + } + } + + throw new RuntimeException('Unable to extract job payload.'); + } + + /** + * Set the job instance of the given class if necessary. + */ + protected function setJobInstanceIfNecessary(Job $job, mixed $instance): mixed + { + // Vérifier si la classe utilise le trait InteractsWithQueue + if (is_object($instance) && $this->usesInteractsWithQueue($instance)) { + if (method_exists($instance, 'setJob')) { + $instance->setJob($job); + } + } + + // Si c'est un CallQueuedClosure, on lui passe le container + if ($instance instanceof CallQueuedClosure) { + // Déjà géré dans executeCommand + } + + return $instance; + } + + /** + * Vérifie si la classe utilise le trait InteractsWithQueue + */ + protected function usesInteractsWithQueue(object $instance): bool + { + $traits = Helpers::classUsesRecursive($instance); + + return isset($traits[Traits\InteractsWithQueue::class]) || + isset($traits['BlitzPHP\\Queue\\Traits\\InteractsWithQueue']); + } + + /** + * Exécute la commande (le job) + */ + protected function executeCommand(object $command): void + { + // Si c'est un CallQueuedClosure + if ($command instanceof CallQueuedClosure) { + $command->handle($this->container); + return; + } + + // Si le job a une méthode handle() (cas standard) + if (method_exists($command, 'handle')) { + $this->container->call([$command, 'handle']); + return; + } + + // Si c'est callable (__invoke) + if (is_callable($command)) { + $this->container->call($command); + return; + } + + throw new RuntimeException( + 'Job does not have a handle() method and is not callable: ' . get_class($command) + ); + } + + /** + * Gère une exception pendant l'exécution du job + */ + protected function handleException(Job $job, array $data, Throwable $e): void + { + // Si le job a déjà été marqué comme échoué, on arrête + if ($job->hasFailed()) { + return; + } + + // Récupérer la commande pour les métadonnées + $command = null; + try { + $command = $this->getCommand($data); + } catch (Throwable $parseError) { + // Ignorer l'erreur de parsing + } + + // Vérifier si le job a dépassé le nombre max de tentatives + $maxTries = $this->getMaxTries($command); + $attempts = $job->attempts(); + + if ($attempts >= $maxTries) { + // Marquer comme échoué + $job->markAsFailed(); + + // Appeler la méthode failed du job si elle existe + if ($command && method_exists($command, 'failed')) { + try { + $command->failed($e); + } catch (Throwable $failedError) { + // Ignorer les erreurs dans failed() + } + } + + // Logger l'échec + logger()->error('Job failed after max attempts', [ + 'job' => $this->getJobName($command, $data), + 'attempts' => $attempts, + 'max_tries' => $maxTries, + 'error' => $e->getMessage(), + 'job_id' => $job->getJobId(), + 'queue' => $job->getQueue(), + ]); + + // Enregistrer dans le provider de jobs échoués + $this->logFailedJob($job, $e); + + // Supprimer le job + $job->delete(); + + throw new MaxAttemptsExceededException( + 'Job failed after ' . $maxTries . ' attempts: ' . $e->getMessage(), + 0, + $e + ); + } + + // Calculer le backoff + $backoff = $this->calculateBackoff($command, $attempts); + + // Relâcher le job avec backoff + $job->release($backoff); + + logger()->warning('Job released for retry', [ + 'job' => $this->getJobName($command, $data), + 'attempts' => $attempts, + 'backoff' => $backoff, + 'error' => $e->getMessage(), + 'job_id' => $job->getJobId(), + 'queue' => $job->getQueue(), + ]); + } + + /** + * Récupère le nombre max de tentatives + */ + protected function getMaxTries(?object $command): int + { + if ($command === null) { + return config('queue.max_tries', 3); + } + + if (method_exists($command, 'maxTries')) { + $maxTries = $command->maxTries(); + if ($maxTries !== null) { + return (int) $maxTries; + } + } + + if (property_exists($command, 'maxTries')) { + return (int) $command->maxTries; + } + + return config('queue.max_tries', 3); + } + + /** + * Calcule le backoff pour le retry + */ + protected function calculateBackoff(?object $command, int $attempts): int + { + $backoff = 60; // Valeur par défaut + + if ($command !== null) { + if (method_exists($command, 'backoff')) { + $backoffValue = $command->backoff(); + if (is_array($backoffValue)) { + $backoff = $backoffValue[$attempts - 1] ?? $backoffValue[0] ?? 60; + } else { + $backoff = (int) $backoffValue; + } + } elseif (property_exists($command, 'backoff')) { + $backoffValue = $command->backoff; + if (is_array($backoffValue)) { + $backoff = $backoffValue[$attempts - 1] ?? $backoffValue[0] ?? 60; + } else { + $backoff = (int) $backoffValue; + } + } + } + + // Si le backoff est 0, on utilise un backoff exponentiel + if ($backoff === 0) { + $backoff = 60 * pow(2, $attempts - 1); + } + + return $backoff; + } + + /** + * Récupère le nom du job pour les logs + */ + protected function getJobName(?object $command, array $data): string + { + if ($command !== null) { + return get_class($command); + } + + return $data['commandName'] ?? $data['displayName'] ?? 'Unknown'; + } + + /** + * Enregistre le job comme échoué + */ + protected function logFailedJob(Job $job, Throwable $e): void + { + try { + $failedProvider = $this->container->get(\BlitzPHP\Queue\Failed\FailedJobProviderInterface::class); + + $failedProvider->log( + $job->getConnectionName(), + $job->getQueue(), + $job->getRawBody(), + $e + ); + } catch (Throwable $logError) { + // Ignorer les erreurs de logging + logger()->error('Failed to log failed job', [ + 'error' => $logError->getMessage(), + 'job_id' => $job->getJobId() + ]); + } + } + + /** + * Gère le cas où un modèle n'est pas trouvé + */ + protected function handleModelNotFound(Job $job, ModelNotFoundException $e): void + { + $payload = $job->payload(); + + // Vérifier si on doit supprimer le job quand les modèles sont manquants + if (isset($payload['deleteWhenMissingModels']) && $payload['deleteWhenMissingModels']) { + $job->delete(); + logger()->warning('Job deleted because model was not found', [ + 'job_id' => $job->getJobId(), + 'queue' => $job->getQueue(), + 'model' => $e->getModel(), + ]); + return; + } + + // Sinon, on marque comme échoué + $job->fail($e); + } + + /** + * Méthode appelée quand le job échoue définitivement + * (appelée par le worker après max attempts) + */ + public function failed(array $data, Throwable $e, string $uuid, ?Job $job = null): void + { + try { + $command = $this->getCommand($data); + + if ($command instanceof \__PHP_Incomplete_Class) { + return; + } + + if ($job !== null) { + $command = $this->setJobInstanceIfNecessary($job, $command); + } + + // Appeler la méthode failed du job si elle existe + if (is_object($command) && method_exists($command, 'failed')) { + $command->failed($e); + } + + logger()->critical('Job permanently failed', [ + 'job' => $this->getJobName($command ?? null, $data), + 'uuid' => $uuid, + 'error' => $e->getMessage(), + ]); + + } catch (Throwable $handledError) { + // Ignorer les erreurs dans failed() + logger()->error('Error in CallQueuedHandler::failed', [ + 'error' => $handledError->getMessage(), + ]); + } + } +} diff --git a/src/Commands/Work.php b/src/Commands/Work.php new file mode 100644 index 0000000..dc3bbe8 --- /dev/null +++ b/src/Commands/Work.php @@ -0,0 +1,367 @@ + 'The name of the queue connection to work', + ]; + + /** @var array Options de la commande */ + protected $options = [ + '--name' => ['The name of the worker', 'default'], + '--queue' => ['The names of the queues to work'], + '--daemon' => ['Run the worker in daemon mode (Deprecated)'], + '--once' => ['Only process the next job on the queue'], + '--stop-when-empty' => ['Stop when the queue is empty'], + '--delay' => ['The number of seconds to delay failed jobs (Deprecated)', 0], + '--backoff' => ['The number of seconds to wait before retrying a job that encountered an uncaught exception', 0], + '--max-jobs' => ['The number of jobs to process before stopping', 0], + '--max-time' => ['The maximum number of seconds the worker should run', 0], + '--force' => ['Force the worker to run even in maintenance mode'], + '--memory' => ['The memory limit in megabytes', 128], + '--sleep' => ['The number of seconds to sleep when no job is available', 3], + '--rest' => ['The number of seconds to rest between jobs', 0], + '--timeout' => ['The number of seconds a child process can run', 60], + '--tries' => ['The number of times to attempt a job before logging it failed', 1], + '--json' => ['Output the queue worker information as JSON'], + ]; + + /** + * The queue worker instance. + */ + protected Worker $worker; + + /** + * The cache store implementation. + */ + protected CacheInterface $cache; + + protected EventManagerInterface $events; + + + /** + * Holds the start time of the last processed job, if any. + */ + protected ?float $latestStartedAt = null; + + /** + * Indicates if the worker's event listeners have been registered. + */ + private static bool $hasRegisteredListeners = false; + + private static ?bool $stty = null; + + /** + * Create a new queue work command. + */ + public function __construct(protected ContainerInterface $container, protected Console $app) + { + parent::__construct($app, $container->get(LoggerInterface::class)); + + $this->worker = service('worker'); + $this->cache = $container->get(CacheInterface::class); + $this->events = $container->get(EventManagerInterface::class); + + BaseHandler::setReservedCharacters(str_replace(':', '', config('cache.reserved_characters'))); + } + + /** + * Execute the console command. + * + * @return int|null + */ + public function execute(array $params) + { + set_time_limit(0); + + if ($this->downForMaintenance() && $this->option('once')) { + return $this->worker->sleep($this->option('sleep')); + } + + // We'll listen to the processed and failed events so we can write information + // to the console as jobs are processed, which will let the developer watch + // which jobs are coming through a queue and be informed on its progress. + $this->listenForEvents(); + + $connection = $this->argument('connection') ?: config('queue.default'); + + // We need to get the right queue for the connection which is set in the queue + // configuration file for the application. We will pull it based on the set + // connection being run for the queue operation currently being executed. + $queue = $this->getQueue($connection); + + if (! $this->outputUsingJson() && static::terminalHasSttyAvailable()) { + $this->info( + sprintf('Processing jobs from the [%s] %s.', $queue, (new Stringable('queue'))->plural(explode(',', $queue))) + ); + } + + return $this->runWorker( + $connection, $queue + ); + } + + /** + * Run the worker instance. + */ + protected function runWorker(string $connection, string $queue): ?int + { + return $this->worker + ->setName($this->option('name')) + ->setCache($this->cache) + ->{$this->option('once') ? 'runNextJob' : 'daemon'}( + $connection, $queue, $this->gatherWorkerOptions() + ); + } + + /** + * Gather all of the queue worker options as a single object. + */ + protected function gatherWorkerOptions(): WorkerOptions + { + return new WorkerOptions( + $this->option('name'), + max($this->option('backoff'), $this->option('delay')), + $this->option('memory'), + $this->option('timeout'), + $this->option('sleep'), + $this->option('tries'), + $this->option('force', false), + $this->option('stop-when-empty', false), + $this->option('max-jobs'), + $this->option('max-time'), + $this->option('rest'), + ); + } + + /** + * Listen for the queue events in order to update the console output. + */ + protected function listenForEvents(): void + { + if (static::$hasRegisteredListeners) { + return; + } + + $this->events->on(QueueEventManager::JOB_PROCESSING, function(QueueEvent $event) { + $this->writeOutput($event->job, 'starting'); + }); + + $this->events->on(QueueEventManager::JOB_PROCESSED, function(QueueEvent $event) { + $this->writeOutput($event->job, 'success'); + }); + + $this->events->on(QueueEventManager::JOB_RELEASED_AFTER_EXCEPTION, function(QueueEvent $event) { + $this->writeOutput($event->job, 'released_after_exception'); + }); + + $this->events->on(QueueEventManager::JOB_FAILED, function(QueueEvent $event) { + $this->writeOutput($event->job, 'failed', $event->exception); + + $this->logFailedJob($event); + }); + + static::$hasRegisteredListeners = true; + } + + /** + * Write the status output for the queue worker for JSON or TTY. + */ + protected function writeOutput(Job $job, string $status, ?Throwable $exception = null): void + { + if ($this->isSilent()) { + return; + } + + $this->outputUsingJson() + ? $this->writeOutputAsJson($job, $status, $exception) + : $this->writeOutputForCli($job, $status); + } + + /** + * Write the status output for the queue worker. + */ + protected function writeOutputForCli(Job $job, string $status): void + { + $isVerbose = $this->option('verbose'); + + $first = sprintf('%s %s %s', + $this->color->comment($this->now()->format('Y-m-d H:i:s')), + $job->resolveName(), + ! $isVerbose ? '' : sprintf('%s %s', + $this->color->comment($job->getJobId()), + $this->color->info($job->getConnectionName() . ' ' . $job->getQueue()) + ) + ); + + if ($status == 'starting') { + $this->latestStartedAt = microtime(true); + + $second = $this->color->warn('RUNNING', ['bold' => 1]); + } else { + $runTime = (microtime(true) - $this->latestStartedAt) * 1000; + $runTime = (float) number_format($runTime, 2, '.', ''); + + $memory = $isVerbose ? round(memory_get_usage(true) / 1024 / 1024, 1).'MB' : ''; + + $second = $this->color->comment("{$runTime} ms".($memory ? " {$memory}" : '') . " "); + $second .= match ($status) { + 'success' => $this->color->ok('DONE', ['bold' => 1]), + 'released_after_exception' => $this->color->warn('FAIL', ['bold' => 1]), + default => $this->color->error('FAIL', ['bold' => 1]), + }; + } + + $this->justify($first, $second); + } + + /** + * Write the status output for the queue worker in JSON format. + */ + protected function writeOutputAsJson(Job $job, $status, ?Throwable $exception = null): void + { + $log = array_filter([ + 'level' => $status === 'starting' || $status === 'success' ? 'info' : 'warning', + 'id' => $job->getJobId(), + 'uuid' => $job->uuid(), + 'connection' => $job->getConnectionName(), + 'queue' => $job->getQueue(), + 'job' => $job->resolveName(), + 'status' => $status, + 'result' => match (true) { + $job->isDeleted() => 'deleted', + $job->isReleased() => 'released', + $job->hasFailed() => 'failed', + default => '', + }, + 'attempts' => $job->attempts(), + 'exception' => $exception ? $exception::class : '', + 'message' => $exception?->getMessage(), + 'timestamp' => $this->now()->format('Y-m-d\TH:i:s.uP'), + ]); + + if ($status === 'starting') { + $this->latestStartedAt = microtime(true); + } else { + $log['duration'] = round(microtime(true) - $this->latestStartedAt, 6); + } + + $this->json($log); + } + + /** + * Get the current date / time. + */ + protected function now(): Date + { + $queueTimezone = config('queue.output_timezone'); + + if ($queueTimezone && $queueTimezone !== config('app.timezone')) { + return Date::now()->setTimezone($queueTimezone); + } + + return Date::now(); + } + + /** + * Store a failed job event. + */ + protected function logFailedJob(QueueEvent $event): void + { + service('queueFailer')->log( + $event->connection, + $event->job->getQueue(), + $event->job->getRawBody(), + $event->exception + ); + } + + /** + * Get the queue name for the worker. + */ + protected function getQueue(string $connection): string + { + return $this->option('queue') ?: config( + "queue.connections.{$connection}.queue", 'default' + ); + } + + /** + * Determine if the worker should run in maintenance mode. + */ + protected function downForMaintenance(): false + { + return $this->option('force') + ? false + : config('app.maintenance.enable', false); // $this->laravel->isDownForMaintenance(); + } + + /** + * Determine if the worker should output using JSON. + */ + protected function outputUsingJson(): bool + { + return filter_var($this->option('json'), FILTER_VALIDATE_BOOLEAN) === true; + } + + /** + * Reset static variables. + */ + public static function flushState(): void + { + static::$hasRegisteredListeners = false; + } + + protected function isSilent(): bool + { + return $this->suppress || !is_cli(); + } + + /** + * @internal + */ + protected static function terminalHasSttyAvailable(): bool + { + if (null !== self::$stty) { + return self::$stty; + } + + // skip check if shell_exec function is disabled + if (!\function_exists('shell_exec')) { + return false; + } + + return self::$stty = (bool) @shell_exec('stty 2> '.('\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null')); + } +} diff --git a/src/Config/Services.php b/src/Config/Services.php new file mode 100644 index 0000000..44d4efb --- /dev/null +++ b/src/Config/Services.php @@ -0,0 +1,108 @@ +get('app.maintenance.enable', false); + }; + + $resetScope = function () { + $logger = static::logger(); + + if (method_exists($logger, 'flushSharedContext')) { + $logger->flushSharedContext(); + } + + if (method_exists($logger, 'withoutContext')) { + $logger->withoutContext(); + } + + $db = static::database(); + + if (method_exists($db, 'getConnections')) { + foreach ($db->getConnections() as $connection) { + // $connection->resetTotalQueryDuration(); + // $connection->allowQueryDurationHandlersToRunAgain(); + } + } + + memory_reset_peak_usage(); + }; + + return static::$instances[Worker::class] = new Worker( + static::queue(), + static::singleton(QueueEventManager::class), + $isDownForMaintenance, + $resetScope, + ); + } + + public static function queueFailer(array $config = [], bool $shared = true): FailedJobProviderInterface + { + if (true === $shared && isset(static::$instances[FailedJobProviderInterface::class])) { + return static::$instances[FailedJobProviderInterface::class]; + } + + $config = $config === [] ? static::config()->get('queue.failed', []) : $config; + $driver = $config['driver'] ?? 'null'; + + return static::$instances[FailedJobProviderInterface::class] =match ($driver) { + 'database' => new DatabaseFailedJobProvider( + static::singleton(ConnectionResolverInterface::class), + $config['database'] ?? 'default', + $config['table'] ?? 'queue_failed_jobs' + ), + 'database-uuids' => new DatabaseUuidFailedJobProvider( + static::singleton(ConnectionResolverInterface::class), + $config['database'] ?? 'default', + $config['table'] ?? 'queue_failed_jobs' + ), + 'file' => new FileFailedJobProvider( + $config['path'] ?? storage_path('logs/failed_jobs.json'), + $config['limit'] ?? 100 + ), + default => new NullFailedJobProvider() + }; + } + +} diff --git a/src/Config/queue.php b/src/Config/queue.php index e5a29c0..163db11 100644 --- a/src/Config/queue.php +++ b/src/Config/queue.php @@ -7,7 +7,7 @@ 'group' => env('queue.database.group', 'default'), 'shared' => true, 'skip_locked' => true, - 'table' => env('queue.database.table', 'jobs'), + 'table' => env('queue.database.table', 'queue_jobs'), ], 'redis' => [ 'driver' => 'redis', @@ -35,10 +35,10 @@ ], 'drivers' => [ - 'database' => \BlitzPHP\Queue\Drivers\Database::class, - 'redis' => \BlitzPHP\Queue\Drivers\Redis::class, - 'predis' => \BlitzPHP\Queue\Drivers\Predis::class, - 'rabbitmq' => \BlitzPHP\Queue\Drivers\RabbitMQ::class, + 'database' => \BlitzPHP\Queue\Drivers\DatabaseDriver::class, + // 'redis' => \BlitzPHP\Queue\Drivers\Redis::class, + // 'predis' => \BlitzPHP\Queue\Drivers\Predis::class, + // 'rabbitmq' => \BlitzPHP\Queue\Drivers\RabbitMQ::class, ], 'keep_failed_jobs' => true, @@ -46,11 +46,11 @@ 'failed' => [ 'driver' => env('queue.failed_driver', 'database-uuids'), 'database' => env('db.connection', 'default'), - 'table' => 'failed_jobs', + 'table' => 'queue_failed_jobs', ], 'batching' => [ 'database' => env('db.connection', 'default'), - 'table' => 'job_batches', + 'table' => 'queue.job_batches', ], ]; diff --git a/src/DTO/Config.php b/src/DTO/Config.php new file mode 100644 index 0000000..dba81ef --- /dev/null +++ b/src/DTO/Config.php @@ -0,0 +1,114 @@ +> $connections Les configurations des connexions + * @param array> $drivers Les drivers disponibles + * @param bool $keep_failed_jobs Garder les jobs échoués + * @param array{driver: string, database: string, table: string} $failed Configuration des jobs échoués + * @param array{database: string, table: string} $batching Configuration du batching + * @param array $raw Données brutes supplémentaires + */ + public function __construct( + public string $default, + public array $connections = [], + public array $drivers = [], + public bool $keep_failed_jobs = true, + public array $failed = [], + public array $batching = [], + private array $raw = [], + ) { + } + + /** + * Crée une instance depuis la configuration automatique + */ + public static function auto(): self + { + return self::fromArray(config('queue', [])); + } + + /** + * Crée une instance depuis un tableau de configuration + */ + public static function fromArray(array $config): self + { + return new self( + default : $config['default'] ?? 'database', + connections : $config['connections'] ?? [], + drivers : $config['drivers'] ?? [], + keep_failed_jobs: $config['keep_failed_jobs'] ?? true, + failed : $config['failed'] ?? [], + batching : $config['batching'] ?? [], + raw : $config, + ); + } + + /** + * Convertit l'objet en tableau + */ + public function toArray(): array + { + return array_merge( + [ + 'default' => $this->default, + 'connections' => $this->connections, + 'drivers' => $this->drivers, + 'keep_failed_jobs' => $this->keep_failed_jobs, + 'failed' => $this->failed, + 'batching' => $this->batching, + ], + $this->raw + ); + } + + /** + * Récupère une connexion spécifique + */ + public function connection(?string $name): array + { + if ($name === null || $name === 'null') { + return ['driver' => 'null']; + } + + if (!isset($this->connections[$name])) { + throw new InvalidArgumentException("The [{$name}] queue connection has not been configured."); + } + + return $this->connections[$name] + ['driver' => $name]; + } + + /** + * @return class-string + */ + public function driver(string $name): string + { + $driver = $this->drivers[$name] ?? null; + + if ($driver === null) { + throw new InvalidArgumentException("Driver [{$name}] not registered."); + } + + if (! is_a($driver, ConnectorInterface::class, true)) { + throw new InvalidArgumentException(); + } + + return $driver; + } + + /** + * Set the name of the default queue connection. + */ + public function setDefaultDriver(string $name): void + { + $this->default = $name; + + config()->set('queue.default', $name); + } +} diff --git a/src/WorkerOptions.php b/src/DTO/WorkerOptions.php similarity index 97% rename from src/WorkerOptions.php rename to src/DTO/WorkerOptions.php index 170ca6c..c41e330 100644 --- a/src/WorkerOptions.php +++ b/src/DTO/WorkerOptions.php @@ -1,6 +1,6 @@ create(config('queue.connections.database.table', 'queue_jobs'), function(Structure $table) { + $table->bigIncrements('id'); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + + return $table; + }); + + $this->create(config('queue.failed.table', 'queue_failed_jobs'), function(Structure $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + + return $table; + }); + } + + public function down() + { + $this->dropIfExists(config('queue.connections.database.table', 'queue_jobs')); + $this->dropIfExists(config('queue.failed.table', 'queue_failed_jobs')); + } +} diff --git a/src/Drivers/ConnectorInterface.php b/src/Drivers/ConnectorInterface.php index fdebdc6..6cf7b75 100644 --- a/src/Drivers/ConnectorInterface.php +++ b/src/Drivers/ConnectorInterface.php @@ -10,5 +10,5 @@ interface ConnectorInterface /** * Establish a queue connection. */ - public function connect(ContainerInterface $container, array $config): Queue; + public static function connect(ContainerInterface $container, array $config): Queue; } diff --git a/src/Drivers/DatabaseDriver.php b/src/Drivers/DatabaseDriver.php index aeb08ad..149f174 100644 --- a/src/Drivers/DatabaseDriver.php +++ b/src/Drivers/DatabaseDriver.php @@ -3,22 +3,22 @@ namespace BlitzPHP\Queue\Drivers; use BlitzPHP\Contracts\Container\ContainerInterface; -use BlitzPHP\Contracts\Database\BuilderInterface; use BlitzPHP\Contracts\Database\ConnectionInterface; use BlitzPHP\Contracts\Database\ConnectionResolverInterface; use BlitzPHP\Contracts\Queue\Job; use BlitzPHP\Contracts\Queue\Queue as QueueContract; +use BlitzPHP\Exceptions\CriticalError; +use BlitzPHP\Queue\Events\QueueEventManager; +use BlitzPHP\Queue\Models\JobModel; use BlitzPHP\Queue\Queue; use BlitzPHP\Queue\Jobs\DatabaseJob; use BlitzPHP\Queue\Jobs\DatabaseJobRecord; use BlitzPHP\Queue\Jobs\InspectedJob; use BlitzPHP\Utilities\Iterable\Collection; -use BlitzPHP\Utilities\DateTime\Date; use BlitzPHP\Utilities\String\Stringable; use BlitzPHP\Utilities\String\Text; use DateTimeInterface; use DateInterval; -use PDO; use Throwable; class DatabaseDriver extends Queue implements QueueContract, ConnectorInterface @@ -32,20 +32,11 @@ class DatabaseDriver extends Queue implements QueueContract, ConnectorInterface /** * Create a new database queue instance. - * - * @param ConnectionInterface $database The database connection instance. - * @param string $table The database table that holds the jobs. - * @param string $default The name of the default queue. - * @param int $retryAfter The expiration time of a job. - * @param bool $dispatchAfterCommit + * + * @param string $default The name of the default queue. */ - public function __construct( - protected ConnectionInterface $database, - protected string $table, - protected string $default = 'default', - protected int $retryAfter = 60, - $dispatchAfterCommit = false, - ) { + public function __construct(protected JobModel $model, protected string $default = 'default', bool $dispatchAfterCommit = false) + { $this->dispatchAfterCommit = $dispatchAfterCommit; } @@ -54,15 +45,36 @@ public function __construct( * * @param array $config */ - public function connect(ContainerInterface $container, array $config): QueueContract + public static function connect(ContainerInterface $container, array $config): QueueContract { - return new self( - $container->get(ConnectionResolverInterface::class)->connection($config['connection'] ?? null), - $config['table'], - $config['queue'], - $config['retry_after'] ?? 60, - $config['after_commit'] ?? null - ); + try { + $connection = service('database', $config['connection'] ?? null, $config['shared'] ?? true); + + $queue = new self( + new JobModel( + $config, + $container->get(ConnectionResolverInterface::class), + $connection, + ), + $config['queue'], + $config['after_commit'] ?? false + ); + + $container->get(QueueEventManager::class)->handlerConnectionEstablished( + connection: $queue->getConnectionName(), + config: $config, + ); + + return $queue; + } catch (Throwable $e) { + $container->get(QueueEventManager::class)->handlerConnectionFailed( + connection: 'default', + config: $config, + exception: $e, + ); + + throw new CriticalError('Queue: Database connection failed. ' . $e->getMessage()); + } } /** @@ -70,9 +82,7 @@ public function connect(ContainerInterface $container, array $config): QueueCont */ public function size(?string $queue = null): int { - return $this->database->table($this->table) - ->where('queue', $this->getQueue($queue)) - ->count(); + return $this->model->size($this->getQueue($queue)); } /** @@ -80,11 +90,7 @@ public function size(?string $queue = null): int */ public function pendingSize(?string $queue = null): int { - return $this->database->table($this->table) - ->where('queue', $this->getQueue($queue)) - ->where('available_at <=', $this->currentTime()) - ->whereNull('reserved_at') - ->count(); + return $this->model->pendingSize($this->getQueue($queue)); } /** @@ -92,11 +98,7 @@ public function pendingSize(?string $queue = null): int */ public function delayedSize(?string $queue = null): int { - return $this->database->table($this->table) - ->where('queue', $this->getQueue($queue)) - ->where('available_at >', $this->currentTime()) - ->whereNull('reserved_at') - ->count(); + return $this->model->delayedSize($this->getQueue($queue)); } /** @@ -104,10 +106,7 @@ public function delayedSize(?string $queue = null): int */ public function reservedSize(?string $queue = null): int { - return $this->database->table($this->table) - ->where('queue', $this->getQueue($queue)) - ->whereNotNull('reserved_at') - ->count(); + return $this->model->reservedSize($this->getQueue($queue)); } /** @@ -117,13 +116,8 @@ public function reservedSize(?string $queue = null): int */ public function pendingJobs(?string $queue = null): Collection { - $data = $this->database->table($this->table) - ->where('queue', $this->getQueue($queue)) - ->where('available_at <=', $this->currentTime()) - ->whereNull('reserved_at') - ->all(); - - return collect($data)->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); + return collect($this->model->pendingJobs($this->getQueue($queue))) + ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); } /** @@ -133,13 +127,7 @@ public function pendingJobs(?string $queue = null): Collection */ public function delayedJobs(?string $queue = null): Collection { - $data = $this->database->table($this->table) - ->where('queue', $this->getQueue($queue)) - ->where('available_at >', $this->currentTime()) - ->whereNull('reserved_at') - ->all(); - - return collect($data) + return collect($this->model->delayedJobs($this->getQueue($queue))) ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); } @@ -150,12 +138,7 @@ public function delayedJobs(?string $queue = null): Collection */ public function reservedJobs(?string $queue = null): Collection { - $data = $this->database->table($this->table) - ->where('queue', $this->getQueue($queue)) - ->whereNotNull('reserved_at') - ->all(); - - return collect($data) + return collect($this->model->reservedJobs($this->getQueue($queue))) ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); } @@ -164,18 +147,13 @@ public function reservedJobs(?string $queue = null): Collection */ public function creationTimeOfOldestPendingJob(?string $queue = null): ?int { - return $this->database->table($this->table) - ->where('queue', $this->getQueue($queue)) - ->where('available_at <=', $this->currentTime()) - ->whereNull('reserved_at') - ->sortAsc('available_at') - ->value('available_at'); + return $this->model->creationTimeOfOldestPendingJob($this->getQueue($queue)); } /** * Push a new job onto the queue. */ - public function push(string|Job $job, mixed $data = '', ?string $queue = null): mixed + public function push(string|object $job, mixed $data = '', ?string $queue = null): mixed { return $this->enqueueUsing( $job, @@ -197,7 +175,7 @@ public function pushRaw(string $payload, ?string $queue = null, array $options = /** * Push a new job onto the queue after (n) seconds. */ - public function later(DateTimeInterface|DateInterval|int $delay, string|Job $job, mixed $data = '', ?string $queue = null): mixed + public function later(DateTimeInterface|DateInterval|int $delay, string|object $job, mixed $data = '', ?string $queue = null): mixed { return $this->enqueueUsing( $job, @@ -217,7 +195,7 @@ public function bulk(array $jobs, mixed $data = '', ?string $queue = null): mixe $now = $this->availableAt(); - $this->database->table($this->table)->insert((new Collection((array) $jobs))->map( + $this->model->insert((new Collection((array) $jobs))->map( function ($job) use ($queue, $data, $now) { return $this->buildDatabaseRecord( $queue, @@ -243,16 +221,12 @@ public function release(string $queue, DatabaseJobRecord $job, int $delay): mixe */ protected function pushToDatabase(?string $queue, string $payload, DateTimeInterface|DateInterval|int $delay = 0, int $attempts = 0): mixed { - $builder = $this->database->table($this->table); - - $builder->insert($this->buildDatabaseRecord( + return $this->model->pushToDatabase($this->buildDatabaseRecord( $this->getQueue($queue), $payload, $this->availableAt($delay), $attempts )); - - return $builder->lastId(); } /** @@ -275,14 +249,14 @@ protected function buildDatabaseRecord(?string $queue, string $payload, int $ava * * @throws Throwable */ - public function pop(string $queue = null): ?Job + public function pop(?string $queue = null): ?Job { $queue = $this->getQueue($queue); $jobRecord = null; try { - return $this->database->transaction(function () use ($queue, &$jobRecord) { + return $this->model->transaction(function () use ($queue, &$jobRecord) { if ($jobRecord = $this->getNextAvailableJob($queue)) { return $this->marshalJob($queue, $jobRecord); } @@ -308,15 +282,7 @@ public function pop(string $queue = null): ?Job */ protected function getNextAvailableJob(?string $queue): ?DatabaseJobRecord { - $job = $this->database->table($this->table) - // ->lock($this->getLockForPopping()) available only in blitz-php/database > 1.2 - ->where('queue', $this->getQueue($queue)) - ->where(function ($query) { - $this->isAvailable($query); - $this->isReservedButExpired($query); - }) - ->orderBy('id', 'asc') - ->first(); + $job = $this->model->getNextAvailableJob($this->getQueue($queue)); return $job ? new DatabaseJobRecord((object) $job) : null; } @@ -332,8 +298,8 @@ protected function getLockForPopping() return $this->lockForPopping; } - $databaseEngine = $this->database->getConnection()->getAttribute(PDO::ATTR_DRIVER_NAME); - $databaseVersion = $this->database->getConnection()->getAttribute(PDO::ATTR_SERVER_VERSION); + $databaseEngine= $this->model->db()->getPlatform(); + $databaseVersion= $this->model->db()->getVersion(); if ((new Stringable($databaseVersion))->contains('MariaDB')) { $databaseEngine = 'mariadb'; @@ -358,29 +324,6 @@ protected function getLockForPopping() return $this->lockForPopping = true; } - /** - * Modify the query to check for available jobs. - */ - protected function isAvailable(BuilderInterface $query): void - { - $query->where(function ($query) { - $query->whereNull('reserved_at') - ->where('available_at <=', $this->currentTime()); - }); - } - - /** - * Modify the query to check for jobs that are reserved but have expired. - */ - protected function isReservedButExpired(BuilderInterface $query): void - { - $expiration = Date::now()->subSeconds($this->retryAfter)->getTimestamp(); - - $query->orWhere(function ($query) use ($expiration) { - $query->where('reserved_at', '<=', $expiration); - }); - } - /** * Marshal the reserved job into a DatabaseJob instance. */ @@ -400,7 +343,7 @@ protected function marshalJob(string $queue, DatabaseJobRecord $job): DatabaseJo */ protected function markJobAsReserved(DatabaseJobRecord $job): DatabaseJobRecord { - $this->database->table($this->table)->where('id', $job->id)->update([ + $this->model->where('id', $job->id)->update([ 'reserved_at' => $job->touch(), 'attempts' => $job->increment(), ]); @@ -415,11 +358,7 @@ protected function markJobAsReserved(DatabaseJobRecord $job): DatabaseJobRecord */ public function deleteReserved(string $queue, string $id): void { - $this->database->transaction(function () use ($id) { - if ($this->database->table($this->table)/*->lockForUpdate()*/->where('id', $id)->first()) { - $this->database->table($this->table)->where('id', $id)->delete(); - } - }); + $this->model->deleteReserved($queue, $id); } /** @@ -427,11 +366,11 @@ public function deleteReserved(string $queue, string $id): void */ public function deleteAndRelease(string $queue, DatabaseJob $job, int $delay): void { - $this->database->transaction(function () use ($queue, $job, $delay) { + $this->model->transaction(function () use ($queue, $job, $delay) { $where = ['id' => $job->getJobId()]; - if ($this->database->table($this->table)/*->lockForUpdate()*/->where($where)->first()) { - $this->database->table($this->table)->where($where)->delete(); + if ($this->model/*->lockForUpdate()*/->where($where)->first()) { + $this->model->where($where)->delete(); } $this->release($queue, $job->getJobRecord(), $delay); @@ -441,13 +380,9 @@ public function deleteAndRelease(string $queue, DatabaseJob $job, int $delay): v /** * Delete all of the jobs from the queue. */ - public function clear(int $queue): bool + public function clear(string $queue): bool { - $this->database->table($this->table) - ->where('queue', $this->getQueue($queue)) - ->delete(); - - return true; + return $this->model->clear($this->getQueue($queue)); } /** @@ -463,6 +398,6 @@ public function getQueue(?string $queue): string */ public function getDatabase(): ConnectionInterface { - return $this->database; + return $this->model->db(); } } diff --git a/src/Drivers/FailoverDriver.php b/src/Drivers/FailoverDriver.php new file mode 100644 index 0000000..c338098 --- /dev/null +++ b/src/Drivers/FailoverDriver.php @@ -0,0 +1,173 @@ + + */ + protected array $failingQueues = []; + + /** + * Create a new failover queue instance. + */ + public function __construct(public Manager $manager, public QueueEventManager $events, public array $connections) + { + } + + /** + * Establish a queue connection. + */ + public static function connect(ContainerInterface $container, array $config): QueueContract + { + return new self( + $container->make(Manager::class), + $container->make(QueueEventManager::class), + $config['connections'], + ); + } + + /** + * Get the size of the queue. + */ + public function size(?string $queue = null): int + { + return $this->manager->connection($this->connections[0])->size($queue); + } + + /** + * Get the number of pending jobs. + */ + public function pendingSize(?string $queue = null): int + { + return $this->manager->connection($this->connections[0])->pendingSize($queue); + } + + /** + * Get the number of delayed jobs. + */ + public function delayedSize(?string $queue = null): int + { + return $this->manager->connection($this->connections[0])->delayedSize($queue); + } + + /** + * Get the number of reserved jobs. + */ + public function reservedSize(?string $queue = null): int + { + return $this->manager->connection($this->connections[0])->reservedSize($queue); + } + + /** + * Get the pending jobs for the given queue. + */ + public function pendingJobs(?string $queue = null): Collection + { + return $this->manager->connection($this->connections[0])->pendingJobs($queue); + } + + /** + * Get the delayed jobs for the given queue. + */ + public function delayedJobs(?string $queue = null): Collection + { + return $this->manager->connection($this->connections[0])->delayedJobs($queue); + } + + /** + * Get the reserved jobs for the given queue. + */ + public function reservedJobs(?string $queue = null): Collection + { + return $this->manager->connection($this->connections[0])->reservedJobs($queue); + } + + /** + * Get the creation timestamp of the oldest pending job, excluding delayed jobs. + */ + public function creationTimeOfOldestPendingJob(?string $queue = null): ?int + { + return $this->manager + ->connection($this->connections[0]) + ->creationTimeOfOldestPendingJob($queue); + } + + /** + * Push a new job onto the queue. + */ + public function push(object|string $job, mixed $data = '', ?string $queue = null): mixed + { + return $this->attemptOnAllConnections(__FUNCTION__, func_get_args(), $job); + } + + /** + * Push a raw payload onto the queue. + */ + public function pushRaw(string $payload, ?string $queue = null, array $options = []): mixed + { + return $this->attemptOnAllConnections(__FUNCTION__, func_get_args()); + } + + /** + * Push a new job onto the queue after (n) seconds. + */ + public function later(DateTimeInterface|DateInterval|int $delay, string|object $job, mixed $data = '', ?string $queue = null): mixed + { + return $this->attemptOnAllConnections(__FUNCTION__, func_get_args(), $job); + } + + /** + * Pop the next job off of the queue. + */ + public function pop(?string $queue = null): ?Job + { + return $this->manager->connection($this->connections[0])->pop($queue); + } + + /** + * Attempt the given method on all connections. + * + * + * @throws Throwable + */ + protected function attemptOnAllConnections(string $method, array $arguments, ?string $job = null): mixed + { + [$lastException, $failedQueues] = [null, []]; + + try { + foreach ($this->connections as $connection) { + try { + return $this->manager->connection($connection)->{$method}(...$arguments); + } catch (Throwable $e) { + $lastException = $e; + + $failedQueues[] = $connection; + + if ($job !== null && ! in_array($connection, $this->failingQueues)) { + $this->events->queueFailedOver($connection, $job, $e); + } + } + } + } finally { + $this->failingQueues = $failedQueues; + } + + throw $lastException ?? new RuntimeException('All failover queue connections failed.'); + } +} diff --git a/src/Drivers/NullDriver.php b/src/Drivers/NullDriver.php new file mode 100644 index 0000000..2a948a9 --- /dev/null +++ b/src/Drivers/NullDriver.php @@ -0,0 +1,118 @@ +dispatchAfterCommit = $dispatchAfterCommit; + } + + /** + * Establish a queue connection. + */ + public static function connect(ContainerInterface $container, array $config): QueueContract + { + return new self($config['after_commit'] ?? null); + } + + + /** + * Get the size of the queue. + */ + public function size(?string $queue = null): int + { + return 0; + } + + /** + * Get the number of pending jobs. + */ + public function pendingSize(?string $queue = null): int + { + return 0; + } + + /** + * Get the number of delayed jobs. + */ + public function delayedSize(?string $queue = null): int + { + return 0; + } + + /** + * Get the number of reserved jobs. + */ + public function reservedSize(?string $queue = null): int + { + return 0; + } + + /** + * Get the pending jobs for the given queue. + */ + public function pendingJobs(?string $queue = null): Collection + { + return new Collection; + } + + /** + * Get the delayed jobs for the given queue. + */ + public function delayedJobs(?string $queue = null): Collection + { + return new Collection; + } + + /** + * Get the reserved jobs for the given queue. + */ + public function reservedJobs(?string $queue = null): Collection + { + return new Collection; + } + + /** + * Get the creation timestamp of the oldest pending job, excluding delayed jobs. + */ + public function creationTimeOfOldestPendingJob(?string $queue = null): ?int + { + return null; + } + + /** + * Push a new job onto the queue. + * + * @throws Throwable + */ + public function push(string|object $job, mixed $data = '', ?string $queue = null): mixed + { + $job = $job instanceof Job ? $job->getJobId() : (string) $job; + + /* + if ($this->shouldDispatchAfterCommit($job) && + $this->container->bound('db.transactions')) { + if ($job instanceof ShouldBeUnique) { + $this->container->make('db.transactions')->addCallbackForRollback( + function () use ($job) { + (new UniqueLock($this->container->make(Cache::class)))->release($job); + } + ); + } + + return $this->container->make('db.transactions')->addCallback( + fn () => $this->executeJob($job, $data, $queue) + ); + } + */ + + return $this->executeJob($job, $data, $queue); + } + + /** + * Execute a given job synchronously. + * + * @throws Throwable + */ + protected function executeJob(string $job, mixed $data = '', ?string $queue = null): int + { + $queueJob = $this->resolveJob($this->createPayload($job, $queue, $data), $queue); + + try { + $this->raiseBeforeJobEvent($queueJob); + + $queueJob->fire(); + + $this->raiseAfterJobEvent($queueJob); + } catch (Throwable $e) { + $exceptionOccurred = $e; + + $this->handleException($queueJob, $e); + } finally { + $this->raiseJobAttemptedEvent($queueJob, $exceptionOccurred ?? null); + } + + return 0; + } + + /** + * Resolve a Sync job instance. + */ + protected function resolveJob(string $payload, string $queue): SyncJob + { + return new SyncJob($this->container, $payload, $this->connectionName, $queue); + } + + /** + * Raise the before queue job event. + */ + protected function raiseBeforeJobEvent(Job $job): void + { + $this->eventManager()->jobProcessing($this->connectionName, $job); + } + + /** + * Raise the after queue job event. + */ + protected function raiseAfterJobEvent(Job $job): void + { + $this->eventManager()->jobProcessed($this->connectionName, $job); + } + + /** + * Raise the job attempted event. + */ + protected function raiseJobAttemptedEvent(Job $job, ?Throwable $exceptionOccurred = null): void + { + $this->eventManager()->jobAttempted($this->connectionName, $job, $exceptionOccurred); + } + + /** + * Raise the exception occurred queue job event. + */ + protected function raiseExceptionOccurredJobEvent(Job $job, Throwable $e): void + { + $this->eventManager()->jobExceptionOccured($this->connectionName, $job, $e); + } + + /** + * Handle an exception that occurred while processing a job. + * + * @throws Throwable + */ + protected function handleException(Job $queueJob, Throwable $e): void + { + $this->raiseExceptionOccurredJobEvent($queueJob, $e); + + $queueJob->fail($e); + + throw $e; + } + + /** + * Push a raw payload onto the queue. + */ + public function pushRaw(string $payload, ?string $queue = null, array $options = []): mixed + { + return null; + } + + /** + * Push a new job onto the queue after (n) seconds. + */ + public function later(DateTimeInterface|DateInterval|int $delay, string|object $job, mixed $data = '', ?string $queue = null): mixed + { + return $this->push($job, $data, $queue); + } + + /** + * Pop the next job off of the queue. + */ + public function pop(?string $queue = null): ?Job + { + return null; + } +} diff --git a/src/Events/QueueEvent.php b/src/Events/QueueEvent.php index 8ab03ad..14e786b 100644 --- a/src/Events/QueueEvent.php +++ b/src/Events/QueueEvent.php @@ -5,16 +5,23 @@ use BlitzPHP\Contracts\Queue\Job; use BlitzPHP\Event\Event; use BlitzPHP\Utilities\Date; +use BlitzPHP\Utilities\String\Text; use Throwable; +/** + * @property mixed $job + * @property ?int $jobId + * @property ?int $attempts + * @property ?Throwable $exception + */ class QueueEvent extends Event { private readonly Date $timestamp; public function __construct( - private readonly string $type, - private readonly string $connection, - private readonly ?string $queue = null, + public readonly string $type, + public readonly string $connection, + public readonly ?string $queue = null, private readonly array $metadata = [], ?Date $timestamp = null, ) { @@ -23,34 +30,10 @@ public function __construct( $this->timestamp = $timestamp ?? Date::now(); } - /** - * Get event type - */ - public function getType(): string - { - return $this->type; - } - - /** - * Get connection name - */ - public function getConnection(): string - { - return $this->connection; - } - - /** - * Get queue name - */ - public function getQueue(): ?string - { - return $this->queue; - } - /** * Get timestamp */ - public function getTimestamp(): Date + public function timestamp(): Date { return $this->timestamp; } @@ -58,7 +41,7 @@ public function getTimestamp(): Date /** * Get all metadata */ - public function getAllMetadata(): array + public function allMetadata(): array { return $this->metadata; } @@ -66,7 +49,7 @@ public function getAllMetadata(): array /** * Get metadata value by key */ - public function getMetadata(string $key, mixed $default = null): mixed + public function metadata(string $key, mixed $default = null): mixed { return $this->metadata[$key] ?? $default; } @@ -106,26 +89,14 @@ public function isConnectionEvent(): bool return str_starts_with($this->type, 'queue.connection.'); } - // Job-related convenience methods (metadata-based) - /** * Get job ID (for job events) */ public function getJobId(): ?int { - $job = $this->getMetadata('job'); + $job = $this->job; - return $job instanceof Job ? $job->getJobId() : $this->getMetadata('job_id'); - } - - /** - * Get job priority (for job events) - */ - public function getPriority(): ?string - { - $job = $this->getMetadata('job'); - - return $job instanceof Job ? $job->priority : $this->getMetadata('priority'); + return $job instanceof Job ? $job->getJobId() : $this->metadata('job_id'); } /** @@ -133,9 +104,9 @@ public function getPriority(): ?string */ public function getAttempts(): ?int { - $job = $this->getMetadata('job'); + $job = $this->job; - return $job instanceof Job ? $job->attempts() : $this->getMetadata('attempts'); + return $job instanceof Job ? $job->attempts() : $this->metadata('attempts'); } /** @@ -143,9 +114,9 @@ public function getAttempts(): ?int */ public function getStatus(): ?int { - $job = $this->getMetadata('job'); + $job = $this->job; - return $job instanceof Job ? $job->status : $this->getMetadata('status'); + return $job instanceof Job ? $job->status : $this->metadata('status'); } /** @@ -153,7 +124,7 @@ public function getStatus(): ?int */ public function getJobClass(): ?string { - return $this->getMetadata('job_class'); + return $this->metadata('job_class'); } /** @@ -161,7 +132,7 @@ public function getJobClass(): ?string */ public function getProcessingTime(): float { - return (float) $this->getMetadata('processing_time', 0.0); + return (float) $this->metadata('processing_time', 0.0); } /** @@ -177,7 +148,7 @@ public function getProcessingTimeMs(): int */ public function getException(): ?Throwable { - return $this->getMetadata('exception'); + return $this->metadata('exception') ?? $this->metadata('e'); } /** @@ -185,9 +156,7 @@ public function getException(): ?Throwable */ public function getExceptionMessage(): ?string { - $exception = $this->getException(); - - return $exception?->getMessage(); + return $this->getException()?->getMessage(); } /** @@ -195,7 +164,7 @@ public function getExceptionMessage(): ?string */ public function hasFailed(): bool { - $job = $this->getMetadata('job'); + $job = $this->job; return $job instanceof Job ? $job->hasFailed() : $this->getException() !== null; } @@ -213,4 +182,13 @@ public function toArray(): array 'timestamp' => $this->timestamp->toDateTimeString(), ]; } + + public function __get(string $name): mixed + { + if (method_exists($this, $method = 'get' . Text::camel($name))) { + return $this->{$method}(); + } + + return $this->metadata($name); + } } diff --git a/src/Events/QueueEventManager.php b/src/Events/QueueEventManager.php index 186ec50..6fce702 100644 --- a/src/Events/QueueEventManager.php +++ b/src/Events/QueueEventManager.php @@ -5,7 +5,7 @@ use BlitzPHP\Contracts\Event\EventManagerInterface; use BlitzPHP\Contracts\Queue\Job; use BlitzPHP\Queue\Enums\WorkerStopReason; -use BlitzPHP\Queue\WorkerOptions; +use BlitzPHP\Queue\DTO\WorkerOptions; use Closure; use DateInterval; use DateTimeInterface; @@ -27,13 +27,14 @@ class QueueEventManager public const JOB_LOOPING = 'queue.job.looping'; public const JOB_RELEASED_AFTER_EXCEPTION = 'queue.job.release-after-exception'; public const JOB_TIMEOUT = 'queue.job.timeout'; - public const JOB_QUEUED = 'queue.job.queued'; - public const JOB_QUEUEING = 'queue.job.queuing'; + public const JOB_QUEUED = 'queue.job.queued'; + public const JOB_QUEUEING = 'queue.job.queuing'; public const QUEUE_CLEARED = 'queue.cleared'; public const QUEUE_PAUSED = 'queue.paused'; public const QUEUE_RESUMED = 'queue.resumed'; - public const WORKER_STARTING = 'queue.worker.starting'; - public const WORKER_STOPPING = 'queue.worker.stopping'; + public const QUEUE_FAILED_OVER = 'queue.failed-over'; + public const WORKER_STARTING = 'queue.worker.starting'; + public const WORKER_STOPPING = 'queue.worker.stopping'; public const HANDLER_CONNECTION_FAILED = 'queue.handler.connection.failed'; public const HANDLER_CONNECTION_ESTABLISHED = 'queue.handler.connection.established'; @@ -147,7 +148,7 @@ public function jobQueued(string $connection, ?string $queue, string|int|null $j /** * Emit job processed event */ - public function jobQueuing(string $connection, ?string $queue, string|object $job, string $payload, DateTimeInterface|DateInterval|int|null $delay): void + public function jobQueueing(string $connection, ?string $queue, string|object $job, string $payload, DateTimeInterface|DateInterval|int|null $delay): void { $this->events->emit(new QueueEvent( type : self::JOB_QUEUEING, @@ -223,6 +224,18 @@ public function queueResumed(string $connection, string $queue): void )); } + /** + * Emit queue resumed event + */ + public function queueFailedOver(string $connection, string $job, Throwable $e): void + { + $this->events->emit(new QueueEvent( + type : self::QUEUE_FAILED_OVER, + connection: $connection, + metadata : compact('job', 'e'), + )); + } + /** * Emit worker started event */ @@ -247,4 +260,28 @@ public function workerStopping(string $connection, int $status, ?WorkerOptions $ metadata : compact('status', 'options', 'reason') )); } + + /** + * Emit handler connection established event + */ + public function handlerConnectionEstablished(string $connection, array $config = []): void + { + $this->events->emit(new QueueEvent( + type : self::HANDLER_CONNECTION_ESTABLISHED, + connection: $connection, + metadata : compact('config') + )); + } + + /** + * Emit handler connection failed event + */ + public function handlerConnectionFailed(string $connection, Throwable $exception, array $config = []): void + { + $this->events->emit(new QueueEvent( + type : self::HANDLER_CONNECTION_FAILED, + connection: $connection, + metadata : compact('config', 'exception') + )); + } } diff --git a/src/Failed/CountableFailedJobProvider.php b/src/Failed/CountableFailedJobProvider.php new file mode 100644 index 0000000..63c4590 --- /dev/null +++ b/src/Failed/CountableFailedJobProvider.php @@ -0,0 +1,10 @@ +insertGetId(compact( + 'connection', 'queue', 'payload', 'exception', 'failed_at' + )); + } + + /** + * Get the IDs of all of the failed jobs. + */ + public function ids(?string $queue = null): array + { + return $this->getTable() + ->when(! is_null($queue), fn ($query) => $query->where('queue', $queue)) + ->orderBy('id', 'desc') + ->values('id'); + } + + /** + * Get a list of all of the failed jobs. + */ + public function all(): array + { + return $this->getTable()->orderBy('id', 'desc')->all(); + } + + /** + * Get a single failed job. + */ + public function find(string|int $id): ?object + { + return $this->getTable()->where($this->whereId($id))->first(); + } + + /** + * Delete a single failed job from storage. + */ + public function forget(string|int $id): bool + { + return $this->getTable()->where($this->whereId($id))->delete() > 0; + } + + /** + * Flush all of the failed jobs from storage. + */ + public function flush(?int $hours = null): void + { + $this->getTable()->when($hours, function ($query, $hours) { + $query->where('failed_at <=', Date::now()->subHours($hours)->format('Y-m-d H:i:s')); + })->delete(); + } + + /** + * Prune all of the entries older than the given date. + */ + public function prune(DateTimeInterface $before): int + { + $query = $this->getTable()->where('failed_at <', $before->format('Y-m-d H:i:s')); + + $totalDeleted = 0; + + do { + $deleted = $query->limit(1000)->delete(); + + $totalDeleted += $deleted; + } while ($deleted !== 0); + + return $totalDeleted; + } + + /** + * Count the failed jobs. + */ + public function count(?string $connection = null, ?string $queue = null): int + { + return $this->getTable() + ->when($connection, fn ($builder) => $builder->where('connection', $connection)) + ->when($queue, fn ($builder) => $builder->where('queue', $queue)) + ->count(); + } + + /** + * Get a new query builder instance for the table. + * + * @return BaseBuilder + */ + public function getTable() + { + return $this->resolver->connection($this->database)->table($this->table); + } + + private function whereId(string|int $id): array + { + return [is_string($id) && strlen($id) === 32 ? 'uuid' : 'id' => $id]; + } + + private function insertGetId(array $data): ?int + { + ($builder = $this->getTable())->insert($data); + + return $builder->db()->lastId($this->table); + } +} diff --git a/src/Failed/DatabaseUuidFailedJobProvider.php b/src/Failed/DatabaseUuidFailedJobProvider.php new file mode 100644 index 0000000..4c6ae7b --- /dev/null +++ b/src/Failed/DatabaseUuidFailedJobProvider.php @@ -0,0 +1,136 @@ +getTable()->insert([ + 'uuid' => $uuid = json_decode($payload, true)['uuid'], + 'connection' => $connection, + 'queue' => $queue, + 'payload' => $payload, + 'exception' => (string) mb_convert_encoding($exception, 'UTF-8'), + 'failed_at' => Date::now()->format('Y-m-d H:i:s'), + ]); + + return $uuid; + } + + /** + * Get the IDs of all of the failed jobs. + */ + public function ids(?string $queue = null): array + { + return $this->getTable() + ->when(! is_null($queue), fn ($query) => $query->where('queue', $queue)) + ->orderBy('id', 'desc') + ->values('uuid'); + } + + /** + * Get a list of all of the failed jobs. + */ + public function all(): array + { + $records = $this->getTable()->orderBy('id', 'desc')->all(); + + return collect($records)->map(function ($record) { + $record->id = $record->uuid; + unset($record->uuid); + + return $record; + })->all(); + } + + /** + * Get a single failed job. + */ + public function find(string|int $id): ?object + { + if ($record = $this->getTable()->where('uuid', $id)->first()) { + $record->id = $record->uuid; + unset($record->uuid); + } + + return $record; + } + + /** + * Delete a single failed job from storage. + */ + public function forget(string|int $id): bool + { + return $this->getTable()->where('uuid', $id)->delete() > 0; + } + + /** + * Flush all of the failed jobs from storage. + */ + public function flush(?int $hours = null): void + { + $this->getTable()->when($hours, function ($query, $hours) { + $query->where('failed_at <=', Date::now()->subHours($hours)->format('Y-m-d H:i:s')); + })->delete(); + } + + /** + * Prune all of the entries older than the given date. + */ + public function prune(DateTimeInterface $before): int + { + $query = $this->getTable()->where('failed_at <', $before->format('Y-m-d H:i:s')); + + $totalDeleted = 0; + + do { + $deleted = $query->limit(1000)->delete(); + + $totalDeleted += $deleted; + } while ($deleted !== 0); + + return $totalDeleted; + } + + /** + * Count the failed jobs. + */ + public function count(?string $connection = null, ?string $queue = null): int + { + return $this->getTable() + ->when($connection, fn ($builder) => $builder->where('connection', $connection)) + ->when($queue, fn ($builder) => $builder->where('queue', $queue)) + ->count(); + } + + /** + * Get a new query builder instance for the table. + * + * @return BaseBuilder + */ + public function getTable() + { + return $this->resolver->connection($this->database)->table($this->table); + } +} diff --git a/src/Failed/FailedJobProviderInterface.php b/src/Failed/FailedJobProviderInterface.php new file mode 100644 index 0000000..bc80c37 --- /dev/null +++ b/src/Failed/FailedJobProviderInterface.php @@ -0,0 +1,43 @@ + + */ + public function ids(?string $queue = null): array; + + /** + * Get a list of all of the failed jobs. + * + * @return array + */ + public function all(): array; + + /** + * Get a single failed job. + */ + public function find(string|int $id): ?object; + + /** + * Delete a single failed job from storage. + */ + public function forget(string|int $id): bool; + + /** + * Flush all of the failed jobs from storage. + */ + public function flush(?int $hours = null): void; +} diff --git a/src/Failed/FileFailedJobProvider.php b/src/Failed/FileFailedJobProvider.php new file mode 100644 index 0000000..0399642 --- /dev/null +++ b/src/Failed/FileFailedJobProvider.php @@ -0,0 +1,181 @@ +lock(function () use ($connection, $queue, $payload, $exception) { + $id = json_decode($payload, true)['uuid']; + + $jobs = $this->read(); + + $failedAt = Date::now(); + + array_unshift($jobs, [ + 'id' => $id, + 'connection' => $connection, + 'queue' => $queue, + 'payload' => $payload, + 'exception' => (string) mb_convert_encoding($exception, 'UTF-8'), + 'failed_at' => $failedAt->format('Y-m-d H:i:s'), + 'failed_at_timestamp' => $failedAt->getTimestamp(), + ]); + + $this->write(array_slice($jobs, 0, $this->limit)); + + return $id; + }); + } + + /** + * Get the IDs of all of the failed jobs. + */ + public function ids(?string $queue = null): array + { + return (new Collection($this->all())) + ->when(! is_null($queue), fn ($collect) => $collect->where('queue', $queue)) + ->pluck('id') + ->all(); + } + + /** + * Get a list of all of the failed jobs. + */ + public function all(): array + { + return $this->read(); + } + + /** + * Get a single failed job. + */ + public function find(int|string $id): ?object + { + return (new Collection($this->read())) + ->first(fn ($job) => $job->id === $id); + } + + /** + * Delete a single failed job from storage. + */ + public function forget(string|int $id): bool + { + return $this->lock(function () use ($id) { + $this->write($pruned = (new Collection($jobs = $this->read())) + ->reject(fn ($job) => $job->id === $id) + ->values() + ->all()); + + return count($jobs) !== count($pruned); + }); + } + + /** + * Flush all of the failed jobs from storage. + */ + public function flush(?int $hours = null): void + { + $this->prune(Date::now()->subHours($hours ?: 0)); + } + + /** + * Prune all of the entries older than the given date. + */ + public function prune(DateTimeInterface $before): int + { + return $this->lock(function () use ($before) { + $jobs = $this->read(); + + $this->write($prunedJobs = (new Collection($jobs)) + ->reject(fn ($job) => $job->failed_at_timestamp <= $before->getTimestamp()) + ->values() + ->all() + ); + + return count($jobs) - count($prunedJobs); + }); + } + + /** + * Execute the given callback while holding a lock. + */ + protected function lock(Closure $callback): mixed + { + if (! $this->lockProviderResolver) { + return $callback(); + } + + return ($this->lockProviderResolver)() + ->lock('blitzphp-failed-jobs', 5) + ->block(10, function () use ($callback) { + return $callback(); + }); + } + + /** + * Read the failed jobs file. + */ + protected function read(): array + { + if (! file_exists($this->path)) { + return []; + } + + $content = file_get_contents($this->path); + + if (empty(trim($content))) { + return []; + } + + $content = json_decode($content); + + return is_array($content) ? $content : []; + } + + /** + * Write the given array of jobs to the failed jobs file. + */ + protected function write(array $jobs): void + { + file_put_contents( + $this->path, + json_encode($jobs, JSON_PRETTY_PRINT) + ); + } + + /** + * Count the failed jobs. + */ + public function count(?string $connection = null, ?string $queue = null): int + { + if (($connection ?? $queue) === null) { + return count($this->read()); + } + + return (new Collection($this->read())) + ->filter(fn ($job) => $job->connection === ($connection ?? $job->connection) && $job->queue === ($queue ?? $job->queue)) + ->count(); + } +} diff --git a/src/Failed/NullFailedJobProvider.php b/src/Failed/NullFailedJobProvider.php new file mode 100644 index 0000000..2a2cfc7 --- /dev/null +++ b/src/Failed/NullFailedJobProvider.php @@ -0,0 +1,63 @@ +maxTries; + } + + public function backoff(): int + { + return $this->backoff; + } + + public function queue(): string + { + return $this->queue; + } +} diff --git a/src/Jobs/DatabaseJob.php b/src/Jobs/DatabaseJob.php index 9e29934..4deff89 100644 --- a/src/Jobs/DatabaseJob.php +++ b/src/Jobs/DatabaseJob.php @@ -52,9 +52,9 @@ public function attempts(): int /** * Get the job identifier. */ - public function getJobId(): string|int + public function getJobId(): string { - return $this->job->id; + return (string) $this->job->id; } /** diff --git a/src/Jobs/InspectedJob.php b/src/Jobs/InspectedJob.php index 093d895..d33f248 100644 --- a/src/Jobs/InspectedJob.php +++ b/src/Jobs/InspectedJob.php @@ -2,7 +2,7 @@ namespace BlitzPHP\Queue\Jobs; -use BlitzPHP\Utilities\DateTime\Date; +use BlitzPHP\Utilities\Date; class InspectedJob { diff --git a/src/Jobs/Job.php b/src/Jobs/Job.php index 86f7a39..38ef2bc 100644 --- a/src/Jobs/Job.php +++ b/src/Jobs/Job.php @@ -76,6 +76,7 @@ public function fire(): void { $payload = $this->payload(); + [$class, $method] = JobName::parse($payload['job']); ($this->instance = $this->resolve($class))->{$method}($this, $payload['data']); diff --git a/src/Jobs/SyncJob.php b/src/Jobs/SyncJob.php new file mode 100644 index 0000000..2e57657 --- /dev/null +++ b/src/Jobs/SyncJob.php @@ -0,0 +1,68 @@ +queue = $queue; + $this->container = $container; + $this->connectionName = $connectionName; + } + + /** + * Release the job back into the queue after (n) seconds. + */ + public function release(int $delay = 0): void + { + parent::release($delay); + } + + /** + * Get the number of times the job has been attempted. + */ + public function attempts(): int + { + return 1; + } + + /** + * Get the job identifier. + */ + public function getJobId(): string + { + return ''; + } + + /** + * Get the raw body string for the job. + */ + public function getRawBody(): string + { + return $this->payload; + } + + /** + * Get the name of the queue the job belongs to. + */ + public function getQueue(): string + { + return 'sync'; + } +} diff --git a/src/Manager.php b/src/Manager.php index 22f249d..646320c 100644 --- a/src/Manager.php +++ b/src/Manager.php @@ -3,14 +3,14 @@ namespace BlitzPHP\Queue; use BlitzPHP\Cache\Cache; +use BlitzPHP\Contracts\Cache\CacheInterface; use BlitzPHP\Contracts\Container\ContainerInterface; use BlitzPHP\Contracts\Event\EventManagerInterface; use BlitzPHP\Contracts\Queue\Factory; use BlitzPHP\Contracts\Queue\Monitor; use BlitzPHP\Contracts\Queue\Queue as QueueContract; -use BlitzPHP\Queue\Drivers\ConnectorInterface; +use BlitzPHP\Queue\DTO\Config; use BlitzPHP\Queue\Events\QueueEventManager; -use BlitzPHP\Utilities\Helpers; use Closure; use DateInterval; use DateTimeInterface; @@ -23,24 +23,25 @@ class Manager implements Factory, Monitor { /** - * The array of resolved queue connections. + * The array of resolved queue drivers. * * @var array */ - protected array $connections = []; - - /** - * The array of resolved queue connectors. - */ - protected array $connectors = []; + protected array $drivers = []; protected QueueEventManager $queueEventManager; + protected Cache $cache; + + protected EventManagerInterface $events; + /** * Create a new queue manager instance. */ - public function __construct(protected ContainerInterface $container) + public function __construct(protected ContainerInterface $container, protected Config $config) { + $this->cache = $container->get(CacheInterface::class); + $this->events = $container->get(EventManagerInterface::class); } /** @@ -48,10 +49,7 @@ public function __construct(protected ContainerInterface $container) */ public function before(callable $callback): void { - $this->container->get(EventManagerInterface::class)->on( - QueueEventManager::JOB_PROCESSING, - $callback - ); + $this->events->on(QueueEventManager::JOB_PROCESSING, $callback); } /** @@ -59,10 +57,7 @@ public function before(callable $callback): void */ public function after(callable $callback): void { - $this->container->get(EventManagerInterface::class)->on( - QueueEventManager::JOB_PROCESSED, - $callback - ); + $this->events->on(QueueEventManager::JOB_PROCESSED, $callback); } /** @@ -70,10 +65,7 @@ public function after(callable $callback): void */ public function exceptionOccurred(callable $callback): void { - $this->container->get(EventManagerInterface::class)->on( - QueueEventManager::JOB_EXCEPTION_OCCURED, - $callback - ); + $this->events->on(QueueEventManager::JOB_EXCEPTION_OCCURED, $callback); } /** @@ -81,10 +73,7 @@ public function exceptionOccurred(callable $callback): void */ public function looping(callable $callback): void { - $this->container->get(EventManagerInterface::class)->on( - QueueEventManager::JOB_LOOPING, - $callback - ); + $this->events->on(QueueEventManager::JOB_LOOPING, $callback); } /** @@ -92,10 +81,7 @@ public function looping(callable $callback): void */ public function failing(callable $callback): void { - $this->container->get(EventManagerInterface::class)->on( - QueueEventManager::JOB_FAILED, - $callback - ); + $this->events->on(QueueEventManager::JOB_FAILED, $callback); } /** @@ -103,10 +89,7 @@ public function failing(callable $callback): void */ public function starting(callable $callback): void { - $this->container->get(EventManagerInterface::class)->on( - QueueEventManager::WORKER_STARTING, - $callback - ); + $this->events->on(QueueEventManager::WORKER_STARTING, $callback); } /** @@ -114,16 +97,13 @@ public function starting(callable $callback): void */ public function stopping(callable $callback): void { - $this->container->get(EventManagerInterface::class)->on( - QueueEventManager::WORKER_STOPPING, - $callback - ); + $this->events->on(QueueEventManager::WORKER_STOPPING, $callback); } protected function queueEventManager(): QueueEventManager { if (! $this->queueEventManager) { - $this->queueEventManager = $this->container->make(QueueEventManager::class); + $this->queueEventManager = $this->container->get(QueueEventManager::class); } return $this->queueEventManager; @@ -134,26 +114,28 @@ protected function queueEventManager(): QueueEventManager */ public function connected(UnitEnum|string|null $name = null): bool { - return isset($this->connections[Helpers::enumValue($name) ?: $this->getDefaultDriver()]); + $name = $name instanceof UnitEnum ? $name->name : ($name ?: $this->getDefaultDriver()); + + return isset($this->drivers[$name]); } /** - * Resolve a queue connection instance. + * Resolve a queue driver instance. */ - public function connection(UnitEnum|string|null $name = null): QueueContract + public function driver(UnitEnum|string|null $name = null): QueueContract { - $name = Helpers::enumValue($name) ?: $this->getDefaultDriver(); + $name = $name instanceof UnitEnum ? $name->name : ($name ?: $this->getDefaultDriver()); - // If the connection has not been resolved yet we will resolve it now as all - // of the connections are resolved when they are actually needed so we do - // not make any unnecessary connection to the various queue end-points. - if (! isset($this->connections[$name])) { - $this->connections[$name] = $this->resolve($name); + // If the driver has not been resolved yet we will resolve it now as all + // of the drivers are resolved when they are actually needed so we do + // not make any unnecessary driver to the various queue end-points. + if (! isset($this->drivers[$name])) { + $this->drivers[$name] = $this->resolve($name); - $this->connections[$name]->setContainer($this->container); + $this->drivers[$name]->setContainer($this->container); } - return $this->connections[$name]; + return $this->drivers[$name]; } /** @@ -163,15 +145,10 @@ public function connection(UnitEnum|string|null $name = null): QueueContract */ protected function resolve(string $name): Queue { - $config = $this->getConfig($name); - - if (is_null($config)) { - throw new InvalidArgumentException("The [{$name}] queue connection has not been configured."); - } + $config = $this->config->connection($name); + $driver = $this->config->driver($config['driver']); - $queue = $this->getConnector($config['driver']) - ->connect($this->container, $config) - ->setConnectionName($name); + $queue = $driver::connect($this->container, $config)->setConnectionName($name); if (method_exists($queue, 'setConfig')) { $queue->setConfig($config); @@ -180,27 +157,12 @@ protected function resolve(string $name): Queue return $queue; } - /** - * Get the connector for a given driver. - * - * @throws InvalidArgumentException - */ - protected function getConnector(string $driver): ConnectorInterface - { - if (! isset($this->connectors[$driver])) { - throw new InvalidArgumentException("No connector for [$driver]."); - } - - return call_user_func($this->connectors[$driver]); - } - /** * Pause a queue by its connection and name. */ public function pause(string $connection, string $queue): void { - $this->container->get(Cache::class) - ->forever("blitzphp:queue:paused:{$connection}:{$queue}", true); + $this->cache->forever("blitzphp-queue-paused-{$connection}-{$queue}", true); $this->queueEventManager()->queuePaused($connection, $queue); } @@ -212,8 +174,7 @@ public function pauseFor(string $connection, string $queue, DateTimeInterface|Da { $convertedTtl = $ttl instanceof DateTimeInterface ? $ttl->getTimestamp() : $ttl; - $this->container->get(Cache::class) - ->set("blitzphp:queue:paused:{$connection}:{$queue}", true, $convertedTtl); + $this->cache->set("blitzphp-queue-paused-{$connection}-{$queue}", true, $convertedTtl); $this->queueEventManager()->queuePaused($connection, $queue, $ttl); } @@ -223,8 +184,7 @@ public function pauseFor(string $connection, string $queue, DateTimeInterface|Da */ public function resume(string $connection, string $queue): void { - $this->container->get(Cache::class) - ->delete("blitzphp:queue:paused:{$connection}:{$queue}"); + $this->cache->delete("blitzphp-queue-paused-{$connection}-{$queue}"); $this->queueEventManager()->queueResumed($connection, $queue); } @@ -234,8 +194,7 @@ public function resume(string $connection, string $queue): void */ public function isPaused(string $connection, string $queue): bool { - return (bool) $this->container->get(Cache::class) - ->get("blitzphp:queue:paused:{$connection}:{$queue}", false); + return (bool) $this->cache->get("blitzphp-queue-paused-{$connection}-{$queue}", false); } /** @@ -249,48 +208,20 @@ public function withoutInterruptionPolling(): void Worker::$pausable = false; } - /** - * Add a queue connection resolver. - */ - public function extend(string $driver, Closure $resolver): void - { - $this->addConnector($driver, $resolver); - } - - /** - * Add a queue connection resolver. - */ - public function addConnector(string $driver, Closure $resolver): void - { - $this->connectors[$driver] = $resolver; - } - - /** - * Get the queue connection configuration. - */ - protected function getConfig(string $name): ?array - { - if (! is_null($name) && $name !== 'null') { - return config("queue.connections.{$name}"); - } - - return ['driver' => 'null']; - } - /** * Get the name of the default queue connection. */ public function getDefaultDriver(): string { - return config('queue.default'); + return $this->config->default; } - - /** + + /** * Set the name of the default queue connection. */ public function setDefaultDriver(string $name): void { - config()->set('queue.default', $name); + $this->config->setDefaultDriver($name); } /** @@ -312,12 +243,12 @@ public function getContainer(): ContainerInterface /** * Set the container instance used by the manager. */ - public function setContainer(ContainerInterface $container) + public function setContainer(ContainerInterface $container): self { $this->container = $container; - foreach ($this->connections as $connection) { - $connection->setContainer($container); + foreach ($this->drivers as $driver) { + $driver->setContainer($container); } return $this; @@ -328,6 +259,6 @@ public function setContainer(ContainerInterface $container) */ public function __call(string $method, array $parameters = []): mixed { - return $this->connection()->$method(...$parameters); + return $this->driver()->$method(...$parameters); } } diff --git a/src/Models/JobModel.php b/src/Models/JobModel.php new file mode 100644 index 0000000..61d62d2 --- /dev/null +++ b/src/Models/JobModel.php @@ -0,0 +1,208 @@ +table = $config['table']; + $this->retryAfter = $config['retry_after'] ?? 60; + + // Turn off the Strict Mode + $db->transStrict(false); + + parent::__construct($resolver, $db); + } + + /** + * Get the size of the queue. + */ + public function size(string $queue): int + { + return $this->builder() + ->where('queue', $queue) + ->count(); + } + + /** + * Get the number of pending jobs. + */ + public function pendingSize(string $queue): int + { + return $this->builder() + ->where('queue', $queue) + ->where('available_at <=', $this->currentTime()) + ->whereNull('reserved_at') + ->count(); + } + + /** + * Get the number of delayed jobs. + */ + public function delayedSize(string $queue): int + { + return $this->builder() + ->where('queue', $$queue) + ->where('available_at >', $this->currentTime()) + ->whereNull('reserved_at') + ->count(); + } + + /** + * Get the number of reserved jobs. + */ + public function reservedSize(string $queue): int + { + return $this->builder() + ->where('queue', $$queue) + ->whereNotNull('reserved_at') + ->count(); + } + + /** + * Get the pending jobs for the given queue. + */ + public function pendingJobs(string $queue): array + { + return $this->builder() + ->where('queue', $queue) + ->where('available_at <=', $this->currentTime()) + ->whereNull('reserved_at') + ->all(); + } + + /** + * Get the delayed jobs for the given queue. + */ + public function delayedJobs(string $queue): array + { + return $this->builder() + ->where('queue', $queue) + ->where('available_at >', $this->currentTime()) + ->whereNull('reserved_at') + ->all(); + } + + /** + * Get the reserved jobs for the given queue. + */ + public function reservedJobs(string $queue): array + { + return $this->builder() + ->where('queue', $queue) + ->whereNotNull('reserved_at') + ->all(); + } + + /** + * Get the creation timestamp of the oldest pending job, excluding delayed jobs. + */ + public function creationTimeOfOldestPendingJob(string $queue): ?int + { + return $this->builder() + ->where('queue', $queue) + ->where('available_at <=', $this->currentTime()) + ->whereNull('reserved_at') + ->sortAsc('available_at') + ->value('available_at'); + } + + /** + * Push a raw payload to the database with a given delay of (n) seconds. + */ + public function pushToDatabase(array $data): mixed + { + $this->builder()->insert($data); + + return $this->db->lastId($this->table); + } + + /** + * Get the next available job for the queue. + */ + public function getNextAvailableJob(string $queue): ?object + { + return $this->builder() + // ->lock($this->getLockForPopping()) available only in blitz-php/database > 1.2 + ->where('queue', $queue) + ->where(function ($query) { + $this->isAvailable($query); + $this->isReservedButExpired($query); + }) + ->orderBy('id', 'asc') + ->first(); + } + + /** + * Delete a reserved job from the queue. + * + * @throws Throwable + */ + public function deleteReserved(string $queue, string $id): void + { + $this->db->transaction(function () use ($id) { + if ($this/*->lockForUpdate()*/->where('id', $id)->first()) { + $this->where('id', $id)->delete(); + } + }); + } + + + /** + * Delete all of the jobs from the queue. + */ + public function clear(string $queue): bool + { + $this->builder()->where('queue', $queue)->delete(); + + return true; + } + + /** + * Modify the query to check for available jobs. + */ + protected function isAvailable(BaseBuilder $query): void + { + $query->where(function ($query) { + $query->whereNull('reserved_at') + ->where('available_at <=', $this->currentTime()); + }); + } + + /** + * Modify the query to check for jobs that are reserved but have expired. + */ + protected function isReservedButExpired(BaseBuilder $query): void + { + $expiration = Date::now()->subSeconds($this->retryAfter)->getTimestamp(); + + $query->orWhere(function ($query) use ($expiration) { + $query->where('reserved_at <=', $expiration); + }); + } +} diff --git a/src/Providers/QueueProvider.php b/src/Providers/QueueProvider.php new file mode 100644 index 0000000..2868b2e --- /dev/null +++ b/src/Providers/QueueProvider.php @@ -0,0 +1,25 @@ + static fn () => service('queue'), + Monitor::class => static fn () => service('queue'), + Manager::class => static fn () => service('queue'), + Worker::class => static fn () => service('worker'), + ]; + } +} diff --git a/src/Queue.php b/src/Queue.php index d062cab..b155cea 100644 --- a/src/Queue.php +++ b/src/Queue.php @@ -10,9 +10,10 @@ use BlitzPHP\Queue\Events\QueueEventManager; use BlitzPHP\Queue\Exceptions\InvalidPayloadException; use BlitzPHP\Traits\Support\InteractsWithTime; -use BlitzPHP\Utilities\DateTime\Date; +use BlitzPHP\Utilities\Date; use BlitzPHP\Utilities\Iterable\Collection; use BlitzPHP\Utilities\String\Text; +use BlitzPHP\Utilities\String\Uuid; use Closure; use DateInterval; use DateTimeInterface; @@ -27,11 +28,16 @@ abstract class Queue implements QueueContract * The IoC container instance. */ protected ContainerInterface $container; + + /** + * The Queue Event Manager instance. + */ + protected ?QueueEventManager $eventManager = null; /** * The connection name for the queue. */ - protected string $connectionName; + protected string $connectionName = ''; /** * The original configuration for the queue. @@ -53,7 +59,7 @@ abstract class Queue implements QueueContract /** * Push a new job onto the queue. */ - public function pushOn(string $queue, string|Job $job, mixed $data = ''): mixed + public function pushOn(string $queue, string|object $job, mixed $data = ''): mixed { return $this->push($job, $data, $queue); } @@ -61,7 +67,7 @@ public function pushOn(string $queue, string|Job $job, mixed $data = ''): mixed /** * Push a new job onto a specific queue after (n) seconds. */ - public function laterOn(string $queue, DateTimeInterface|DateInterval|int $delay, string|Job $job, mixed $data = ''): mixed + public function laterOn(string $queue, DateTimeInterface|DateInterval|int $delay, string|object $job, mixed $data = ''): mixed { return $this->later($delay, $job, $data, $queue); } @@ -80,6 +86,14 @@ public function bulk(array $jobs, mixed $data = '', ?string $queue = null) } } + /** + * {@inheritDoc} + */ + public function clear(string $queue): bool + { + return true; + } + /** * Create a payload string from the given job and data. * @@ -127,7 +141,7 @@ protected function createPayloadArray(string|object $job, string $queue, mixed $ protected function createObjectPayload(object $job, string $queue): array { $payload = $this->withCreatePayloadHooks($queue, [ - 'uuid' => (string) Text::uuid(), + 'uuid' => (string) Uuid::v4(), 'displayName' => $this->getDisplayName($job), 'job' => 'BlitzPHP\Queue\CallQueuedHandler@call', 'maxTries' => $this->getJobTries($job), @@ -241,7 +255,7 @@ protected function jobShouldBeEncrypted(object $job): bool protected function createStringPayload(string $job, string $queue, mixed $data): array { return $this->withCreatePayloadHooks($queue, [ - 'uuid' => (string) Text::uuid(), + 'uuid' => (string) Uuid::v4(), 'displayName' => is_string($job) ? explode('@', $job)[0] : null, 'job' => $job, 'maxTries' => null, @@ -329,13 +343,9 @@ protected function shouldDispatchAfterCommit(string|object $job): bool /** * Raise the job queueing event. */ - protected function raiseJobQueueingEvent(string $queue, string|object $job, string $payload, DateTimeInterface|DateInterval|int|null $delay): void + protected function raiseJobQueueingEvent(?string $queue, string|object $job, string $payload, DateTimeInterface|DateInterval|int|null $delay): void { - if ($this->container->bound(EventManagerInterface::class)) { - $delay = ! is_null($delay) ? $this->secondsUntil($delay) : $delay; - - $this->container->get(QueueEventManager::class)->jobQueueing($this->connectionName, $queue, $job, $payload, $delay); - } + $this->eventManager()->jobQueueing($this->connectionName, $queue, $job, $payload, $delay); } /** @@ -343,11 +353,16 @@ protected function raiseJobQueueingEvent(string $queue, string|object $job, stri */ protected function raiseJobQueuedEvent(?string $queue, string|int|null $jobId, string|object $job, string $payload, DateTimeInterface|DateInterval|int|null $delay) { - if ($this->container->bound(EventManagerInterface::class)) { - $delay = ! is_null($delay) ? $this->secondsUntil($delay) : $delay; + $this->eventManager()->jobQueued($this->connectionName, $queue, $jobId, $job, $payload, $delay); + } - $this->container->get(QueueEventManager::class)->jobQueued($this->connectionName, $queue, $jobId, $job, $payload, $delay); + protected function eventManager(): QueueEventManager + { + if (! $this->eventManager) { + $this->eventManager = new QueueEventManager($this->container->get(EventManagerInterface::class)); } + + return $this->eventManager; } /** diff --git a/src/Traits/Dispatchable.php b/src/Traits/Dispatchable.php new file mode 100644 index 0000000..9dade5c --- /dev/null +++ b/src/Traits/Dispatchable.php @@ -0,0 +1,59 @@ +push($job, queue: $job->queue ?? null); + } + + /** + * Dispatch le job sur une queue spécifique + */ + public static function dispatchOn(string $queue, mixed ...$parameters): mixed + { + $job = new static(...$parameters); + + return Services::queue()->pushOn($queue, $job); + } + + /** + * Dispatch le job avec délai + */ + public static function dispatchLater(DateTimeInterface|DateInterval|int $delay, mixed ...$parameters): mixed + { + $job = new static(...$parameters); + + return Services::queue()->later($delay, $job, queue: $job->queue ?? null); + } + + /** + * Dispatch le job sur une queue spécifique avec délai + */ + public static function dispatchLaterOn(string $queue, DateTimeInterface|DateInterval|int $delay, ...$parameters): mixed + { + $job = new static(...$parameters); + + return Services::queue()->laterOn($queue, $delay, $job); + } + + /** + * Dispatch le job immédiatement (synchrone) + */ + public static function dispatchSync(mixed ...$parameters): void + { + $job = new static(...$parameters); + + $job->handle(); + } +} diff --git a/src/Traits/InteractsWithQueue.php b/src/Traits/InteractsWithQueue.php new file mode 100644 index 0000000..adaa8e9 --- /dev/null +++ b/src/Traits/InteractsWithQueue.php @@ -0,0 +1,254 @@ +job ? $this->job->attempts() : 1; + } + + /** + * Delete the job from the queue. + */ + public function delete(): void + { + if ($this->job) { + $this->job->delete(); + } + } + + /** + * Fail the job from the queue. + * + * @throws InvalidArgumentException + */ + public function fail(Throwable|string|null $exception = null): void + { + if (is_string($exception)) { + $exception = new ManuallyFailedException($exception); + } + + if ($exception instanceof Throwable || is_null($exception)) { + if ($this->job) { + $this->job->fail($exception); + } + } else { + throw new InvalidArgumentException('The fail method requires a string or an instance of Throwable.'); + } + } + + /** + * Release the job back into the queue after (n) seconds. + */ + public function release(DateTimeInterface|DateInterval|int $delay = 0): void + { + $delay = $delay instanceof DateTimeInterface + ? $this->secondsUntil($delay) + : $delay; + + if ($this->job) { + $this->job->release($delay); + } + } + + /** + * Indicate that queue interactions like fail, delete, and release should be faked. + */ + public function withFakeQueueInteractions(): self + { + $this->job = new FakeJob; + + return $this; + } + + /** + * Assert that the job was deleted from the queue. + */ + public function assertDeleted(): self + { + $this->ensureQueueInteractionsHaveBeenFaked(); + + /* PHPUnit::assertTrue( + $this->job->isDeleted(), + 'Job was expected to be deleted, but was not.' + ); */ + + return $this; + } + + /** + * Assert that the job was not deleted from the queue. + */ + public function assertNotDeleted(): self + { + $this->ensureQueueInteractionsHaveBeenFaked(); + + /* PHPUnit::assertTrue( + ! $this->job->isDeleted(), + 'Job was unexpectedly deleted.' + ); */ + + return $this; + } + + /** + * Assert that the job was manually failed. + */ + public function assertFailed(): self + { + $this->ensureQueueInteractionsHaveBeenFaked(); + + /* PHPUnit::assertTrue( + $this->job->hasFailed(), + 'Job was expected to be manually failed, but was not.' + ); */ + + return $this; + } + + /** + * Assert that the job was manually failed with a specific exception. + */ + public function assertFailedWith(Throwable|string $exception): self + { + $this->assertFailed(); + + if (is_string($exception) && class_exists($exception)) { + /* PHPUnit::assertInstanceOf( + $exception, + $this->job->failedWith, + 'Expected job to be manually failed with ['.$exception.'] but job failed with ['.get_class($this->job->failedWith).'].' + ); */ + + return $this; + } + + if (is_string($exception)) { + $exception = new ManuallyFailedException($exception); + } + + if ($exception instanceof Throwable) { + /* PHPUnit::assertInstanceOf( + get_class($exception), + $this->job->failedWith, + 'Expected job to be manually failed with ['.get_class($exception).'] but job failed with ['.get_class($this->job->failedWith).'].' + ); + + PHPUnit::assertEquals( + $exception->getCode(), + $this->job->failedWith->getCode(), + 'Expected exception code ['.$exception->getCode().'] but job failed with exception code ['.$this->job->failedWith->getCode().'].' + ); + + PHPUnit::assertEquals( + $exception->getMessage(), + $this->job->failedWith->getMessage(), + 'Expected exception message ['.$exception->getMessage().'] but job failed with exception message ['.$this->job->failedWith->getMessage().'].'); + */ + } + + return $this; + } + + /** + * Assert that the job was not manually failed. + */ + public function assertNotFailed(): self + { + $this->ensureQueueInteractionsHaveBeenFaked(); + + /* PHPUnit::assertTrue( + ! $this->job->hasFailed(), + 'Job was unexpectedly failed manually.' + ); */ + + return $this; + } + + /** + * Assert that the job was released back onto the queue. + */ + public function assertReleased(DateTimeInterface|DateInterval|int|null $delay = null): self + { + $this->ensureQueueInteractionsHaveBeenFaked(); + + $delay = $delay instanceof DateTimeInterface + ? $this->secondsUntil($delay) + : $delay; + + /* PHPUnit::assertTrue( + $this->job->isReleased(), + 'Job was expected to be released, but was not.' + ); */ + + if (! is_null($delay)) { + /* PHPUnit::assertSame( + $delay, + $this->job->releaseDelay, + "Expected job to be released with delay of [{$delay}] seconds, but was released with delay of [{$this->job->releaseDelay}] seconds." + ); */ + } + + return $this; + } + + /** + * Assert that the job was not released back onto the queue. + */ + public function assertNotReleased(): self + { + $this->ensureQueueInteractionsHaveBeenFaked(); + + /* PHPUnit::assertTrue( + ! $this->job->isReleased(), + 'Job was unexpectedly released.' + ); */ + + return $this; + } + + /** + * Ensure that queue interactions have been faked. + * + * @throws RuntimeException + */ + private function ensureQueueInteractionsHaveBeenFaked(): void + { + if (! $this->job instanceof FakeJob) { + throw new RuntimeException('Queue interactions have not been faked.'); + } + } + + /** + * Set the base queue job instance. + */ + public function setJob(JobContract $job): self + { + $this->job = $job; + + return $this; + } +} diff --git a/src/Traits/SerializesAndRestoresModelIdentifiers.php b/src/Traits/SerializesAndRestoresModelIdentifiers.php new file mode 100644 index 0000000..25fb5cc --- /dev/null +++ b/src/Traits/SerializesAndRestoresModelIdentifiers.php @@ -0,0 +1,120 @@ +getQueueableClass(), + $value->getQueueableIds(), + $withRelations ? $value->getQueueableRelations() : [], + $value->getQueueableConnection() + ))->useCollectionClass( + ($collectionClass = get_class($value)) !== WolkeCollection;::class + ? $collectionClass + : null + ); + } + + if ($value instanceof QueueableEntity) { + return new ModelIdentifier( + get_class($value), + $value->getQueueableId(), + $withRelations ? $value->getQueueableRelations() : [], + $value->getQueueableConnection() + ); + } + + return $value; + } + + /** + * Get the restored property value after deserialization. + */ + protected function getRestoredPropertyValue(mixed $value): mixed + { + if (! $value instanceof ModelIdentifier) { + return $value; + } + + return is_array($value->id) + ? $this->restoreCollection($value) + : $this->restoreModel($value); + } + + /** + * Restore a queueable collection instance. + * + * @param \Illuminate\Contracts\Database\ModelIdentifier $value + * @return WolkeCollection + */ + protected function restoreCollection($value) + { + $class = $value->getClass(); + + if (! $class || count($value->id) === 0) { + return ! is_null($value->collectionClass ?? null) + ? new $value->collectionClass + : new WolkeCollection;; + } + + $collection = $this->getQueryForModelRestoration( + (new $class)->setConnection($value->connection), $value->id + )->useWritePdo()->get(); + + if (is_a($class, Pivot::class, true) || in_array(AsPivot::class, class_uses($class))) { + return $collection; + } + + $collection = $collection->keyBy->getKey(); + + $collectionClass = get_class($collection); + + return (new $collectionClass( + (new Collection($value->id)) + ->map(fn ($id) => $collection[$id] ?? null) + ->filter() + ))->loadMissing($value->relations ?? []); + } + + /** + * Restore the model from the model identifier instance. + * + * @param \Illuminate\Contracts\Database\ModelIdentifier $value + * @return \BlitzPHP\Wolke\Model + */ + public function restoreModel($value) + { + return $this->getQueryForModelRestoration( + (new ($value->getClass()))->setConnection($value->connection), $value->id + )->useWritePdo()->firstOrFail()->loadMissing($value->relations ?? []); + } + + /** + * Get the query for model restoration. + * + * @template TModel of \BlitzPHP\Wolke\Model + * + * @param TModel $model + * + * @return \BlitzPHP\Wolke\Builder + */ + protected function getQueryForModelRestoration($model, array|int $ids) + { + return $model->newQueryForRestoration($ids); + } +} diff --git a/src/Traits/SerializesModels.php b/src/Traits/SerializesModels.php new file mode 100644 index 0000000..5bdbbfc --- /dev/null +++ b/src/Traits/SerializesModels.php @@ -0,0 +1,101 @@ +getProperties(), + property_exists($this, 'withoutRelations') && $this->withoutRelations === true, + ]; + + foreach ($properties as $property) { + if ($property->isStatic()) { + continue; + } + + if (! $property->isInitialized($this)) { + continue; + } + + if (method_exists($property, 'isVirtual') && $property->isVirtual()) { + continue; + } + + $value = $this->getPropertyValue($property); + + if ($property->hasDefaultValue() && $value === $property->getDefaultValue()) { + continue; + } + + $name = $property->getName(); + + if ($property->isPrivate()) { + $name = "\0{$class}\0{$name}"; + } elseif ($property->isProtected()) { + $name = "\0*\0{$name}"; + } + + $values[$name] = $this->getSerializedPropertyValue( + $value, + ! $classLevelWithoutRelations); + } + + return $values; + } + + /** + * Restore the model after serialization. + */ + public function __unserialize(array $values): void + { + $properties = (new ReflectionClass($this))->getProperties(); + + $class = get_class($this); + + foreach ($properties as $property) { + if ($property->isStatic()) { + continue; + } + + $name = $property->getName(); + + if ($property->isPrivate()) { + $name = "\0{$class}\0{$name}"; + } elseif ($property->isProtected()) { + $name = "\0*\0{$name}"; + } + + if (! array_key_exists($name, $values)) { + continue; + } + + $property->setValue( + $this, $this->getRestoredPropertyValue($values[$name]) + ); + } + } + + /** + * Get the property value for the given property. + */ + protected function getPropertyValue(ReflectionProperty $property): mixed + { + return $property->getValue($this); + } +} diff --git a/src/Worker.php b/src/Worker.php index 6c1d9cb..d37fea1 100644 --- a/src/Worker.php +++ b/src/Worker.php @@ -2,14 +2,15 @@ namespace BlitzPHP\Queue; -use BlitzPHP\Cache\Cache; +use BlitzPHP\Contracts\Cache\CacheInterface; use BlitzPHP\Contracts\Queue\Job; use BlitzPHP\Contracts\Queue\Queue; +use BlitzPHP\Queue\DTO\WorkerOptions; use BlitzPHP\Queue\Enums\WorkerStopReason; use BlitzPHP\Queue\Events\QueueEventManager; use BlitzPHP\Queue\Exceptions\MaxAttemptsExceededException; use BlitzPHP\Queue\Exceptions\TimeoutExceededException; -use BlitzPHP\Utilities\DateTime\Date; +use BlitzPHP\Utilities\Date; use Illuminate\Contracts\Debug\ExceptionHandler; // use BlitzPHP\Database\DetectsLostConnections; // available only in blitz-php/database 1.1 use Throwable; @@ -31,7 +32,7 @@ class Worker /** * The cache repository implementation. */ - protected Cache $cache; + protected CacheInterface $cache; /** * The exception handler instance. @@ -152,7 +153,7 @@ public function daemon(string $connectionName, string $queue, WorkerOptions $opt // register the timeout handler and reset the alarm for this job so it is // not stuck in a frozen state forever. Then, we can fire off this job. $job = $this->getNextJob( - $this->manager->connection($connectionName), $queue + $this->manager->driver($connectionName), $queue ); if ($supportsAsyncSignals) { @@ -262,7 +263,7 @@ protected function pauseWorker(WorkerOptions $options, int $lastRestart): ?array /** * Determine the exit code to stop the process if necessary. */ - protected function stopIfNecessary(WorkerOptions $options, int $lastRestart, int $startTime = 0, int $jobsProcessed = 0, mixed $job = null): ?array + protected function stopIfNecessary(WorkerOptions $options, int $lastRestart, float|int $startTime = 0, int $jobsProcessed = 0, mixed $job = null): ?array { return match (true) { $this->lostConnection => [static::EXIT_SUCCESS, WorkerStopReason::LostConnection], @@ -289,39 +290,41 @@ public function runNextJob(string $connectionName, string $queue, WorkerOptions // from this method. If there is no job on the queue, we will "sleep" the worker // for the specified number of seconds, then keep processing jobs after sleep. if ($job) { - return $this->runJob($job, $connectionName, $options); + $this->runJob($job, $connectionName, $options); + + return; } $this->sleep($options->sleep); } /** - * Get the next job from the queue connection. + * Get the next job from the queue driver. */ - protected function getNextJob(Queue $connection, string $queue): ?Job + protected function getNextJob(Queue $driver, string $queue): ?Job { - $popJobCallback = function ($queue, $index = 0) use ($connection) { - return $connection->pop($queue, $index); + $popJobCallback = function ($queue, $index = 0) use ($driver) { + return $driver->pop($queue, $index); }; - $this->raiseBeforeJobPopEvent($connection->getConnectionName(), $queue); + $this->raiseBeforeJobPopEvent($driver->getConnectionName(), $queue); try { if (isset(static::$popCallbacks[$this->name ?? ''])) { if (! is_null($job = (static::$popCallbacks[$this->name ?? ''])($popJobCallback, $queue))) { - $this->raiseAfterJobPopEvent($connection->getConnectionName(), $job); + $this->raiseAfterJobPopEvent($driver->getConnectionName(), $job); } return $job; } foreach (explode(',', $queue) as $index => $queue) { - if ($this->queuePaused($connection->getConnectionName(), $queue)) { + if ($this->queuePaused($driver->getConnectionName(), $queue)) { continue; } if (! is_null($job = $popJobCallback($queue, $index))) { - $this->raiseAfterJobPopEvent($connection->getConnectionName(), $job); + $this->raiseAfterJobPopEvent($driver->getConnectionName(), $job); return $job; } @@ -356,7 +359,7 @@ protected function queuePaused(string $connectionName, string $queue): bool protected function runJob(Job $job, string $connectionName, WorkerOptions $options): void { try { - return $this->process($connectionName, $job, $options); + $this->process($connectionName, $job, $options); } catch (Throwable $e) { if (static::$reportJobExceptions) { logger()->error($e->getMessage()); @@ -397,7 +400,9 @@ public function process(string $connectionName, Job $job, WorkerOptions $options ); if ($job->isDeleted()) { - return $this->raiseAfterJobEvent($connectionName, $job); + $this->raiseAfterJobEvent($connectionName, $job); + + return; } // Here we will fire off the job and let it process. We will catch any exceptions, so @@ -507,12 +512,12 @@ protected function markJobAsFailedIfWillExceedMaxExceptions(string $connectionNa return; } - if (! $this->cache->get('job-exceptions:'.$uuid)) { - $this->cache->set('job-exceptions:'.$uuid, 0, Date::now()->addDay()->getTimestamp()); + if (! $this->cache->get('job-exceptions-'.$uuid)) { + $this->cache->set('job-exceptions-'.$uuid, 0, Date::now()->addDay()->getTimestamp()); } - if ($maxExceptions <= $this->cache->increment('job-exceptions:'.$uuid)) { - $this->cache->delete('job-exceptions:'.$uuid); + if ($maxExceptions <= $this->cache->increment('job-exceptions-'.$uuid)) { + $this->cache->delete('job-exceptions-'.$uuid); $this->failJob($job, $e); } @@ -621,7 +626,7 @@ protected function getTimestampOfLastQueueRestart(): ?int } if ($this->cache) { - return (int) $this->cache->get('blitzphp:queue:restart'); + return (int) $this->cache->get('blitzphp-queue-restart'); } return null; @@ -712,7 +717,7 @@ public function sleep(int|float $seconds): void /** * Set the cache repository implementation. */ - public function setCache(Cache $cache): self + public function setCache(CacheInterface $cache): self { $this->cache = $cache; From 3ec097f6ba960c65856f9a7543ace4a62c1ffed3 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 27 Aug 2026 19:00:10 +0100 Subject: [PATCH 3/5] chore: phpdocs en francais --- composer.json | 8 +- src/CallQueuedClosure.php | 25 ++- src/CallQueuedHandler.php | 9 +- src/Commands/Work.php | 96 ++++---- src/Compatibility/SignalTrait.php | 122 +++++------ src/Config/Services.php | 10 +- src/Config/queue.php | 207 ++++++++++++++++++ src/DTO/Config.php | 28 ++- src/DTO/WorkerOptions.php | 30 ++- .../2026-08-26-061438_CreateQueueTables.php | 9 + src/Drivers/ConnectorInterface.php | 5 +- src/Drivers/DatabaseDriver.php | 67 +++--- src/Drivers/FailoverDriver.php | 35 +-- src/Drivers/NullDriver.php | 29 +-- src/Drivers/SyncDriver.php | 45 ++-- src/Enums/WorkerStopReason.php | 11 + src/Events/QueueEvent.php | 55 +++-- src/Events/QueueEventManager.php | 48 ++-- src/Exceptions/InvalidPayloadException.php | 7 +- src/Exceptions/ManuallyFailedException.php | 3 + .../MaxAttemptsExceededException.php | 7 +- src/Exceptions/QueueException.php | 64 ------ src/Exceptions/TimeoutExceededException.php | 5 +- src/Failed/CountableFailedJobProvider.php | 5 +- src/Failed/DatabaseFailedJobProvider.php | 37 ++-- src/Failed/DatabaseUuidFailedJobProvider.php | 29 +-- src/Failed/FailedJobProviderInterface.php | 15 +- src/Failed/FileFailedJobProvider.php | 33 +-- src/Failed/NullFailedJobProvider.php | 3 + src/Failed/PrunableFailedJobProvider.php | 5 +- src/Job.php | 33 ++- src/Jobs/DatabaseJob.php | 21 +- src/Jobs/DatabaseJobRecord.php | 13 +- src/Jobs/FakeJob.php | 21 +- src/Jobs/InspectedJob.php | 19 +- src/Jobs/Job.php | 87 ++++---- src/Jobs/JobName.php | 9 +- src/Jobs/SyncJob.php | 19 +- src/Manager.php | 77 ++++--- src/Models/JobModel.php | 48 ++-- src/Providers/QueueProvider.php | 3 + src/Queue.php | 84 +++---- src/Traits/Dispatchable.php | 3 + src/Traits/InteractsWithQueue.php | 33 +-- .../SerializesAndRestoresModelIdentifiers.php | 13 +- src/Traits/SerializesModels.php | 9 +- src/Worker.php | 167 +++++++------- src/WorkerOptions.php | 41 ++++ 48 files changed, 1087 insertions(+), 665 deletions(-) delete mode 100644 src/Exceptions/QueueException.php create mode 100644 src/WorkerOptions.php diff --git a/composer.json b/composer.json index 4274556..65aab65 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ }, "require-dev": { "blitz-php/coding-standard": "^1.4", - "blitz-php/framework": "^0.11.3", + "blitz-php/framework": "^0.12.4", "kahlan/kahlan": "^6.0", "phpstan/phpstan": "^2.1", "predis/predis": "^2.0 || ^3.0", @@ -38,9 +38,9 @@ } }, "suggest": { - "ext-redis": "If you want to use RedisHandler", - "predis/predis": "If you want to use PredisHandler", - "php-amqplib/php-amqplib": "If you want to use RabbitMQHandler" + "ext-redis": "Si vous souhaitez utiliser RedisDriver", + "predis/predis": "Si vous souhaitez utiliser PredisDriver", + "php-amqplib/php-amqplib": "Si vous souhaitez utiliser RabbitMQDriver" }, "scripts": { "test": "vendor/bin/kahlan", diff --git a/src/CallQueuedClosure.php b/src/CallQueuedClosure.php index ffd60ed..5f14d88 100644 --- a/src/CallQueuedClosure.php +++ b/src/CallQueuedClosure.php @@ -11,34 +11,37 @@ use ReflectionFunction; use Throwable; +/** + * Job enveloppe d'une Closure sérialisable, exécutable par le worker. + */ class CallQueuedClosure { use Dispatchable, InteractsWithQueue, SerializesModels; /** - * The serializable Closure instance. + * Instance de Closure sérialisable. * * @var \Laravel\SerializableClosure\SerializableClosure */ public $closure; /** - * The name assigned to the job. + * Nom assigné au job. */ public ?string $name = null; /** - * The callbacks that should be executed on failure. + * Callbacks à exécuter en cas d'échec. */ public array $failureCallbacks = []; /** - * Indicate if the job should be deleted when models are missing. + * Indique si le job doit être supprimé lorsque des modèles sont introuvables. */ public bool $deleteWhenMissingModels = true; /** - * Create a new job instance. + * Crée une nouvelle instance de job. */ public function __construct(SerializableClosure $closure) { @@ -46,7 +49,7 @@ public function __construct(SerializableClosure $closure) } /** - * Create a new job instance. + * Crée une nouvelle instance de job. */ public static function create(Closure $job): self { @@ -54,7 +57,7 @@ public static function create(Closure $job): self } /** - * Execute the job. + * Exécute le job. */ public function handle(ContainerInterface $container): void { @@ -62,7 +65,7 @@ public function handle(ContainerInterface $container): void } /** - * Add a callback to be executed if the job fails. + * Ajoute un callback exécuté si le job échoue. */ public function onFailure(callable $callback): self { @@ -74,7 +77,7 @@ public function onFailure(callable $callback): self } /** - * Handle a job failure. + * Traite l'échec du job. */ public function failed(Throwable $e):void { @@ -84,7 +87,7 @@ public function failed(Throwable $e):void } /** - * Get the display name for the queued job. + * Retourne le nom d'affichage du job enfilé. */ public function displayName(): string { @@ -100,7 +103,7 @@ public function displayName(): string } /** - * Assign a name to the job. + * Assigne un nom au job. */ public function name(string $name): self { diff --git a/src/CallQueuedHandler.php b/src/CallQueuedHandler.php index 486a70e..170bc13 100644 --- a/src/CallQueuedHandler.php +++ b/src/CallQueuedHandler.php @@ -11,6 +11,9 @@ use RuntimeException; use Throwable; +/** + * Handler invoqué par le worker pour désérialiser et exécuter un job utilisateur. + */ class CallQueuedHandler { /** @@ -21,8 +24,8 @@ public function __construct(protected ContainerInterface $container) } /** - * Handle the queued job. - * C'est la méthode appelée par le worker via JobName::parse() + * Traite le job enfilé. + * Méthode invoquée par le worker via JobName::parse(). */ public function call(Job $job, array $data): void { @@ -103,7 +106,7 @@ protected function getCommand(array $data): mixed } /** - * Set the job instance of the given class if necessary. + * Attache l'instance de job au handler si le trait InteractsWithQueue est utilisé. */ protected function setJobInstanceIfNecessary(Job $job, mixed $instance): mixed { diff --git a/src/Commands/Work.php b/src/Commands/Work.php index dc3bbe8..7baee5e 100644 --- a/src/Commands/Work.php +++ b/src/Commands/Work.php @@ -19,6 +19,9 @@ use Psr\Log\LoggerInterface; use Throwable; +/** + * Commande console `queue:work` : traite les jobs en daemon ou un par un. + */ class Work extends Command { use InteractsWithTime; @@ -30,60 +33,66 @@ class Work extends Command protected $name = 'queue:work'; /** @var string Description de la commande */ - protected $description = 'Start processing jobs on the queue as a daemon'; + protected $description = 'Traite les jobs de la file d\'attente en mode daemon'; /** @var array Arguments de la commande */ protected $arguments = [ - 'connection' => 'The name of the queue connection to work', + 'connection' => 'Nom de la connexion de file à traiter', ]; /** @var array Options de la commande */ protected $options = [ - '--name' => ['The name of the worker', 'default'], - '--queue' => ['The names of the queues to work'], - '--daemon' => ['Run the worker in daemon mode (Deprecated)'], - '--once' => ['Only process the next job on the queue'], - '--stop-when-empty' => ['Stop when the queue is empty'], - '--delay' => ['The number of seconds to delay failed jobs (Deprecated)', 0], - '--backoff' => ['The number of seconds to wait before retrying a job that encountered an uncaught exception', 0], - '--max-jobs' => ['The number of jobs to process before stopping', 0], - '--max-time' => ['The maximum number of seconds the worker should run', 0], - '--force' => ['Force the worker to run even in maintenance mode'], - '--memory' => ['The memory limit in megabytes', 128], - '--sleep' => ['The number of seconds to sleep when no job is available', 3], - '--rest' => ['The number of seconds to rest between jobs', 0], - '--timeout' => ['The number of seconds a child process can run', 60], - '--tries' => ['The number of times to attempt a job before logging it failed', 1], - '--json' => ['Output the queue worker information as JSON'], + '--name' => ['Nom du worker', 'default'], + '--queue' => ['Noms des files à traiter (séparés par des virgules)'], + '--daemon' => ['Exécute le worker en mode daemon (obsolète)'], + '--once' => ['Ne traite que le prochain job de la file'], + '--stop-when-empty' => ['S\'arrête lorsque la file est vide'], + '--delay' => ['Secondes de délai avant retry d\'un job échoué (obsolète)', 0], + '--backoff' => ['Secondes d\'attente avant de relancer un job ayant levé une exception', 0], + '--max-jobs' => ['Nombre de jobs à traiter avant arrêt', 0], + '--max-time' => ['Durée maximale d\'exécution du worker (secondes)', 0], + '--force' => ['Force l\'exécution même en mode maintenance'], + '--memory' => ['Limite mémoire en mégaoctets', 128], + '--sleep' => ['Secondes d\'attente lorsqu\'aucun job n\'est disponible', 3], + '--rest' => ['Secondes de pause entre deux jobs', 0], + '--timeout' => ['Durée maximale d\'un processus enfant (secondes)', 60], + '--tries' => ['Nombre de tentatives avant d\'enregistrer l\'échec', 1], + '--json' => ['Affiche les informations du worker au format JSON'], ]; /** - * The queue worker instance. + * Instance du worker de file. */ protected Worker $worker; /** - * The cache store implementation. + * Implémentation du cache. */ protected CacheInterface $cache; + /** + * Gestionnaire d'événements de l'application. + */ protected EventManagerInterface $events; /** - * Holds the start time of the last processed job, if any. + * Horodatage de début du dernier job traité, s'il y en a un. */ protected ?float $latestStartedAt = null; /** - * Indicates if the worker's event listeners have been registered. + * Indique si les écouteurs d'événements du worker ont été enregistrés. */ private static bool $hasRegisteredListeners = false; + /** + * Indique si `stty` est disponible (null = pas encore sondé). + */ private static ?bool $stty = null; /** - * Create a new queue work command. + * Crée la commande de traitement de la file. */ public function __construct(protected ContainerInterface $container, protected Console $app) { @@ -97,7 +106,7 @@ public function __construct(protected ContainerInterface $container, protected C } /** - * Execute the console command. + * Exécute la commande console. * * @return int|null */ @@ -109,16 +118,12 @@ public function execute(array $params) return $this->worker->sleep($this->option('sleep')); } - // We'll listen to the processed and failed events so we can write information - // to the console as jobs are processed, which will let the developer watch - // which jobs are coming through a queue and be informed on its progress. + // Écoute des événements de succès / échec pour afficher la progression en console. $this->listenForEvents(); $connection = $this->argument('connection') ?: config('queue.default'); - // We need to get the right queue for the connection which is set in the queue - // configuration file for the application. We will pull it based on the set - // connection being run for the queue operation currently being executed. + // File cible : option --queue, sinon valeur de configuration de la connexion. $queue = $this->getQueue($connection); if (! $this->outputUsingJson() && static::terminalHasSttyAvailable()) { @@ -133,7 +138,7 @@ public function execute(array $params) } /** - * Run the worker instance. + * Lance l'instance du worker. */ protected function runWorker(string $connection, string $queue): ?int { @@ -146,7 +151,7 @@ protected function runWorker(string $connection, string $queue): ?int } /** - * Gather all of the queue worker options as a single object. + * Regroupe les options du worker dans un seul objet. */ protected function gatherWorkerOptions(): WorkerOptions { @@ -166,7 +171,7 @@ protected function gatherWorkerOptions(): WorkerOptions } /** - * Listen for the queue events in order to update the console output. + * Écoute les événements de file pour mettre à jour la sortie console. */ protected function listenForEvents(): void { @@ -196,7 +201,7 @@ protected function listenForEvents(): void } /** - * Write the status output for the queue worker for JSON or TTY. + * Affiche l'état du worker (JSON ou TTY). */ protected function writeOutput(Job $job, string $status, ?Throwable $exception = null): void { @@ -210,7 +215,7 @@ protected function writeOutput(Job $job, string $status, ?Throwable $exception = } /** - * Write the status output for the queue worker. + * Affiche l'état du worker dans le terminal. */ protected function writeOutputForCli(Job $job, string $status): void { @@ -247,7 +252,7 @@ protected function writeOutputForCli(Job $job, string $status): void } /** - * Write the status output for the queue worker in JSON format. + * Affiche l'état du worker au format JSON. */ protected function writeOutputAsJson(Job $job, $status, ?Throwable $exception = null): void { @@ -281,7 +286,7 @@ protected function writeOutputAsJson(Job $job, $status, ?Throwable $exception = } /** - * Get the current date / time. + * Retourne la date et l'heure courantes. */ protected function now(): Date { @@ -295,7 +300,7 @@ protected function now(): Date } /** - * Store a failed job event. + * Enregistre un événement de job échoué. */ protected function logFailedJob(QueueEvent $event): void { @@ -308,7 +313,7 @@ protected function logFailedJob(QueueEvent $event): void } /** - * Get the queue name for the worker. + * Retourne le nom de file à traiter par le worker. */ protected function getQueue(string $connection): string { @@ -318,7 +323,7 @@ protected function getQueue(string $connection): string } /** - * Determine if the worker should run in maintenance mode. + * Indique si l'application est en maintenance (et si le worker doit s'arrêter). */ protected function downForMaintenance(): false { @@ -328,7 +333,7 @@ protected function downForMaintenance(): false } /** - * Determine if the worker should output using JSON. + * Indique si la sortie du worker doit être en JSON. */ protected function outputUsingJson(): bool { @@ -336,19 +341,24 @@ protected function outputUsingJson(): bool } /** - * Reset static variables. + * Réinitialise les variables statiques. */ public static function flushState(): void { static::$hasRegisteredListeners = false; } + /** + * Indique si la sortie console est silencieuse (non CLI ou mode suppress). + */ protected function isSilent(): bool { return $this->suppress || !is_cli(); } /** + * Indique si le terminal courant prend en charge `stty`. + * * @internal */ protected static function terminalHasSttyAvailable(): bool @@ -357,7 +367,7 @@ protected static function terminalHasSttyAvailable(): bool return self::$stty; } - // skip check if shell_exec function is disabled + // Pas de vérification si shell_exec est désactivé if (!\function_exists('shell_exec')) { return false; } diff --git a/src/Compatibility/SignalTrait.php b/src/Compatibility/SignalTrait.php index c724ff4..a9b6f01 100644 --- a/src/Compatibility/SignalTrait.php +++ b/src/Compatibility/SignalTrait.php @@ -11,51 +11,51 @@ trait SignalTrait } } else { /** - * Signal Trait + * Trait de gestion des signaux. * - * Provides PCNTL signal handling capabilities for CLI commands. - * Requires the PCNTL extension (Unix only). + * Fournit la gestion des signaux PCNTL pour les commandes CLI. + * Nécessite l'extension PCNTL (Unix uniquement). * - * Bundled compatibility version for BlitzPHP < 1.2 + * Version de compatibilité fournie pour BlitzPHP < 1.2. */ trait SignalTrait { /** - * Whether the process should continue running (false = termination requested). + * Indique si le processus doit continuer (false = arrêt demandé). */ private bool $running = true; /** - * Whether signals are currently blocked. + * Indique si les signaux sont actuellement bloqués. */ private bool $signalsBlocked = false; /** - * Array of registered signals. + * Liste des signaux enregistrés. * * @var list */ private array $registeredSignals = []; /** - * Signal-to-method mapping. + * Correspondance signal → méthode. * * @var array */ private array $signalMethodMap = []; /** - * Cached result of PCNTL extension availability. + * Résultat mis en cache de la disponibilité de l'extension PCNTL. */ private static ?bool $isPcntlAvailable = null; /** - * Cached result of POSIX extension availability. + * Résultat mis en cache de la disponibilité de l'extension POSIX. */ private static ?bool $isPosixAvailable = null; /** - * Check if PCNTL extension is available (cached). + * Indique si l'extension PCNTL est disponible (valeur mise en cache). */ protected function isPcntlAvailable(): bool { @@ -74,7 +74,7 @@ protected function isPcntlAvailable(): bool } /** - * Check if POSIX extension is available (cached). + * Indique si l'extension POSIX est disponible (valeur mise en cache). */ protected function isPosixAvailable(): bool { @@ -86,10 +86,10 @@ protected function isPosixAvailable(): bool } /** - * Register signal handlers. + * Enregistre les gestionnaires de signaux. * - * @param list $signals List of signals to handle - * @param array $methodMap Optional signal-to-method mapping + * @param list $signals Liste des signaux à traiter. + * @param array $methodMap Correspondance optionnelle signal → méthode. */ protected function registerSignals( array $signals = [], @@ -107,7 +107,7 @@ protected function registerSignals( // CLI::write('POSIX extension is not available. SIGTSTP and SIGCONT signals will be disabled.', 'yellow'); $signals = array_diff($signals, [SIGTSTP, SIGCONT]); - // Remove from method map as well + // Retire aussi les associations de méthodes unset($methodMap[SIGTSTP], $methodMap[SIGCONT]); if ($signals === []) { @@ -115,7 +115,7 @@ protected function registerSignals( } } - // Enable async signals for immediate response + // Active les signaux asynchrones pour une réaction immédiate pcntl_async_signals(true); $this->signalMethodMap = $methodMap; @@ -131,13 +131,13 @@ protected function registerSignals( } /** - * Handle incoming signals. + * Traite les signaux reçus. */ protected function handleSignal(int $signal): void { $this->callCustomHandler($signal); - // Apply standard Unix signal behavior for registered signals + // Applique le comportement Unix standard pour les signaux enregistrés switch ($signal) { case SIGTERM: case SIGINT: @@ -147,25 +147,25 @@ protected function handleSignal(int $signal): void break; case SIGTSTP: - // Restore default handler and re-send signal to actually suspend + // Restaure le handler par défaut et renvoie le signal pour suspendre vraiment pcntl_signal(SIGTSTP, SIG_DFL); posix_kill(posix_getpid(), SIGTSTP); break; case SIGCONT: - // Re-register SIGTSTP handler after resume + // Réenregistre le handler SIGTSTP après reprise pcntl_signal(SIGTSTP, [$this, 'handleSignal']); break; } } /** - * Call custom signal handler if one is mapped for this signal. - * Falls back to generic onInterruption() method if no explicit mapping exists. + * Appelle le gestionnaire personnalisé s'il est associé à ce signal. + * Se rabat sur onInterruption() si aucune association explicite n'existe. */ private function callCustomHandler(int $signal): void { - // Check for explicit mapping first + // Association explicite en priorité $method = $this->signalMethodMap[$signal] ?? null; if ($method !== null && method_exists($this, $method)) { @@ -174,14 +174,14 @@ private function callCustomHandler(int $signal): void return; } - // If no explicit mapping, try generic catch-all method + // Si aucune association, tente la méthode générique onInterruption() if (method_exists($this, 'onInterruption')) { // @phpstan-ignore-line $this->onInterruption($signal); } } /** - * Check if command should terminate. + * Indique si la commande doit s'arrêter. */ protected function shouldTerminate(): bool { @@ -189,7 +189,7 @@ protected function shouldTerminate(): bool } /** - * Check if the process is currently running (not terminated). + * Indique si le processus est encore en cours d'exécution. */ protected function isRunning(): bool { @@ -197,7 +197,7 @@ protected function isRunning(): bool } /** - * Request immediate termination. + * Demande l'arrêt immédiat. */ protected function requestTermination(): void { @@ -205,28 +205,28 @@ protected function requestTermination(): void } /** - * Reset all states (for testing or restart scenarios). + * Réinitialise tous les états (tests ou redémarrage). */ protected function resetState(): void { $this->running = true; - // Unblock signals if they were blocked + // Débloque les signaux s'ils l'étaient if ($this->signalsBlocked) { $this->unblockSignals(); } } /** - * Execute a callable with ALL signals blocked to prevent ANY interruption during critical operations. + * Exécute un callable en bloquant tous les signaux pour éviter toute interruption. * - * This blocks ALL interruptible signals including: - * - Termination signals (SIGTERM, SIGINT, etc.) - * - Pause/resume signals (SIGTSTP, SIGCONT) - * - Custom signals (SIGUSR1, SIGUSR2) + * Bloque tous les signaux interruptibles, notamment : + * - signaux de terminaison (SIGTERM, SIGINT, etc.) + * - pause / reprise (SIGTSTP, SIGCONT) + * - signaux personnalisés (SIGUSR1, SIGUSR2) * - * Only SIGKILL (unblockable) can still terminate the process. - * Use this for database transactions, file operations, or any critical atomic operations. + * Seul SIGKILL (non bloquable) peut encore terminer le processus. + * À utiliser pour les transactions SQL, les I/O fichiers ou toute opération atomique critique. * * @template TReturn * @@ -246,42 +246,42 @@ protected function withSignalsBlocked(Closure $operation) } /** - * Block ALL interruptible signals during critical sections. - * Only SIGKILL (unblockable) can terminate the process. + * Bloque tous les signaux interruptibles pendant une section critique. + * Seul SIGKILL (non bloquable) peut encore terminer le processus. */ protected function blockSignals(): void { if (! $this->signalsBlocked && $this->isPcntlAvailable()) { - // Block ALL signals that could interrupt critical operations + // Bloque tous les signaux susceptibles d'interrompre une section critique pcntl_sigprocmask(SIG_BLOCK, [ - SIGTERM, SIGINT, SIGHUP, SIGQUIT, // Termination signals - SIGTSTP, SIGCONT, // Pause/resume signals - SIGUSR1, SIGUSR2, // Custom signals - SIGPIPE, SIGALRM, // Other common signals + SIGTERM, SIGINT, SIGHUP, SIGQUIT, // Signaux de terminaison + SIGTSTP, SIGCONT, // Pause / reprise + SIGUSR1, SIGUSR2, // Signaux personnalisés + SIGPIPE, SIGALRM, // Autres signaux courants ]); $this->signalsBlocked = true; } } /** - * Unblock previously blocked signals. + * Débloque les signaux précédemment bloqués. */ protected function unblockSignals(): void { if ($this->signalsBlocked && $this->isPcntlAvailable()) { - // Unblock the same signals we blocked + // Débloque les mêmes signaux qu'on a bloqués pcntl_sigprocmask(SIG_UNBLOCK, [ - SIGTERM, SIGINT, SIGHUP, SIGQUIT, // Termination signals - SIGTSTP, SIGCONT, // Pause/resume signals - SIGUSR1, SIGUSR2, // Custom signals - SIGPIPE, SIGALRM, // Other common signals + SIGTERM, SIGINT, SIGHUP, SIGQUIT, // Signaux de terminaison + SIGTSTP, SIGCONT, // Pause / reprise + SIGUSR1, SIGUSR2, // Signaux personnalisés + SIGPIPE, SIGALRM, // Autres signaux courants ]); $this->signalsBlocked = false; } } /** - * Check if signals are currently blocked. + * Indique si les signaux sont actuellement bloqués. */ protected function signalsBlocked(): bool { @@ -289,7 +289,7 @@ protected function signalsBlocked(): bool } /** - * Add or update signal-to-method mapping at runtime. + * Ajoute ou met à jour une association signal → méthode à l'exécution. */ protected function mapSignal(int $signal, string $method): void { @@ -297,7 +297,7 @@ protected function mapSignal(int $signal, string $method): void } /** - * Get human-readable signal name. + * Retourne le nom lisible du signal. */ protected function getSignalName(int $signal): string { @@ -317,7 +317,7 @@ protected function getSignalName(int $signal): string } /** - * Unregister all signals (cleanup). + * Désenregistre tous les signaux (nettoyage). */ protected function unregisterSignals(): void { @@ -334,7 +334,7 @@ protected function unregisterSignals(): void } /** - * Check if signals are registered. + * Indique si des signaux sont enregistrés. */ protected function hasSignals(): bool { @@ -342,7 +342,7 @@ protected function hasSignals(): bool } /** - * Get list of registered signals. + * Retourne la liste des signaux enregistrés. * * @return list */ @@ -352,7 +352,7 @@ protected function getSignals(): array } /** - * Get comprehensive process state information. + * Retourne un état complet du processus. * * @return array{ * pid: int, @@ -373,23 +373,23 @@ protected function getProcessState(): array { $pid = getmypid(); $state = [ - // Process identification + // Identification du processus 'pid' => $pid, 'running' => $this->running, - // Signal handling status + // État de la gestion des signaux 'pcntl_available' => $this->isPcntlAvailable(), 'registered_signals' => count($this->registeredSignals), 'registered_signals_names' => array_map([$this, 'getSignalName'], $this->registeredSignals), 'signals_blocked' => $this->signalsBlocked, 'explicit_mappings' => count($this->signalMethodMap), - // System resources + // Ressources système 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2), 'memory_peak_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2), ]; - // Add terminal control info if POSIX extension is available + // Infos de contrôle de terminal si l'extension POSIX est disponible if ($this->isPosixAvailable()) { $state['session_id'] = posix_getsid($pid); $state['process_group'] = posix_getpgid($pid); diff --git a/src/Config/Services.php b/src/Config/Services.php index 44d4efb..d0c7d8e 100644 --- a/src/Config/Services.php +++ b/src/Config/Services.php @@ -14,10 +14,13 @@ use BlitzPHP\Queue\Manager; use BlitzPHP\Queue\Worker; +/** + * Fabrique des services liés à la file d'attente (gestionnaire, worker, jobs échoués). + */ class Services extends BaseServices { /** - * Queue Manager + * Gestionnaire de files d'attente. */ public static function queue(array $config = [], bool $shared = true): Manager { @@ -34,7 +37,7 @@ public static function queue(array $config = [], bool $shared = true): Manager } /** - * Queue Worker + * Worker de file d'attente. */ public static function worker(bool $shared = true): Worker { @@ -77,6 +80,9 @@ public static function worker(bool $shared = true): Worker ); } + /** + * Fournisseur de jobs échoués selon `queue.failed.driver`. + */ public static function queueFailer(array $config = [], bool $shared = true): FailedJobProviderInterface { if (true === $shared && isset(static::$instances[FailedJobProviderInterface::class])) { diff --git a/src/Config/queue.php b/src/Config/queue.php index 163db11..820fb1b 100644 --- a/src/Config/queue.php +++ b/src/Config/queue.php @@ -1,56 +1,263 @@ env('queue.connection', 'database'), + /** + * Définitions des connexions disponibles. + * + * Chaque entrée est identifiée par un nom (utilisé comme `driver` si la + * clé `driver` n'est pas fournie) et contient les options propres au backend. + */ 'connections' => [ + /** + * File d'attente persistée en base de données. + * + * Les jobs sont stockés dans une table SQL, réservés par le worker + * (verrouillage de lignes) puis supprimés ou relâchés selon le résultat. + */ 'database' => [ + /** + * Groupe / nom de connexion base de données BlitzPHP à utiliser + * pour lire et écrire les jobs. Variable : `queue.database.group`. + */ 'group' => env('queue.database.group', 'default'), + + /** + * Si `true`, réutilise une connexion partagée du gestionnaire de + * connexions plutôt que d'en ouvrir une dédiée au worker. + */ 'shared' => true, + + /** + * Si `true`, tente d'utiliser un verrouillage de type + * `SKIP LOCKED` / `READPAST` (selon le moteur) afin que plusieurs + * workers ne récupèrent pas le même job. + */ 'skip_locked' => true, + + /** + * Nom de la table contenant les jobs en attente, retardés ou + * réservés. Variable : `queue.database.table`. + */ 'table' => env('queue.database.table', 'queue_jobs'), + + /** + * Nom de la file logique par défaut pour cette connexion + * (colonne `queue` en base). Utilisé si `queue:work` n'en précise pas. + */ + // 'queue' => 'default', + + /** + * Délai en secondes au-delà duquel un job réservé est considéré + * comme expiré et peut être repris par un autre worker. + */ + // 'retry_after' => 60, + + /** + * Si `true`, n'envoie le job qu'après le commit des transactions + * de base de données en cours. + */ + // 'after_commit' => false, ], + + /** + * Connexion Redis (extension PHP `redis` / PhpRedis). + * + * Les jobs sont poussés dans des listes Redis. Le pilote correspondant + * doit être enregistré dans `drivers` pour être utilisable. + */ 'redis' => [ + /** + * Identifiant du pilote à instancier (`drivers.redis`). + */ 'driver' => 'redis', + + /** + * Hôte du serveur Redis. Variable : `redis.host`. + */ 'host' => env('redis.host', '127.0.0.1'), + + /** + * Mot de passe d'authentification Redis, ou `null` si aucun. + * Variable : `redis.password`. + */ 'password' => env('redis.password', null), + + /** + * Port TCP du serveur Redis. Variable : `redis.port`. + */ 'port' => env('redis.port', 6379), + + /** + * Index de la base Redis (0–15 en configuration par défaut). + * Variable : `redis.database`. + */ 'database' => env('redis.database', 0), ], + + /** + * Connexion Redis via Predis (client PHP pur, sans extension). + * + * Utile lorsque l'extension `redis` n'est pas disponible. + */ 'predis' => [ + /** + * Identifiant du pilote à instancier (`drivers.predis`). + */ 'driver' => 'predis', + + /** + * Schéma de connexion (`tcp`, `tls`, `unix`). + */ 'scheme' => 'tcp', + + /** + * Hôte du serveur Redis. Variable : `redis.host`. + */ 'host' => env('redis.host', '127.0.0.1'), + + /** + * Mot de passe d'authentification Redis, ou `null` si aucun. + * Variable : `redis.password`. + */ 'password' => env('redis.password', null), + + /** + * Port TCP du serveur Redis. Variable : `redis.port`. + */ 'port' => env('redis.port', 6379), + + /** + * Index de la base Redis. Variable : `redis.database`. + */ 'database' => env('redis.database', 0), ], + + /** + * Connexion RabbitMQ (AMQP). + * + * Les jobs sont publiés dans des files AMQP. Le pilote correspondant + * doit être enregistré dans `drivers` pour être utilisable. + */ 'rabbitmq' => [ + /** + * Identifiant du pilote à instancier (`drivers.rabbitmq`). + */ 'driver' => 'rabbitmq', + + /** + * Hôte du courtier RabbitMQ. Variable : `rabbitmq.host`. + */ 'host' => env('rabbitmq.host', '127.0.0.1'), + + /** + * Port AMQP (5672 en clair, 5671 en TLS en général). + * Variable : `rabbitmq.port`. + */ 'port' => env('rabbitmq.port', 5672), + + /** + * Nom d'utilisateur AMQP. Variable : `rabbitmq.user`. + */ 'user' => env('rabbitmq.user', 'guest'), + + /** + * Mot de passe AMQP. Variable : `rabbitmq.password`. + */ 'password' => env('rabbitmq.password', 'guest'), + + /** + * Hôte virtuel (vhost) isolant les files et échanges. + * Variable : `rabbitmq.vhost`. + */ 'vhost' => env('rabbitmq.vhost', '/'), ], ], + /** + * Correspondance entre le nom d'un pilote et sa classe PHP. + * + * La classe doit implémenter `ConnectorInterface` et exposer + * `connect(ContainerInterface $container, array $config)`. + * Les connexions dont le pilote n'est pas listé ici ne peuvent pas être résolues. + */ 'drivers' => [ + /** + * Pilote SQL : table `queue_jobs` (ou celle configurée). + */ 'database' => \BlitzPHP\Queue\Drivers\DatabaseDriver::class, // 'redis' => \BlitzPHP\Queue\Drivers\Redis::class, // 'predis' => \BlitzPHP\Queue\Drivers\Predis::class, // 'rabbitmq' => \BlitzPHP\Queue\Drivers\RabbitMQ::class, ], + /** + * Si `true`, les jobs définitivement en échec sont conservés via le + * fournisseur configuré dans `failed` (base, fichier, etc.). + * Si `false`, l'échec est uniquement journalisé / ignoré selon le fournisseur. + */ 'keep_failed_jobs' => true, + /** + * Stockage des jobs échoués (après épuisement des tentatives ou échec manuel). + */ 'failed' => [ + /** + * Fournisseur de persistance : + * - `database` : identifiants numériques auto-incrémentés + * - `database-uuids` : UUID du payload comme identifiant (recommandé) + * - `file` : fichier JSON (voir `path` / `limit` côté service) + * - `null` : aucun stockage + * + * Variable : `queue.failed_driver`. + */ 'driver' => env('queue.failed_driver', 'database-uuids'), + + /** + * Nom de la connexion base de données utilisée pour la table des échecs + * (pilotes `database` et `database-uuids`). Variable : `db.connection`. + */ 'database' => env('db.connection', 'default'), + + /** + * Table SQL des jobs échoués (`uuid`, `connection`, `queue`, `payload`, + * `exception`, `failed_at`). + */ 'table' => 'queue_failed_jobs', ], + /** + * Stockage des lots de jobs (batching) : suivi d'un groupe de jobs + * dispatchés ensemble (progression, annulation, callbacks de fin). + */ 'batching' => [ + /** + * Connexion base de données pour la table des lots. + * Variable : `db.connection`. + */ 'database' => env('db.connection', 'default'), + + /** + * Nom de la table (ou identifiant de stockage) des lots de jobs. + */ 'table' => 'queue.job_batches', ], ]; diff --git a/src/DTO/Config.php b/src/DTO/Config.php index dba81ef..fbb3115 100644 --- a/src/DTO/Config.php +++ b/src/DTO/Config.php @@ -4,6 +4,12 @@ use BlitzPHP\Queue\Drivers\ConnectorInterface; use InvalidArgumentException; +/** + * Représentation objet de la configuration `queue.php`. + * + * Sert au gestionnaire pour résoudre la connexion par défaut, les options + * de chaque backend, les classes de pilotes et le stockage des échecs. + */ class Config { /** @@ -69,7 +75,13 @@ public function toArray(): array } /** - * Récupère une connexion spécifique + * Retourne la configuration d'une connexion, ou un pilote `null` si le nom est vide. + * + * @param string|null $name Nom de la connexion (`connections.{name}`). + * + * @return array + * + * @throws InvalidArgumentException Si la connexion n'est pas définie. */ public function connection(?string $name): array { @@ -85,7 +97,13 @@ public function connection(?string $name): array } /** - * @return class-string + * Retourne le nom de classe du pilote enregistré pour le nom donné. + * + * @param string $name Nom du pilote (ex. `database`). + * + * @return class-string + * + * @throws InvalidArgumentException Si le pilote n'est pas enregistré ou n'implémente pas le contrat. */ public function driver(string $name): string { @@ -102,8 +120,10 @@ public function driver(string $name): string return $driver; } - /** - * Set the name of the default queue connection. + /** + * Définit le nom de la connexion de file d'attente par défaut. + * + * Met aussi à jour la configuration runtime `queue.default`. */ public function setDefaultDriver(string $name): void { diff --git a/src/DTO/WorkerOptions.php b/src/DTO/WorkerOptions.php index c41e330..124464f 100644 --- a/src/DTO/WorkerOptions.php +++ b/src/DTO/WorkerOptions.php @@ -2,22 +2,28 @@ namespace BlitzPHP\Queue\DTO; +/** + * Options d'exécution d'un worker de file d'attente. + * + * Ces valeurs sont généralement renseignées par la commande `queue:work` + * et contrôlent la durée de vie, les limites et le comportement du processus. + */ class WorkerOptions { /** - * Create a new worker options instance. + * Crée une instance d'options du worker. * - * @param string $name The name of the worker. - * @param int|int[] $backoff The number of seconds to wait before retrying a job that encountered an uncaught exception. - * @param int $memory The maximum amount of RAM the worker may consume. - * @param int $timeout The maximum number of seconds a child worker may run. - * @param int $sleep The number of seconds to wait in between polling the queue. - * @param int $maxTries The maximum number of times a job may be attempted. - * @param bool $force Indicates if the worker should run in maintenance mode. - * @param bool $stopWhenEmpty Indicates if the worker should stop when the queue is empty. - * @param int $maxJobs The maximum number of jobs to run. - * @param int $maxTime The maximum number of seconds a worker may live. - * @param int $rest The number of seconds to rest between jobs. + * @param string $name Nom du worker (utilisé pour les callbacks de pop personnalisés). + * @param int|int[] $backoff Secondes d'attente avant de relancer un job ayant levé une exception non gérée. + * @param int $memory Mémoire maximale autorisée (Mo) avant arrêt du worker. + * @param int $timeout Durée maximale d'exécution d'un job enfant (secondes). + * @param int $sleep Secondes d'attente entre deux sondages lorsque la file est vide. + * @param int $maxTries Nombre maximal de tentatives par job. + * @param bool $force Si `true`, le worker tourne même en mode maintenance. + * @param bool $stopWhenEmpty Si `true`, le worker s'arrête dès que la file est vide. + * @param int $maxJobs Nombre maximal de jobs à traiter (0 = illimité). + * @param int $maxTime Durée de vie maximale du worker en secondes (0 = illimitée). + * @param int $rest Secondes de pause entre deux jobs traités avec succès. */ public function __construct( public string $name = 'default', diff --git a/src/Database/Migrations/2026-08-26-061438_CreateQueueTables.php b/src/Database/Migrations/2026-08-26-061438_CreateQueueTables.php index 4055b0e..a3fbcdd 100644 --- a/src/Database/Migrations/2026-08-26-061438_CreateQueueTables.php +++ b/src/Database/Migrations/2026-08-26-061438_CreateQueueTables.php @@ -5,8 +5,14 @@ use BlitzPHP\Database\Migration\Migration; use BlitzPHP\Database\Migration\Structure; +/** + * Crée les tables des jobs en file et des jobs échoués. + */ class CreateQueueTables extends Migration { + /** + * Crée `queue_jobs` (ou table configurée) et `queue_failed_jobs`. + */ public function up() { $this->create(config('queue.connections.database.table', 'queue_jobs'), function(Structure $table) { @@ -34,6 +40,9 @@ public function up() }); } + /** + * Supprime les tables de file et de jobs échoués. + */ public function down() { $this->dropIfExists(config('queue.connections.database.table', 'queue_jobs')); diff --git a/src/Drivers/ConnectorInterface.php b/src/Drivers/ConnectorInterface.php index 6cf7b75..ff01f87 100644 --- a/src/Drivers/ConnectorInterface.php +++ b/src/Drivers/ConnectorInterface.php @@ -5,10 +5,13 @@ use BlitzPHP\Contracts\Container\ContainerInterface; use BlitzPHP\Contracts\Queue\Queue; +/** + * Contrat des pilotes de file : établit une connexion à partir de la configuration. + */ interface ConnectorInterface { /** - * Establish a queue connection. + * Établit une connexion de file d'attente. */ public static function connect(ContainerInterface $container, array $config): Queue; } diff --git a/src/Drivers/DatabaseDriver.php b/src/Drivers/DatabaseDriver.php index 149f174..8070be0 100644 --- a/src/Drivers/DatabaseDriver.php +++ b/src/Drivers/DatabaseDriver.php @@ -21,19 +21,22 @@ use DateInterval; use Throwable; +/** + * Pilote de file d'attente persisté en base de données. + */ class DatabaseDriver extends Queue implements QueueContract, ConnectorInterface { /** - * The cached lock type for popping jobs. + * Type de verrou mis en cache pour le prélèvement des jobs. * * @var string|bool|null */ protected $lockForPopping = null; /** - * Create a new database queue instance. + * Crée une instance de file d'attente base de données. * - * @param string $default The name of the default queue. + * @param string $default Nom de la file par défaut. */ public function __construct(protected JobModel $model, protected string $default = 'default', bool $dispatchAfterCommit = false) { @@ -41,9 +44,9 @@ public function __construct(protected JobModel $model, protected string $default } /** - * Establish a queue connection. + * Établit une connexion de file d'attente. * - * @param array $config + * @param array $config Configuration de la connexion. */ public static function connect(ContainerInterface $container, array $config): QueueContract { @@ -78,7 +81,7 @@ public static function connect(ContainerInterface $container, array $config): Qu } /** - * Get the size of the queue. + * Retourne le nombre total de jobs dans la file. */ public function size(?string $queue = null): int { @@ -86,7 +89,7 @@ public function size(?string $queue = null): int } /** - * Get the number of pending jobs. + * Retourne le nombre de jobs en attente. */ public function pendingSize(?string $queue = null): int { @@ -94,7 +97,7 @@ public function pendingSize(?string $queue = null): int } /** - * Get the number of delayed jobs. + * Retourne le nombre de jobs retardés. */ public function delayedSize(?string $queue = null): int { @@ -102,7 +105,7 @@ public function delayedSize(?string $queue = null): int } /** - * Get the number of reserved jobs. + * Retourne le nombre de jobs réservés. */ public function reservedSize(?string $queue = null): int { @@ -110,7 +113,7 @@ public function reservedSize(?string $queue = null): int } /** - * Get the pending jobs for the given queue. + * Retourne les jobs en attente de la file donnée. * * @return Collection */ @@ -121,7 +124,7 @@ public function pendingJobs(?string $queue = null): Collection } /** - * Get the delayed jobs for the given queue. + * Retourne les jobs retardés de la file donnée. * * @return Collection */ @@ -132,7 +135,7 @@ public function delayedJobs(?string $queue = null): Collection } /** - * Get the reserved jobs for the given queue. + * Retourne les jobs réservés de la file donnée. * * @return Collection */ @@ -143,7 +146,7 @@ public function reservedJobs(?string $queue = null): Collection } /** - * Get the creation timestamp of the oldest pending job, excluding delayed jobs. + * Retourne l'horodatage de création du plus ancien job en attente (hors retardés). */ public function creationTimeOfOldestPendingJob(?string $queue = null): ?int { @@ -151,7 +154,7 @@ public function creationTimeOfOldestPendingJob(?string $queue = null): ?int } /** - * Push a new job onto the queue. + * Envoie un nouveau job dans la file. */ public function push(string|object $job, mixed $data = '', ?string $queue = null): mixed { @@ -165,7 +168,7 @@ public function push(string|object $job, mixed $data = '', ?string $queue = null } /** - * Push a raw payload onto the queue. + * Envoie un payload brut dans la file. */ public function pushRaw(string $payload, ?string $queue = null, array $options = []): mixed { @@ -173,7 +176,7 @@ public function pushRaw(string $payload, ?string $queue = null, array $options = } /** - * Push a new job onto the queue after (n) seconds. + * Envoie un job dans la file après n secondes. */ public function later(DateTimeInterface|DateInterval|int $delay, string|object $job, mixed $data = '', ?string $queue = null): mixed { @@ -187,7 +190,7 @@ public function later(DateTimeInterface|DateInterval|int $delay, string|object $ } /** - * Push an array of jobs onto the queue. + * Envoie un tableau de jobs dans la file. */ public function bulk(array $jobs, mixed $data = '', ?string $queue = null): mixed { @@ -209,7 +212,7 @@ function ($job) use ($queue, $data, $now) { } /** - * Release a reserved job back onto the queue after (n) seconds. + * Relâche un job réservé dans la file après n secondes. */ public function release(string $queue, DatabaseJobRecord $job, int $delay): mixed { @@ -217,7 +220,7 @@ public function release(string $queue, DatabaseJobRecord $job, int $delay): mixe } /** - * Push a raw payload to the database with a given delay of (n) seconds. + * Insère un payload brut en base avec un délai de n secondes. */ protected function pushToDatabase(?string $queue, string $payload, DateTimeInterface|DateInterval|int $delay = 0, int $attempts = 0): mixed { @@ -230,7 +233,7 @@ protected function pushToDatabase(?string $queue, string $payload, DateTimeInter } /** - * Create an array to insert for the given job. + * Construit le tableau à insérer pour le job donné. */ protected function buildDatabaseRecord(?string $queue, string $payload, int $availableAt, int $attempts = 0): array { @@ -245,7 +248,7 @@ protected function buildDatabaseRecord(?string $queue, string $payload, int $ava } /** - * Pop the next job off of the queue. + * Prélève le prochain job de la file. * * @throws Throwable */ @@ -262,14 +265,14 @@ public function pop(?string $queue = null): ?Job } }); } catch (Throwable $e) { - // Potentially invalid job that we need to fail (#58978)... + // Job potentiellement invalide : on tente de le marquer en échec. if ($jobRecord) { try { (new DatabaseJob( $this->container, $this, $jobRecord, $this->connectionName, $queue ))->fail($e); } catch (Throwable) { - // Ignore and throw the original exception... + // Ignore et relance l'exception d'origine. } } @@ -278,7 +281,7 @@ public function pop(?string $queue = null): ?Job } /** - * Get the next available job for the queue. + * Retourne le prochain job disponible de la file. */ protected function getNextAvailableJob(?string $queue): ?DatabaseJobRecord { @@ -288,7 +291,7 @@ protected function getNextAvailableJob(?string $queue): ?DatabaseJobRecord } /** - * Get the lock required for popping the next job. + * Retourne le verrou SQL nécessaire pour prélever le prochain job. * * @return string|bool */ @@ -325,7 +328,7 @@ protected function getLockForPopping() } /** - * Marshal the reserved job into a DatabaseJob instance. + * Transforme le job réservé en instance DatabaseJob. */ protected function marshalJob(string $queue, DatabaseJobRecord $job): DatabaseJob { @@ -339,7 +342,7 @@ protected function marshalJob(string $queue, DatabaseJobRecord $job): DatabaseJo } /** - * Mark the given job ID as reserved. + * Marque le job comme réservé. */ protected function markJobAsReserved(DatabaseJobRecord $job): DatabaseJobRecord { @@ -352,7 +355,7 @@ protected function markJobAsReserved(DatabaseJobRecord $job): DatabaseJobRecord } /** - * Delete a reserved job from the queue. + * Supprime un job réservé de la file. * * @throws Throwable */ @@ -362,7 +365,7 @@ public function deleteReserved(string $queue, string $id): void } /** - * Delete a reserved job from the reserved queue and release it. + * Supprime le job réservé puis le relâche dans la file. */ public function deleteAndRelease(string $queue, DatabaseJob $job, int $delay): void { @@ -378,7 +381,7 @@ public function deleteAndRelease(string $queue, DatabaseJob $job, int $delay): v } /** - * Delete all of the jobs from the queue. + * Supprime tous les jobs de la file. */ public function clear(string $queue): bool { @@ -386,7 +389,7 @@ public function clear(string $queue): bool } /** - * Get the queue or return the default. + * Retourne le nom de file, ou la file par défaut. */ public function getQueue(?string $queue): string { @@ -394,7 +397,7 @@ public function getQueue(?string $queue): string } /** - * Get the underlying database instance. + * Retourne l'instance de connexion base de données. */ public function getDatabase(): ConnectionInterface { diff --git a/src/Drivers/FailoverDriver.php b/src/Drivers/FailoverDriver.php index c338098..b9a9ef2 100644 --- a/src/Drivers/FailoverDriver.php +++ b/src/Drivers/FailoverDriver.php @@ -14,24 +14,27 @@ use RuntimeException; use Throwable; +/** + * Pilote de bascule : tente successivement plusieurs connexions en cas d'échec. + */ class FailoverDriver extends Queue implements QueueContract, ConnectorInterface { /** - * The queues which failed on the last action. + * Connexions ayant échoué lors de la dernière opération. * * @var list */ protected array $failingQueues = []; /** - * Create a new failover queue instance. + * Crée une instance de file en bascule (failover). */ public function __construct(public Manager $manager, public QueueEventManager $events, public array $connections) { } /** - * Establish a queue connection. + * Établit une connexion de file d'attente. */ public static function connect(ContainerInterface $container, array $config): QueueContract { @@ -43,7 +46,7 @@ public static function connect(ContainerInterface $container, array $config): Qu } /** - * Get the size of the queue. + * Retourne le nombre total de jobs dans la file. */ public function size(?string $queue = null): int { @@ -51,7 +54,7 @@ public function size(?string $queue = null): int } /** - * Get the number of pending jobs. + * Retourne le nombre de jobs en attente. */ public function pendingSize(?string $queue = null): int { @@ -59,7 +62,7 @@ public function pendingSize(?string $queue = null): int } /** - * Get the number of delayed jobs. + * Retourne le nombre de jobs retardés. */ public function delayedSize(?string $queue = null): int { @@ -67,7 +70,7 @@ public function delayedSize(?string $queue = null): int } /** - * Get the number of reserved jobs. + * Retourne le nombre de jobs réservés. */ public function reservedSize(?string $queue = null): int { @@ -75,7 +78,7 @@ public function reservedSize(?string $queue = null): int } /** - * Get the pending jobs for the given queue. + * Retourne les jobs en attente de la file donnée. */ public function pendingJobs(?string $queue = null): Collection { @@ -83,7 +86,7 @@ public function pendingJobs(?string $queue = null): Collection } /** - * Get the delayed jobs for the given queue. + * Retourne les jobs retardés de la file donnée. */ public function delayedJobs(?string $queue = null): Collection { @@ -91,7 +94,7 @@ public function delayedJobs(?string $queue = null): Collection } /** - * Get the reserved jobs for the given queue. + * Retourne les jobs réservés de la file donnée. */ public function reservedJobs(?string $queue = null): Collection { @@ -99,7 +102,7 @@ public function reservedJobs(?string $queue = null): Collection } /** - * Get the creation timestamp of the oldest pending job, excluding delayed jobs. + * Retourne l'horodatage de création du plus ancien job en attente (hors retardés). */ public function creationTimeOfOldestPendingJob(?string $queue = null): ?int { @@ -109,7 +112,7 @@ public function creationTimeOfOldestPendingJob(?string $queue = null): ?int } /** - * Push a new job onto the queue. + * Envoie un nouveau job dans la file. */ public function push(object|string $job, mixed $data = '', ?string $queue = null): mixed { @@ -117,7 +120,7 @@ public function push(object|string $job, mixed $data = '', ?string $queue = null } /** - * Push a raw payload onto the queue. + * Envoie un payload brut dans la file. */ public function pushRaw(string $payload, ?string $queue = null, array $options = []): mixed { @@ -125,7 +128,7 @@ public function pushRaw(string $payload, ?string $queue = null, array $options = } /** - * Push a new job onto the queue after (n) seconds. + * Envoie un job dans la file après n secondes. */ public function later(DateTimeInterface|DateInterval|int $delay, string|object $job, mixed $data = '', ?string $queue = null): mixed { @@ -133,7 +136,7 @@ public function later(DateTimeInterface|DateInterval|int $delay, string|object $ } /** - * Pop the next job off of the queue. + * Prélève le prochain job de la file. */ public function pop(?string $queue = null): ?Job { @@ -141,7 +144,7 @@ public function pop(?string $queue = null): ?Job } /** - * Attempt the given method on all connections. + * Tente la méthode donnée sur toutes les connexions, dans l'ordre. * * * @throws Throwable diff --git a/src/Drivers/NullDriver.php b/src/Drivers/NullDriver.php index 2a948a9..c95557e 100644 --- a/src/Drivers/NullDriver.php +++ b/src/Drivers/NullDriver.php @@ -10,10 +10,13 @@ use DateInterval; use DateTimeInterface; +/** + * Pilote nul : accepte les jobs sans les stocker ni les exécuter. + */ class NullDriver extends Queue implements QueueContract, ConnectorInterface { /** - * Establish a queue connection. + * Établit une connexion de file d'attente. */ public static function connect(ContainerInterface $container, array $config): QueueContract { @@ -21,7 +24,7 @@ public static function connect(ContainerInterface $container, array $config): Qu } /** - * Get the size of the queue. + * Retourne le nombre total de jobs dans la file. */ public function size(?string $queue = null): int { @@ -29,7 +32,7 @@ public function size(?string $queue = null): int } /** - * Get the number of pending jobs. + * Retourne le nombre de jobs en attente. */ public function pendingSize(?string $queue = null): int { @@ -37,7 +40,7 @@ public function pendingSize(?string $queue = null): int } /** - * Get the number of delayed jobs. + * Retourne le nombre de jobs retardés. */ public function delayedSize(?string $queue = null): int { @@ -45,7 +48,7 @@ public function delayedSize(?string $queue = null): int } /** - * Get the number of reserved jobs. + * Retourne le nombre de jobs réservés. */ public function reservedSize(?string $queue = null): int { @@ -53,7 +56,7 @@ public function reservedSize(?string $queue = null): int } /** - * Get the pending jobs for the given queue. + * Retourne les jobs en attente de la file donnée. */ public function pendingJobs(?string $queue = null): Collection { @@ -61,7 +64,7 @@ public function pendingJobs(?string $queue = null): Collection } /** - * Get the delayed jobs for the given queue. + * Retourne les jobs retardés de la file donnée. */ public function delayedJobs(?string $queue = null): Collection { @@ -69,7 +72,7 @@ public function delayedJobs(?string $queue = null): Collection } /** - * Get the reserved jobs for the given queue. + * Retourne les jobs réservés de la file donnée. */ public function reservedJobs(?string $queue = null): Collection { @@ -77,7 +80,7 @@ public function reservedJobs(?string $queue = null): Collection } /** - * Get the creation timestamp of the oldest pending job, excluding delayed jobs. + * Retourne l'horodatage de création du plus ancien job en attente (hors retardés). */ public function creationTimeOfOldestPendingJob(?string $queue = null): ?int { @@ -85,7 +88,7 @@ public function creationTimeOfOldestPendingJob(?string $queue = null): ?int } /** - * Push a new job onto the queue. + * Envoie un nouveau job dans la file. */ public function push(string|object $job, mixed $data = '', ?string $queue = null): mixed { @@ -93,7 +96,7 @@ public function push(string|object $job, mixed $data = '', ?string $queue = null } /** - * Push a raw payload onto the queue. + * Envoie un payload brut dans la file. */ public function pushRaw(string $payload, ?string $queue = null, array $options = []): mixed { @@ -101,7 +104,7 @@ public function pushRaw(string $payload, ?string $queue = null, array $options = } /** - * Push a new job onto the queue after (n) seconds. + * Envoie un job dans la file après n secondes. */ public function later(DateTimeInterface|DateInterval|int $delay, string|object $job, mixed $data = '', ?string $queue = null): mixed { @@ -109,7 +112,7 @@ public function later(DateTimeInterface|DateInterval|int $delay, string|object $ } /** - * Pop the next job off of the queue. + * Prélève le prochain job de la file. */ public function pop(?string $queue = null): ?Job { diff --git a/src/Drivers/SyncDriver.php b/src/Drivers/SyncDriver.php index 56c7946..20e8a2b 100644 --- a/src/Drivers/SyncDriver.php +++ b/src/Drivers/SyncDriver.php @@ -12,10 +12,13 @@ use Psr\Container\ContainerInterface; use Throwable; +/** + * Pilote synchrone : exécute le job immédiatement dans le processus courant. + */ class SyncDriver extends Queue implements QueueContract, ConnectorInterface { /** - * Create a new sync queue instance. + * Crée une instance de file synchrone. */ public function __construct(bool $dispatchAfterCommit = false) { @@ -23,7 +26,7 @@ public function __construct(bool $dispatchAfterCommit = false) } /** - * Establish a queue connection. + * Établit une connexion de file d'attente. */ public static function connect(ContainerInterface $container, array $config): QueueContract { @@ -32,7 +35,7 @@ public static function connect(ContainerInterface $container, array $config): Qu /** - * Get the size of the queue. + * Retourne le nombre total de jobs dans la file. */ public function size(?string $queue = null): int { @@ -40,7 +43,7 @@ public function size(?string $queue = null): int } /** - * Get the number of pending jobs. + * Retourne le nombre de jobs en attente. */ public function pendingSize(?string $queue = null): int { @@ -48,7 +51,7 @@ public function pendingSize(?string $queue = null): int } /** - * Get the number of delayed jobs. + * Retourne le nombre de jobs retardés. */ public function delayedSize(?string $queue = null): int { @@ -56,7 +59,7 @@ public function delayedSize(?string $queue = null): int } /** - * Get the number of reserved jobs. + * Retourne le nombre de jobs réservés. */ public function reservedSize(?string $queue = null): int { @@ -64,7 +67,7 @@ public function reservedSize(?string $queue = null): int } /** - * Get the pending jobs for the given queue. + * Retourne les jobs en attente de la file donnée. */ public function pendingJobs(?string $queue = null): Collection { @@ -72,7 +75,7 @@ public function pendingJobs(?string $queue = null): Collection } /** - * Get the delayed jobs for the given queue. + * Retourne les jobs retardés de la file donnée. */ public function delayedJobs(?string $queue = null): Collection { @@ -80,7 +83,7 @@ public function delayedJobs(?string $queue = null): Collection } /** - * Get the reserved jobs for the given queue. + * Retourne les jobs réservés de la file donnée. */ public function reservedJobs(?string $queue = null): Collection { @@ -88,7 +91,7 @@ public function reservedJobs(?string $queue = null): Collection } /** - * Get the creation timestamp of the oldest pending job, excluding delayed jobs. + * Retourne l'horodatage de création du plus ancien job en attente (hors retardés). */ public function creationTimeOfOldestPendingJob(?string $queue = null): ?int { @@ -96,7 +99,7 @@ public function creationTimeOfOldestPendingJob(?string $queue = null): ?int } /** - * Push a new job onto the queue. + * Envoie un nouveau job dans la file. * * @throws Throwable */ @@ -125,7 +128,7 @@ function () use ($job) { } /** - * Execute a given job synchronously. + * Exécute un job de façon synchrone. * * @throws Throwable */ @@ -151,7 +154,7 @@ protected function executeJob(string $job, mixed $data = '', ?string $queue = nu } /** - * Resolve a Sync job instance. + * Résout une instance de job synchrone. */ protected function resolveJob(string $payload, string $queue): SyncJob { @@ -159,7 +162,7 @@ protected function resolveJob(string $payload, string $queue): SyncJob } /** - * Raise the before queue job event. + * Émet l'événement avant traitement du job. */ protected function raiseBeforeJobEvent(Job $job): void { @@ -167,7 +170,7 @@ protected function raiseBeforeJobEvent(Job $job): void } /** - * Raise the after queue job event. + * Émet l'événement après traitement du job. */ protected function raiseAfterJobEvent(Job $job): void { @@ -175,7 +178,7 @@ protected function raiseAfterJobEvent(Job $job): void } /** - * Raise the job attempted event. + * Émet l'événement de tentative de job. */ protected function raiseJobAttemptedEvent(Job $job, ?Throwable $exceptionOccurred = null): void { @@ -183,7 +186,7 @@ protected function raiseJobAttemptedEvent(Job $job, ?Throwable $exceptionOccurre } /** - * Raise the exception occurred queue job event. + * Émet l'événement d'exception survenue sur un job. */ protected function raiseExceptionOccurredJobEvent(Job $job, Throwable $e): void { @@ -191,7 +194,7 @@ protected function raiseExceptionOccurredJobEvent(Job $job, Throwable $e): void } /** - * Handle an exception that occurred while processing a job. + * Traite une exception survenue pendant le traitement d'un job. * * @throws Throwable */ @@ -205,7 +208,7 @@ protected function handleException(Job $queueJob, Throwable $e): void } /** - * Push a raw payload onto the queue. + * Envoie un payload brut dans la file. */ public function pushRaw(string $payload, ?string $queue = null, array $options = []): mixed { @@ -213,7 +216,7 @@ public function pushRaw(string $payload, ?string $queue = null, array $options = } /** - * Push a new job onto the queue after (n) seconds. + * Envoie un job dans la file après n secondes. */ public function later(DateTimeInterface|DateInterval|int $delay, string|object $job, mixed $data = '', ?string $queue = null): mixed { @@ -221,7 +224,7 @@ public function later(DateTimeInterface|DateInterval|int $delay, string|object $ } /** - * Pop the next job off of the queue. + * Prélève le prochain job de la file. */ public function pop(?string $queue = null): ?Job { diff --git a/src/Enums/WorkerStopReason.php b/src/Enums/WorkerStopReason.php index 063feb3..ef5613b 100644 --- a/src/Enums/WorkerStopReason.php +++ b/src/Enums/WorkerStopReason.php @@ -2,14 +2,25 @@ namespace BlitzPHP\Queue\Enums; +/** + * Motifs d'arrêt d'un worker de file d'attente. + */ enum WorkerStopReason: string { + /** Interruption par signal (SIGINT, SIGTERM, etc.). */ case Interrupted = 'interrupted'; + /** Perte de connexion (base de données, courtier, etc.). */ case LostConnection = 'lost_connection'; + /** Nombre maximal de jobs atteint. */ case MaxJobsExceeded = 'max_jobs'; + /** Limite mémoire dépassée. */ case MaxMemoryExceeded = 'memory'; + /** Durée de vie maximale du worker atteinte. */ case MaxTimeExceeded = 'max_time'; + /** File vide et option `stopWhenEmpty` active. */ case QueueEmpty = 'empty'; + /** Signal de redémarrage reçu via le cache. */ case ReceivedRestartSignal = 'restart_signal'; + /** Dépassement du délai d'exécution d'un job. */ case TimedOut = 'timed_out'; } diff --git a/src/Events/QueueEvent.php b/src/Events/QueueEvent.php index 14e786b..ad129da 100644 --- a/src/Events/QueueEvent.php +++ b/src/Events/QueueEvent.php @@ -9,15 +9,27 @@ use Throwable; /** - * @property mixed $job - * @property ?int $jobId - * @property ?int $attempts + * Événement du cycle de vie de la file d'attente (job, worker, connexion, opération). + * + * @property mixed $job + * @property ?int $jobId + * @property ?int $attempts * @property ?Throwable $exception */ class QueueEvent extends Event { + /** + * Instant de survenue de l'événement. + */ private readonly Date $timestamp; + /** + * @param string $type Identifiant de l'événement (constantes de QueueEventManager). + * @param string $connection Nom de la connexion concernée. + * @param string|null $queue Nom de la file, le cas échéant. + * @param array $metadata Données contextuelles (job, exception, etc.). + * @param Date|null $timestamp Horodatage (maintenant par défaut). + */ public function __construct( public readonly string $type, public readonly string $connection, @@ -31,7 +43,7 @@ public function __construct( } /** - * Get timestamp + * Retourne l'horodatage de l'événement. */ public function timestamp(): Date { @@ -39,7 +51,7 @@ public function timestamp(): Date } /** - * Get all metadata + * Retourne l'ensemble des métadonnées. */ public function allMetadata(): array { @@ -47,7 +59,7 @@ public function allMetadata(): array } /** - * Get metadata value by key + * Retourne une métadonnée par sa clé. */ public function metadata(string $key, mixed $default = null): mixed { @@ -55,7 +67,7 @@ public function metadata(string $key, mixed $default = null): mixed } /** - * Check if this is a job-related event + * Indique s'il s'agit d'un événement lié à un job. */ public function isJobEvent(): bool { @@ -63,7 +75,7 @@ public function isJobEvent(): bool } /** - * Check if this is a worker-related event + * Indique s'il s'agit d'un événement lié au worker. */ public function isWorkerEvent(): bool { @@ -71,7 +83,7 @@ public function isWorkerEvent(): bool } /** - * Check if this is an operation event (like queue.cleared) + * Indique s'il s'agit d'un événement d'opération (ex. file vidée). */ public function isOperationEvent(): bool { @@ -82,7 +94,7 @@ public function isOperationEvent(): bool } /** - * Check if this is a connection event + * Indique s'il s'agit d'un événement de connexion. */ public function isConnectionEvent(): bool { @@ -90,7 +102,7 @@ public function isConnectionEvent(): bool } /** - * Get job ID (for job events) + * Retourne l'identifiant du job (événements de job). */ public function getJobId(): ?int { @@ -100,7 +112,7 @@ public function getJobId(): ?int } /** - * Get number of attempts (for job events) + * Retourne le nombre de tentatives (événements de job). */ public function getAttempts(): ?int { @@ -110,7 +122,7 @@ public function getAttempts(): ?int } /** - * Get job status (for job events) + * Retourne le statut du job (événements de job). */ public function getStatus(): ?int { @@ -120,7 +132,7 @@ public function getStatus(): ?int } /** - * Get job class name (for job events) + * Retourne le nom de classe du job (événements de job). */ public function getJobClass(): ?string { @@ -128,7 +140,7 @@ public function getJobClass(): ?string } /** - * Get processing time in seconds (for job events) + * Retourne le temps de traitement en secondes. */ public function getProcessingTime(): float { @@ -136,7 +148,7 @@ public function getProcessingTime(): float } /** - * Get processing time in milliseconds (for job events) + * Retourne le temps de traitement en millisecondes. */ public function getProcessingTimeMs(): int { @@ -144,7 +156,7 @@ public function getProcessingTimeMs(): int } /** - * Get exception (for failed events) + * Retourne l'exception (événements d'échec). */ public function getException(): ?Throwable { @@ -152,7 +164,7 @@ public function getException(): ?Throwable } /** - * Get exception message (for failed events) + * Retourne le message d'exception (événements d'échec). */ public function getExceptionMessage(): ?string { @@ -160,7 +172,7 @@ public function getExceptionMessage(): ?string } /** - * Check if event has failed + * Indique si l'événement correspond à un échec. */ public function hasFailed(): bool { @@ -170,7 +182,7 @@ public function hasFailed(): bool } /** - * Convert to array for serialization + * Convertit l'événement en tableau pour sérialisation. */ public function toArray(): array { @@ -183,6 +195,9 @@ public function toArray(): array ]; } + /** + * Accès magique aux métadonnées et accesseurs `get*`. + */ public function __get(string $name): mixed { if (method_exists($this, $method = 'get' . Text::camel($name))) { diff --git a/src/Events/QueueEventManager.php b/src/Events/QueueEventManager.php index 6fce702..e407cf7 100644 --- a/src/Events/QueueEventManager.php +++ b/src/Events/QueueEventManager.php @@ -11,9 +11,14 @@ use DateTimeInterface; use Throwable; +/** + * Émet les événements du cycle de vie des files, jobs et workers. + */ class QueueEventManager { - // Event names for queue operations + /** + * Noms d'événements des opérations de file. + */ public const JOB_POPPING = 'queue.job.popping'; public const JOB_POPPED = 'queue.job.popped'; public const JOB_PUSHED = 'queue.job.pushed'; @@ -38,12 +43,15 @@ class QueueEventManager public const HANDLER_CONNECTION_FAILED = 'queue.handler.connection.failed'; public const HANDLER_CONNECTION_ESTABLISHED = 'queue.handler.connection.established'; + /** + * @param EventManagerInterface $events Gestionnaire d'événements de l'application. + */ public function __construct(protected EventManagerInterface $events) { } /** - * Emit job attempted event + * Émet l'événement de tentative de job. */ public function jobAttempted(string $connection, Job $job, ?Throwable $e = null): void { @@ -56,7 +64,7 @@ public function jobAttempted(string $connection, Job $job, ?Throwable $e = null) } /** - * Emit job failed event + * Émet l'événement d'échec de job. */ public function jobFailed(string $connection, Job $job, ?Throwable $e): void { @@ -69,7 +77,7 @@ public function jobFailed(string $connection, Job $job, ?Throwable $e): void } /** - * Emit job exception-occurent event + * Émet l'événement d'exception survenue sur un job. */ public function jobExceptionOccured(string $connection, Job $job, Throwable $e): void { @@ -82,7 +90,7 @@ public function jobExceptionOccured(string $connection, Job $job, Throwable $e): } /** - * Emit job popping event + * Émet l'événement de prélèvement imminent d'un job. */ public function jobPopping(string $connection, ?string $queue = null): void { @@ -94,7 +102,7 @@ public function jobPopping(string $connection, ?string $queue = null): void } /** - * Emit job popped event + * Émet l'événement de job prélevé. */ public function jobPopped(string $connection, ?Job $job = null): void { @@ -107,7 +115,7 @@ public function jobPopped(string $connection, ?Job $job = null): void } /** - * Emit job processing event + * Émet l'événement de traitement en cours. */ public function jobProcessing(string $connection, Job $job): void { @@ -120,7 +128,7 @@ public function jobProcessing(string $connection, Job $job): void } /** - * Emit job processed event + * Émet l'événement de job traité. */ public function jobProcessed(string $connection, Job $job): void { @@ -133,7 +141,7 @@ public function jobProcessed(string $connection, Job $job): void } /** - * Emit job processed event + * Émet l'événement « job enfilé ». */ public function jobQueued(string $connection, ?string $queue, string|int|null $jobId, string|object $job, string $payload, DateTimeInterface|DateInterval|int|null $delay): void { @@ -146,7 +154,7 @@ public function jobQueued(string $connection, ?string $queue, string|int|null $j } /** - * Emit job processed event + * Émet l'événement « job en cours d'enfilement ». */ public function jobQueueing(string $connection, ?string $queue, string|object $job, string $payload, DateTimeInterface|DateInterval|int|null $delay): void { @@ -159,7 +167,7 @@ public function jobQueueing(string $connection, ?string $queue, string|object $j } /** - * Emit job released-after-exception started event + * Émet l'événement de relâchement après exception. */ public function jobReleasedAfterException(string $connection, Job $job, int $backoff): void { @@ -172,7 +180,7 @@ public function jobReleasedAfterException(string $connection, Job $job, int $bac } /** - * Emit job timeout event + * Émet l'événement de dépassement de délai. */ public function jobTimeout(string $connection, string $queue, Job $job, array $metadata = []): void { @@ -188,7 +196,7 @@ public function jobTimeout(string $connection, string $queue, Job $job, array $m } /** - * Emit queue cleared event + * Émet l'événement de file vidée. */ public function queueCleared(string $connection, ?string $queue = null): void { @@ -200,7 +208,7 @@ public function queueCleared(string $connection, ?string $queue = null): void } /** - * Emit queue paused event + * Émet l'événement de file en pause. */ public function queuePaused(string $connection, string $queue, DateTimeInterface|DateInterval|int|null $ttl = null): void { @@ -213,7 +221,7 @@ public function queuePaused(string $connection, string $queue, DateTimeInterface } /** - * Emit queue resumed event + * Émet l'événement de reprise de file. */ public function queueResumed(string $connection, string $queue): void { @@ -225,7 +233,7 @@ public function queueResumed(string $connection, string $queue): void } /** - * Emit queue resumed event + * Émet l'événement de bascule (failover) vers une autre connexion. */ public function queueFailedOver(string $connection, string $job, Throwable $e): void { @@ -237,7 +245,7 @@ public function queueFailedOver(string $connection, string $job, Throwable $e): } /** - * Emit worker started event + * Émet l'événement de démarrage du worker. */ public function workerStarting(string $connection, string $queue, WorkerOptions $options): void { @@ -250,7 +258,7 @@ public function workerStarting(string $connection, string $queue, WorkerOptions } /** - * Emit worker stopped event + * Émet l'événement d'arrêt du worker. */ public function workerStopping(string $connection, int $status, ?WorkerOptions $options = null, ?WorkerStopReason $reason = null): void { @@ -262,7 +270,7 @@ public function workerStopping(string $connection, int $status, ?WorkerOptions $ } /** - * Emit handler connection established event + * Émet l'événement de connexion de pilote établie. */ public function handlerConnectionEstablished(string $connection, array $config = []): void { @@ -274,7 +282,7 @@ public function handlerConnectionEstablished(string $connection, array $config = } /** - * Emit handler connection failed event + * Émet l'événement d'échec de connexion de pilote. */ public function handlerConnectionFailed(string $connection, Throwable $exception, array $config = []): void { diff --git a/src/Exceptions/InvalidPayloadException.php b/src/Exceptions/InvalidPayloadException.php index e99d2d7..56b5794 100644 --- a/src/Exceptions/InvalidPayloadException.php +++ b/src/Exceptions/InvalidPayloadException.php @@ -4,15 +4,18 @@ use InvalidArgumentException; +/** + * Exception levée lorsque le payload d'un job ne peut pas être encodé en JSON. + */ class InvalidPayloadException extends InvalidArgumentException { /** - * The value that failed to decode. + * Valeur dont le décodage / l'encodage a échoué. */ public mixed $value; /** - * Create a new exception instance. + * Crée une nouvelle instance d'exception. */ public function __construct(?string $message = null, mixed $value = null) { diff --git a/src/Exceptions/ManuallyFailedException.php b/src/Exceptions/ManuallyFailedException.php index 345651d..60c81e2 100644 --- a/src/Exceptions/ManuallyFailedException.php +++ b/src/Exceptions/ManuallyFailedException.php @@ -4,6 +4,9 @@ use RuntimeException; +/** + * Exception levée lorsqu'un job est marqué en échec manuellement (`fail()`). + */ class ManuallyFailedException extends RuntimeException { // diff --git a/src/Exceptions/MaxAttemptsExceededException.php b/src/Exceptions/MaxAttemptsExceededException.php index 40c3090..0841bc8 100644 --- a/src/Exceptions/MaxAttemptsExceededException.php +++ b/src/Exceptions/MaxAttemptsExceededException.php @@ -5,15 +5,18 @@ use BlitzPHP\Contracts\Queue\Job; use RuntimeException; +/** + * Exception levée lorsqu'un job a épuisé son nombre maximal de tentatives. + */ class MaxAttemptsExceededException extends RuntimeException { /** - * The job instance. + * Instance du job concerné. */ public ?Job $job = null; /** - * Create a new instance for the job. + * Crée une instance d'exception liée au job. */ public static function forJob(Job $job): static { diff --git a/src/Exceptions/QueueException.php b/src/Exceptions/QueueException.php deleted file mode 100644 index 321fd2d..0000000 --- a/src/Exceptions/QueueException.php +++ /dev/null @@ -1,64 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace CodeIgniter\Queue\Exceptions; - -use RuntimeException; - -final class QueueException extends RuntimeException -{ - public static function forIncorrectHandler(): static - { - return new self(lang('Queue.incorrectHandler')); - } - - public static function forIncorrectQueueFormat(): static - { - return new self(lang('Queue.incorrectQueueFormat')); - } - - public static function forTooLongQueueName(): static - { - return new self(lang('Queue.tooLongQueueName')); - } - - public static function forIncorrectJobHandler(): static - { - return new self(lang('Queue.incorrectJobHandler')); - } - - public static function forIncorrectPriorityFormat(): static - { - return new self(lang('Queue.incorrectPriorityFormat')); - } - - public static function forTooLongPriorityName(): static - { - return new self(lang('Queue.tooLongPriorityName')); - } - - public static function forIncorrectQueuePriority(string $priority, string $queue): static - { - return new self(lang('Queue.incorrectQueuePriority', [$priority, $queue])); - } - - public static function forIncorrectDelayValue(): static - { - return new self(lang('Queue.incorrectDelayValue')); - } - - public static function forFailedJsonEncode(string $error): static - { - return new self(lang('Queue.failedToJsonEncode', [$error])); - } -} diff --git a/src/Exceptions/TimeoutExceededException.php b/src/Exceptions/TimeoutExceededException.php index 1f5ed47..d78b513 100644 --- a/src/Exceptions/TimeoutExceededException.php +++ b/src/Exceptions/TimeoutExceededException.php @@ -4,10 +4,13 @@ use BlitzPHP\Contracts\Queue\Job; +/** + * Exception levée lorsqu'un job dépasse son délai d'exécution (timeout). + */ class TimeoutExceededException extends MaxAttemptsExceededException { /** - * Create a new instance for the job. + * Crée une instance d'exception liée au job. */ public static function forJob(Job $job): static { diff --git a/src/Failed/CountableFailedJobProvider.php b/src/Failed/CountableFailedJobProvider.php index 63c4590..9eb82e8 100644 --- a/src/Failed/CountableFailedJobProvider.php +++ b/src/Failed/CountableFailedJobProvider.php @@ -1,10 +1,13 @@ resolver->connection($this->database)->table($this->table); } + /** + * Clause WHERE selon que l'identifiant est un UUID (32 caractères) ou un entier. + * + * @return array + */ private function whereId(string|int $id): array { return [is_string($id) && strlen($id) === 32 ? 'uuid' : 'id' => $id]; } + /** + * Insère une ligne et retourne l'identifiant généré. + */ private function insertGetId(array $data): ?int { ($builder = $this->getTable())->insert($data); diff --git a/src/Failed/DatabaseUuidFailedJobProvider.php b/src/Failed/DatabaseUuidFailedJobProvider.php index 4c6ae7b..b56e438 100644 --- a/src/Failed/DatabaseUuidFailedJobProvider.php +++ b/src/Failed/DatabaseUuidFailedJobProvider.php @@ -8,21 +8,24 @@ use DateTimeInterface; use Throwable; +/** + * Stocke les jobs échoués en base, identifiés par l'UUID du payload. + */ class DatabaseUuidFailedJobProvider implements CountableFailedJobProvider, FailedJobProviderInterface, PrunableFailedJobProvider { /** - * Create a new database failed job provider. + * Crée un fournisseur de jobs échoués en base de données. * - * @param ConnectionResolverInterface $resolver The connection resolver implementation. - * @param string $database The database connection name. - * @param string $table The database table. + * @param ConnectionResolverInterface $resolver Résolveur de connexions base de données. + * @param string $database Nom de la connexion base de données. + * @param string $table Nom de la table. */ public function __construct(protected ConnectionResolverInterface $resolver, protected string $database, protected string $table) { } /** - * Log a failed job into storage. + * Enregistre un job échoué dans le stockage. */ public function log(string $connection, string $queue, string $payload, Throwable $exception): ?string { @@ -39,7 +42,7 @@ public function log(string $connection, string $queue, string $payload, Throwabl } /** - * Get the IDs of all of the failed jobs. + * Retourne les identifiants de tous les jobs échoués. */ public function ids(?string $queue = null): array { @@ -50,7 +53,7 @@ public function ids(?string $queue = null): array } /** - * Get a list of all of the failed jobs. + * Retourne la liste de tous les jobs échoués. */ public function all(): array { @@ -65,7 +68,7 @@ public function all(): array } /** - * Get a single failed job. + * Retourne un job échoué. */ public function find(string|int $id): ?object { @@ -78,7 +81,7 @@ public function find(string|int $id): ?object } /** - * Delete a single failed job from storage. + * Supprime un job échoué du stockage. */ public function forget(string|int $id): bool { @@ -86,7 +89,7 @@ public function forget(string|int $id): bool } /** - * Flush all of the failed jobs from storage. + * Vide le stockage des jobs échoués. */ public function flush(?int $hours = null): void { @@ -96,7 +99,7 @@ public function flush(?int $hours = null): void } /** - * Prune all of the entries older than the given date. + * Purge les entrées antérieures à la date donnée. */ public function prune(DateTimeInterface $before): int { @@ -114,7 +117,7 @@ public function prune(DateTimeInterface $before): int } /** - * Count the failed jobs. + * Compte les jobs échoués. */ public function count(?string $connection = null, ?string $queue = null): int { @@ -125,7 +128,7 @@ public function count(?string $connection = null, ?string $queue = null): int } /** - * Get a new query builder instance for the table. + * Retourne un constructeur de requêtes pour la table. * * @return BaseBuilder */ diff --git a/src/Failed/FailedJobProviderInterface.php b/src/Failed/FailedJobProviderInterface.php index bc80c37..f8d798c 100644 --- a/src/Failed/FailedJobProviderInterface.php +++ b/src/Failed/FailedJobProviderInterface.php @@ -3,41 +3,44 @@ use Throwable; +/** + * Contrat de persistance des jobs définitivement échoués. + */ interface FailedJobProviderInterface { /** - * Log a failed job into storage. + * Enregistre un job échoué dans le stockage. * * @return string|int|null */ public function log(string $connection, string $queue, string $payload, Throwable $exception): string|int|null; /** - * Get the IDs of all of the failed jobs. + * Retourne les identifiants de tous les jobs échoués. * * @return array */ public function ids(?string $queue = null): array; /** - * Get a list of all of the failed jobs. + * Retourne la liste de tous les jobs échoués. * * @return array */ public function all(): array; /** - * Get a single failed job. + * Retourne un job échoué. */ public function find(string|int $id): ?object; /** - * Delete a single failed job from storage. + * Supprime un job échoué du stockage. */ public function forget(string|int $id): bool; /** - * Flush all of the failed jobs from storage. + * Vide le stockage des jobs échoués. */ public function flush(?int $hours = null): void; } diff --git a/src/Failed/FileFailedJobProvider.php b/src/Failed/FileFailedJobProvider.php index 0399642..df1dc60 100644 --- a/src/Failed/FileFailedJobProvider.php +++ b/src/Failed/FileFailedJobProvider.php @@ -8,21 +8,24 @@ use DateTimeInterface; use Throwable; +/** + * Stocke les jobs échoués dans un fichier JSON, avec un plafond d'entrées. + */ class FileFailedJobProvider implements CountableFailedJobProvider, FailedJobProviderInterface, PrunableFailedJobProvider { /** - * Create a new file failed job provider. + * Crée un fournisseur de jobs échoués sur fichier. * - * @param string $path The file path where the failed job file should be stored. - * @param int $limit The maximum number of failed jobs to retain. - * @param Closure|null $lockProviderResolver The lock provider resolver. + * @param string $path Chemin du fichier de stockage des jobs échoués. + * @param int $limit Nombre maximal de jobs échoués à conserver. + * @param Closure|null $lockProviderResolver Résolveur du fournisseur de verrous. */ public function __construct(protected string $path, protected int $limit = 100, protected ?Closure $lockProviderResolver = null) { } /** - * Log a failed job into storage. + * Enregistre un job échoué dans le stockage. */ public function log(string $connection, string $queue, string $payload, Throwable $exception): ?int { @@ -50,7 +53,7 @@ public function log(string $connection, string $queue, string $payload, Throwabl } /** - * Get the IDs of all of the failed jobs. + * Retourne les identifiants de tous les jobs échoués. */ public function ids(?string $queue = null): array { @@ -61,7 +64,7 @@ public function ids(?string $queue = null): array } /** - * Get a list of all of the failed jobs. + * Retourne la liste de tous les jobs échoués. */ public function all(): array { @@ -69,7 +72,7 @@ public function all(): array } /** - * Get a single failed job. + * Retourne un job échoué. */ public function find(int|string $id): ?object { @@ -78,7 +81,7 @@ public function find(int|string $id): ?object } /** - * Delete a single failed job from storage. + * Supprime un job échoué du stockage. */ public function forget(string|int $id): bool { @@ -93,7 +96,7 @@ public function forget(string|int $id): bool } /** - * Flush all of the failed jobs from storage. + * Vide le stockage des jobs échoués. */ public function flush(?int $hours = null): void { @@ -101,7 +104,7 @@ public function flush(?int $hours = null): void } /** - * Prune all of the entries older than the given date. + * Purge les entrées antérieures à la date donnée. */ public function prune(DateTimeInterface $before): int { @@ -119,7 +122,7 @@ public function prune(DateTimeInterface $before): int } /** - * Execute the given callback while holding a lock. + * Exécute le callback en détenant un verrou. */ protected function lock(Closure $callback): mixed { @@ -135,7 +138,7 @@ protected function lock(Closure $callback): mixed } /** - * Read the failed jobs file. + * Lit le fichier des jobs échoués. */ protected function read(): array { @@ -155,7 +158,7 @@ protected function read(): array } /** - * Write the given array of jobs to the failed jobs file. + * Écrit le tableau de jobs dans le fichier des échecs. */ protected function write(array $jobs): void { @@ -166,7 +169,7 @@ protected function write(array $jobs): void } /** - * Count the failed jobs. + * Compte les jobs échoués. */ public function count(?string $connection = null, ?string $queue = null): int { diff --git a/src/Failed/NullFailedJobProvider.php b/src/Failed/NullFailedJobProvider.php index 2a2cfc7..54493e7 100644 --- a/src/Failed/NullFailedJobProvider.php +++ b/src/Failed/NullFailedJobProvider.php @@ -3,6 +3,9 @@ use Throwable; +/** + * Fournisseur vide : n'enregistre aucun job échoué. + */ class NullFailedJobProvider implements CountableFailedJobProvider, FailedJobProviderInterface { /** diff --git a/src/Failed/PrunableFailedJobProvider.php b/src/Failed/PrunableFailedJobProvider.php index 60369a8..8a736a7 100644 --- a/src/Failed/PrunableFailedJobProvider.php +++ b/src/Failed/PrunableFailedJobProvider.php @@ -3,10 +3,13 @@ use DateTimeInterface; +/** + * Contrat de purge des jobs échoués antérieurs à une date donnée. + */ interface PrunableFailedJobProvider { /** - * Prune all of the entries older than the given date. + * Purge les entrées antérieures à la date donnée. */ public function prune(DateTimeInterface $before): int; } \ No newline at end of file diff --git a/src/Job.php b/src/Job.php index ba7c992..cdeea02 100644 --- a/src/Job.php +++ b/src/Job.php @@ -6,27 +6,52 @@ use BlitzPHP\Queue\Traits\InteractsWithQueue; use BlitzPHP\Queue\Traits\SerializesModels; +/** + * Classe de base des jobs métier destinés à la file d'attente. + * + * Étendez cette classe et implémentez `handle()` pour définir le travail + * à exécuter. Les traits associés permettent le dispatch, l'interaction + * avec le worker et la sérialisation des modèles. + */ abstract class Job { use Dispatchable, InteractsWithQueue, SerializesModels; + /** + * Nombre maximal de tentatives avant échec définitif. + */ protected int $maxTries = 3; - + + /** + * Délai en secondes avant une nouvelle tentative après une exception. + */ protected int $backoff = 60; + /** + * Nom de la file logique sur laquelle dispatcher le job (vide = file par défaut). + */ protected string $queue = ''; - public function maxTries(): int + /** + * Retourne le nombre maximal de tentatives autorisées. + */ + public function maxTries(): int { return $this->maxTries; } - public function backoff(): int + /** + * Retourne le délai d'attente (en secondes) avant retry. + */ + public function backoff(): int { return $this->backoff; } - public function queue(): string + /** + * Retourne le nom de la file cible du job. + */ + public function queue(): string { return $this->queue; } diff --git a/src/Jobs/DatabaseJob.php b/src/Jobs/DatabaseJob.php index 4deff89..4650828 100644 --- a/src/Jobs/DatabaseJob.php +++ b/src/Jobs/DatabaseJob.php @@ -6,13 +6,16 @@ use BlitzPHP\Contracts\Queue\Job as JobContract; use BlitzPHP\Queue\Drivers\DatabaseDriver; +/** + * Job persisté et prélevé via le pilote base de données. + */ class DatabaseJob extends Job implements JobContract { /** - * Create a new job instance. + * Crée une nouvelle instance de job. * - * @param DatabaseDriver $database The database driver instance. - * @param DatabaseJobRecord $job The database job payload. + * @param DatabaseDriver $database Instance du pilote base de données. + * @param DatabaseJobRecord $job Enregistrement / payload du job en base. */ public function __construct(ContainerInterface $container, protected DatabaseDriver $database, protected DatabaseJobRecord $job, string $connectionName, string $queue) { @@ -22,7 +25,7 @@ public function __construct(ContainerInterface $container, protected DatabaseDri } /** - * Release the job back into the queue after (n) seconds. + * Relâche le job dans la file après n secondes. */ public function release(int $delay = 0): void { @@ -32,7 +35,7 @@ public function release(int $delay = 0): void } /** - * Delete the job from the queue. + * Supprime le job de la file. */ public function delete(): void { @@ -42,7 +45,7 @@ public function delete(): void } /** - * Get the number of times the job has been attempted. + * Retourne le nombre de tentatives déjà effectuées. */ public function attempts(): int { @@ -50,7 +53,7 @@ public function attempts(): int } /** - * Get the job identifier. + * Retourne l'identifiant du job. */ public function getJobId(): string { @@ -58,7 +61,7 @@ public function getJobId(): string } /** - * Get the raw body string for the job. + * Retourne le corps brut du job sous forme de chaîne. */ public function getRawBody(): string { @@ -66,7 +69,7 @@ public function getRawBody(): string } /** - * Get the database job record. + * Retourne l'enregistrement SQL du job. */ public function getJobRecord(): DatabaseJobRecord { diff --git a/src/Jobs/DatabaseJobRecord.php b/src/Jobs/DatabaseJobRecord.php index d1436e2..7659098 100644 --- a/src/Jobs/DatabaseJobRecord.php +++ b/src/Jobs/DatabaseJobRecord.php @@ -4,21 +4,24 @@ use BlitzPHP\Traits\Support\InteractsWithTime; +/** + * Enveloppe d'une ligne SQL représentant un job en file d'attente. + */ class DatabaseJobRecord { use InteractsWithTime; /** - * Create a new job record instance. + * Crée une instance d'enregistrement de job. * - * @param \stdClass $record The underlying job record. + * @param \stdClass $record Enregistrement sous-jacent du job. */ public function __construct(protected \stdClass $record) { } /** - * Increment the number of times the job has been attempted. + * Incrémente le nombre de tentatives du job. */ public function increment(): int { @@ -28,7 +31,7 @@ public function increment(): int } /** - * Update the "reserved at" timestamp of the job. + * Met à jour l'horodatage de réservation du job. */ public function touch(): int { @@ -38,7 +41,7 @@ public function touch(): int } /** - * Dynamically access the underlying job information. + * Accède dynamiquement aux champs de l'enregistrement. */ public function __get(string $key): mixed { diff --git a/src/Jobs/FakeJob.php b/src/Jobs/FakeJob.php index 74eb2bd..29dd848 100644 --- a/src/Jobs/FakeJob.php +++ b/src/Jobs/FakeJob.php @@ -8,29 +8,32 @@ use DateTimeInterface; use Throwable; +/** + * Job factice utilisé pour tester les interactions avec la file (delete, fail, release). + */ class FakeJob extends Job implements JobContract { /** - * The number of seconds the released job was delayed. + * Délai (secondes) avec lequel le job a été relâché. * * @var int */ public $releaseDelay; /** - * The number of attempts made to process the job. + * Nombre de tentatives de traitement du job. */ public int $attempts = 1; /** - * The exception the job failed with. + * Exception ayant provoqué l'échec du job. * * @var \Throwable */ public $failedWith; /** - * Get the job identifier. + * Retourne l'identifiant du job. */ public function getJobId(): string { @@ -38,7 +41,7 @@ public function getJobId(): string } /** - * Get the raw body of the job. + * Retourne le corps brut (JSON) du job. */ public function getRawBody(): string { @@ -46,7 +49,7 @@ public function getRawBody(): string } /** - * Release the job back into the queue after (n) seconds. + * Relâche le job dans la file après n secondes. */ public function release(DateTimeInterface|DateInterval|int $delay = 0): void { @@ -55,7 +58,7 @@ public function release(DateTimeInterface|DateInterval|int $delay = 0): void } /** - * Get the number of times the job has been attempted. + * Retourne le nombre de tentatives déjà effectuées. */ public function attempts(): int { @@ -63,7 +66,7 @@ public function attempts(): int } /** - * Delete the job from the queue. + * Supprime le job de la file. */ public function delete(): void { @@ -71,7 +74,7 @@ public function delete(): void } /** - * Delete the job, call the "failed" method, and raise the failed job event. + * Supprime le job, appelle `failed()` et émet l'événement d'échec. */ public function fail(?Throwable $e = null): void { diff --git a/src/Jobs/InspectedJob.php b/src/Jobs/InspectedJob.php index d33f248..fe05480 100644 --- a/src/Jobs/InspectedJob.php +++ b/src/Jobs/InspectedJob.php @@ -4,15 +4,18 @@ use BlitzPHP\Utilities\Date; +/** + * Vue en lecture seule d'un job (inspection des files en attente, retardées ou réservées). + */ class InspectedJob { /** - * Create a new inspected job instance. + * Crée une instance de job inspecté. * - * @param string|null $uuid The unique identifier for the job. - * @param string|null $name The display name of the job. - * @param int $attempts The number of times the job has been attempted. - * @param Date|null $createdAt The date and time the job was created. + * @param string|null $uuid Identifiant unique du job. + * @param string|null $name Nom d'affichage du job. + * @param int $attempts Nombre de tentatives déjà effectuées. + * @param Date|null $createdAt Date et heure de création du job. */ public function __construct( public readonly ?string $uuid, @@ -23,10 +26,10 @@ public function __construct( } /** - * Create a new instance from a raw job payload. + * Crée une instance à partir d'un payload JSON brut. * - * @param string $payload The raw JSON job payload. - * @param int|null $attempts The number of times the job has been attempted. + * @param string $payload Payload JSON brut du job. + * @param int|null $attempts Nombre de tentatives déjà effectuées. */ public static function fromPayload(string $payload, ?int $attempts = null): static { diff --git a/src/Jobs/Job.php b/src/Jobs/Job.php index 38ef2bc..0f411cf 100644 --- a/src/Jobs/Job.php +++ b/src/Jobs/Job.php @@ -10,59 +10,65 @@ use BlitzPHP\Traits\Support\InteractsWithTime; use Throwable; +/** + * Représentation d'un job prélevé d'une file d'attente. + * + * Encapsule le payload, le cycle de vie (exécution, suppression, relâchement, + * échec) et les métadonnées (tentatives, timeout, backoff). + */ abstract class Job { use InteractsWithTime; /** - * The job handler instance. + * Instance du handler de job résolu. * * @var mixed */ protected $instance; /** - * The IoC container instance. + * Conteneur d'injection de dépendances. */ protected ContainerInterface $container; /** - * Indicates if the job has been deleted. + * Indique si le job a été supprimé de la file. */ protected bool $deleted = false; /** - * Indicates if the job has been released. + * Indique si le job a été relâché dans la file. */ protected bool $released = false; /** - * Indicates if the job has failed. + * Indique si le job a été marqué en échec. */ protected bool $failed = false; /** - * The name of the connection the job belongs to. + * Nom de la connexion à laquelle appartient le job. */ protected string $connectionName; /** - * The name of the queue the job belongs to. + * Nom de la file à laquelle appartient le job. */ protected string $queue; /** - * Get the job identifier. + * Retourne l'identifiant du job. */ abstract public function getJobId() : string|int|null; /** - * Get the raw body of the job. + * Retourne le corps brut (JSON) du job. */ abstract public function getRawBody(): string; /** - * Get the UUID of the job. + * Retourne l'UUID du job. */ public function uuid(): ?string { @@ -70,7 +76,7 @@ public function uuid(): ?string } /** - * Fire the job. + * Déclenche l'exécution du job. */ public function fire(): void { @@ -83,7 +89,7 @@ public function fire(): void } /** - * Delete the job from the queue. + * Supprime le job de la file. */ public function delete(): void { @@ -91,7 +97,7 @@ public function delete(): void } /** - * Determine if the job has been deleted. + * Indique si le job a été supprimé. */ public function isDeleted(): bool { @@ -99,7 +105,7 @@ public function isDeleted(): bool } /** - * Release the job back into the queue after (n) seconds. + * Relâche le job dans la file après n secondes. */ public function release(int $delay = 0): void { @@ -107,7 +113,7 @@ public function release(int $delay = 0): void } /** - * Determine if the job was released back into the queue. + * Indique si le job a été relâché dans la file. */ public function isReleased(): bool { @@ -115,7 +121,7 @@ public function isReleased(): bool } /** - * Determine if the job has been deleted or released. + * Indique si le job a été supprimé ou relâché. */ public function isDeletedOrReleased(): bool { @@ -123,7 +129,7 @@ public function isDeletedOrReleased(): bool } /** - * Determine if the job has been marked as a failure. + * Indique si le job a été marqué en échec. */ public function hasFailed(): bool { @@ -131,7 +137,7 @@ public function hasFailed(): bool } /** - * Mark the job as "failed" + * Marque le job comme échoué. */ public function markAsFailed(): void { @@ -139,7 +145,7 @@ public function markAsFailed(): void } /** - * Delete the job, call the "failed" method, and raise the failed job event. + * Supprime le job, appelle `failed()` et émet l'événement d'échec. */ public function fail(?Throwable $e = null): void { @@ -156,9 +162,8 @@ public function fail(?Throwable $e = null): void } try { - // If the job has failed, we will delete it, call the "failed" method and then call - // an event indicating the job has failed so it can be logged if needed. This is - // to allow every developer to better keep monitor of their failed queue jobs. + // En cas d'échec : suppression, appel de failed(), puis événement + // pour permettre le suivi et la journalisation des jobs échoués. $this->delete(); $this->failed($e); @@ -168,7 +173,7 @@ public function fail(?Throwable $e = null): void } /** - * Determine if the current database transaction should be rolled back to level zero. + * Indique si la transaction SQL courante doit être annulée jusqu'au niveau zéro. */ protected function shouldRollBackDatabaseTransaction(Throwable $e): bool { @@ -181,7 +186,7 @@ protected function shouldRollBackDatabaseTransaction(Throwable $e): bool } /** - * Process an exception that caused the job to fail. + * Traite l'exception à l'origine de l'échec du job. */ protected function failed(?Throwable $e): void { @@ -195,7 +200,7 @@ protected function failed(?Throwable $e): void } /** - * Resolve the given class. + * Résout la classe donnée via le conteneur. */ protected function resolve(string $class): mixed { @@ -203,7 +208,7 @@ protected function resolve(string $class): mixed } /** - * Get the resolved job handler instance. + * Retourne l'instance du handler déjà résolue. */ public function getResolvedJob(): mixed { @@ -211,7 +216,7 @@ public function getResolvedJob(): mixed } /** - * Get the decoded body of the job. + * Retourne le corps du job décodé (tableau). */ public function payload(): array { @@ -219,7 +224,7 @@ public function payload(): array } /** - * Get the number of times to attempt a job. + * Retourne le nombre maximal de tentatives du job. */ public function maxTries(): ?int { @@ -227,7 +232,7 @@ public function maxTries(): ?int } /** - * Get the number of times to attempt a job after an exception. + * Retourne le nombre maximal d'exceptions avant échec définitif. */ public function maxExceptions(): ?int { @@ -235,7 +240,7 @@ public function maxExceptions(): ?int } /** - * Determine if the job should fail when it timeouts. + * Indique si le job doit échouer en cas de dépassement de délai. */ public function shouldFailOnTimeout(): bool { @@ -243,7 +248,7 @@ public function shouldFailOnTimeout(): bool } /** - * The number of seconds to wait before retrying a job that encountered an uncaught exception. + * Secondes d'attente avant de relancer un job ayant levé une exception non gérée. * * @return int|int[]|null */ @@ -253,7 +258,7 @@ public function backoff() } /** - * Get the number of seconds the job can run. + * Retourne la durée maximale d'exécution du job (secondes). */ public function timeout(): ?int { @@ -261,7 +266,7 @@ public function timeout(): ?int } /** - * Get the timestamp indicating when the job should timeout. + * Retourne l'horodatage limite au-delà duquel le job ne doit plus être retenté. */ public function retryUntil(): ?int { @@ -269,7 +274,7 @@ public function retryUntil(): ?int } /** - * Get the name of the queued job class. + * Retourne le nom du handler de job enfilé. */ public function getName(): string { @@ -277,9 +282,9 @@ public function getName(): string } /** - * Get the resolved display name of the queued job class. + * Retourne le nom d'affichage résolu du job. * - * Resolves the name of "wrapped" jobs such as class-based handlers. + * Résout le nom des jobs « enveloppés » (handlers de classe). */ public function resolveName(): string { @@ -287,9 +292,9 @@ public function resolveName(): string } /** - * Get the class of the queued job. + * Retourne la classe du job enfilé. * - * Resolves the class of "wrapped" jobs such as class-based handlers. + * Résout la classe des jobs « enveloppés » (handlers de classe). */ public function resolveQueuedJobClass(): string { @@ -297,7 +302,7 @@ public function resolveQueuedJobClass(): string } /** - * Get the name of the connection the job belongs to. + * Retourne le nom de la connexion du job. */ public function getConnectionName(): string { @@ -305,7 +310,7 @@ public function getConnectionName(): string } /** - * Get the name of the queue the job belongs to. + * Retourne le nom de la file du job. */ public function getQueue(): string { @@ -313,7 +318,7 @@ public function getQueue(): string } /** - * Get the service container instance. + * Retourne le conteneur de services. */ public function getContainer(): ContainerInterface { diff --git a/src/Jobs/JobName.php b/src/Jobs/JobName.php index 4720bd0..290354b 100644 --- a/src/Jobs/JobName.php +++ b/src/Jobs/JobName.php @@ -4,10 +4,13 @@ use BlitzPHP\Utilities\String\Text; +/** + * Utilitaires de résolution du nom et de la classe d'un job enfilé. + */ class JobName { /** - * Parse the given job name into a class / method array. + * Découpe le nom du job en tableau [classe, méthode]. */ public static function parse(string $job): array { @@ -15,7 +18,7 @@ public static function parse(string $job): array } /** - * Get the resolved name of the queued job class. + * Retourne le nom résolu de la classe de job. */ public static function resolve(string $name, array $payload): string { @@ -27,7 +30,7 @@ public static function resolve(string $name, array $payload): string } /** - * Get the class name for queued job class. + * Retourne le nom de classe du job enfilé. * * @param array $payload */ diff --git a/src/Jobs/SyncJob.php b/src/Jobs/SyncJob.php index 2e57657..48b0c74 100644 --- a/src/Jobs/SyncJob.php +++ b/src/Jobs/SyncJob.php @@ -5,19 +5,22 @@ use BlitzPHP\Contracts\Container\ContainerInterface; use BlitzPHP\Contracts\Queue\Job as JobContract; +/** + * Job exécuté immédiatement par le pilote synchrone (sans persistance). + */ class SyncJob extends Job implements JobContract { /** - * The class name of the job. + * Nom de classe du job. * * @var string */ protected $job; /** - * Create a new job instance. + * Crée une nouvelle instance de job. * - * @param string $payload The queue message data. + * @param string $payload Données du message de file. */ public function __construct(ContainerInterface $container, protected string $payload, string $connectionName, string $queue) { @@ -27,7 +30,7 @@ public function __construct(ContainerInterface $container, protected string $pay } /** - * Release the job back into the queue after (n) seconds. + * Relâche le job dans la file après n secondes. */ public function release(int $delay = 0): void { @@ -35,7 +38,7 @@ public function release(int $delay = 0): void } /** - * Get the number of times the job has been attempted. + * Retourne le nombre de tentatives déjà effectuées. */ public function attempts(): int { @@ -43,7 +46,7 @@ public function attempts(): int } /** - * Get the job identifier. + * Retourne l'identifiant du job. */ public function getJobId(): string { @@ -51,7 +54,7 @@ public function getJobId(): string } /** - * Get the raw body string for the job. + * Retourne le corps brut du job sous forme de chaîne. */ public function getRawBody(): string { @@ -59,7 +62,7 @@ public function getRawBody(): string } /** - * Get the name of the queue the job belongs to. + * Retourne le nom de la file du job. */ public function getQueue(): string { diff --git a/src/Manager.php b/src/Manager.php index 646320c..a51f785 100644 --- a/src/Manager.php +++ b/src/Manager.php @@ -18,25 +18,39 @@ use UnitEnum; /** + * Gestionnaire des files d'attente. + * + * Résout les pilotes, expose les connexions et permet d'écouter le cycle de vie + * des jobs et des workers. Les appels magiques sont délégués à la connexion par défaut. + * * @mixin QueueContract */ class Manager implements Factory, Monitor { /** - * The array of resolved queue drivers. - * - * @var array + * Instances de pilotes déjà résolues, indexées par nom de connexion. + * + * @var array */ protected array $drivers = []; + /** + * Gestionnaire d'événements de la file. + */ protected QueueEventManager $queueEventManager; + /** + * Cache applicatif (pause / redémarrage des workers). + */ protected Cache $cache; + /** + * Gestionnaire d'événements de l'application. + */ protected EventManagerInterface $events; /** - * Create a new queue manager instance. + * Crée une instance du gestionnaire de files. */ public function __construct(protected ContainerInterface $container, protected Config $config) { @@ -45,7 +59,7 @@ public function __construct(protected ContainerInterface $container, protected C } /** - * Register an event listener for the before job event. + * Enregistre un écouteur exécuté avant le traitement d'un job. */ public function before(callable $callback): void { @@ -53,7 +67,7 @@ public function before(callable $callback): void } /** - * Register an event listener for the after job event. + * Enregistre un écouteur exécuté après le traitement réussi d'un job. */ public function after(callable $callback): void { @@ -61,7 +75,7 @@ public function after(callable $callback): void } /** - * Register an event listener for the exception occurred job event. + * Enregistre un écouteur lorsqu'une exception survient pendant un job. */ public function exceptionOccurred(callable $callback): void { @@ -69,7 +83,7 @@ public function exceptionOccurred(callable $callback): void } /** - * Register an event listener for the daemon queue loop. + * Enregistre un écouteur à chaque itération de la boucle du daemon. */ public function looping(callable $callback): void { @@ -77,7 +91,7 @@ public function looping(callable $callback): void } /** - * Register an event listener for the failed job event. + * Enregistre un écouteur lorsqu'un job échoue définitivement. */ public function failing(callable $callback): void { @@ -85,7 +99,7 @@ public function failing(callable $callback): void } /** - * Register an event listener for the daemon queue starting. + * Enregistre un écouteur au démarrage du worker daemon. */ public function starting(callable $callback): void { @@ -93,13 +107,16 @@ public function starting(callable $callback): void } /** - * Register an event listener for the daemon queue stopping. + * Enregistre un écouteur à l'arrêt du worker daemon. */ public function stopping(callable $callback): void { $this->events->on(QueueEventManager::WORKER_STOPPING, $callback); } + /** + * Retourne (et instancie si besoin) le gestionnaire d'événements de file. + */ protected function queueEventManager(): QueueEventManager { if (! $this->queueEventManager) { @@ -110,7 +127,7 @@ protected function queueEventManager(): QueueEventManager } /** - * Determine if the driver is connected. + * Indique si le pilote (connexion) donné est déjà résolu. */ public function connected(UnitEnum|string|null $name = null): bool { @@ -120,15 +137,16 @@ public function connected(UnitEnum|string|null $name = null): bool } /** - * Resolve a queue driver instance. + * Résout une instance de connexion de file d'attente. + * + * Les pilotes sont instanciés à la demande pour éviter les connexions inutiles. */ public function driver(UnitEnum|string|null $name = null): QueueContract { $name = $name instanceof UnitEnum ? $name->name : ($name ?: $this->getDefaultDriver()); - // If the driver has not been resolved yet we will resolve it now as all - // of the drivers are resolved when they are actually needed so we do - // not make any unnecessary driver to the various queue end-points. + // Si le pilote n'a pas encore été résolu, on l'instancie maintenant : + // les connexions ne sont ouvertes que lorsqu'elles sont réellement utilisées. if (! isset($this->drivers[$name])) { $this->drivers[$name] = $this->resolve($name); @@ -139,7 +157,7 @@ public function driver(UnitEnum|string|null $name = null): QueueContract } /** - * Resolve a queue connection. + * Instancie une connexion à partir de sa configuration. * * @throws InvalidArgumentException */ @@ -158,7 +176,7 @@ protected function resolve(string $name): Queue } /** - * Pause a queue by its connection and name. + * Met une file en pause (les workers cessent d'y prélever des jobs). */ public function pause(string $connection, string $queue): void { @@ -168,7 +186,7 @@ public function pause(string $connection, string $queue): void } /** - * Pause a queue by its connection and name for a given amount of time. + * Met une file en pause pendant une durée donnée. */ public function pauseFor(string $connection, string $queue, DateTimeInterface|DateInterval|int $ttl): void { @@ -180,7 +198,7 @@ public function pauseFor(string $connection, string $queue, DateTimeInterface|Da } /** - * Resume a paused queue by its connection and name. + * Reprend une file précédemment mise en pause. */ public function resume(string $connection, string $queue): void { @@ -190,7 +208,7 @@ public function resume(string $connection, string $queue): void } /** - * Determine if a queue is paused. + * Indique si une file est actuellement en pause. */ public function isPaused(string $connection, string $queue): bool { @@ -198,9 +216,10 @@ public function isPaused(string $connection, string $queue): bool } /** - * Indicate that queue workers should not poll for restart or pause signals. + * Désactive le sondage cache des signaux de pause et de redémarrage. * - * This prevents the workers from hitting the application cache to determine if they need to pause or restart. + * Évite que les workers interrogent le cache applicatif pour savoir s'ils + * doivent se mettre en pause ou redémarrer. */ public function withoutInterruptionPolling(): void { @@ -209,7 +228,7 @@ public function withoutInterruptionPolling(): void } /** - * Get the name of the default queue connection. + * Retourne le nom de la connexion par défaut. */ public function getDefaultDriver(): string { @@ -217,7 +236,7 @@ public function getDefaultDriver(): string } /** - * Set the name of the default queue connection. + * Définit le nom de la connexion par défaut. */ public function setDefaultDriver(string $name): void { @@ -225,7 +244,7 @@ public function setDefaultDriver(string $name): void } /** - * Get the full name for the given connection. + * Retourne le nom effectif d'une connexion (ou la connexion par défaut). */ public function getName(?string $connection = null): string { @@ -233,7 +252,7 @@ public function getName(?string $connection = null): string } /** - * Get the container instance used by the manager. + * Retourne le conteneur utilisé par le gestionnaire. */ public function getContainer(): ContainerInterface { @@ -241,7 +260,7 @@ public function getContainer(): ContainerInterface } /** - * Set the container instance used by the manager. + * Définit le conteneur et le propage aux pilotes déjà résolus. */ public function setContainer(ContainerInterface $container): self { @@ -255,7 +274,7 @@ public function setContainer(ContainerInterface $container): self } /** - * Dynamically pass calls to the default connection. + * Délègue dynamiquement les appels à la connexion par défaut. */ public function __call(string $method, array $parameters = []): mixed { diff --git a/src/Models/JobModel.php b/src/Models/JobModel.php index 61d62d2..4c94aa6 100644 --- a/src/Models/JobModel.php +++ b/src/Models/JobModel.php @@ -11,20 +11,32 @@ use BlitzPHP\Utilities\Date; use Throwable; +/** + * Modèle des lignes de la table des jobs en file d'attente. + */ class JobModel extends Model { use InteractsWithTime; + /** + * Format de stockage des dates (horodatage Unix). + */ protected string $dateFormat = 'int'; + + /** + * Désactive les callbacks du modèle pendant les opérations de file. + */ protected bool $allowCallbacks = false; /** - * The expiration time of a job. + * Délai d'expiration d'un job réservé (secondes). */ protected ?int $retryAfter = 60; /** - * @param ConnectionInterface $db + * @param array $config Configuration de la connexion `database`. + * @param ConnectionResolverInterface $resolver Résolveur de connexions. + * @param ConnectionInterface $db Connexion SQL utilisée. */ public function __construct(array $config, protected ConnectionResolverInterface $resolver, ConnectionInterface $db) { @@ -33,14 +45,14 @@ public function __construct(array $config, protected ConnectionResolverInterface $this->table = $config['table']; $this->retryAfter = $config['retry_after'] ?? 60; - // Turn off the Strict Mode + // Désactive le mode transaction strict $db->transStrict(false); parent::__construct($resolver, $db); } /** - * Get the size of the queue. + * Retourne le nombre total de jobs dans la file. */ public function size(string $queue): int { @@ -50,7 +62,7 @@ public function size(string $queue): int } /** - * Get the number of pending jobs. + * Retourne le nombre de jobs en attente. */ public function pendingSize(string $queue): int { @@ -62,7 +74,7 @@ public function pendingSize(string $queue): int } /** - * Get the number of delayed jobs. + * Retourne le nombre de jobs retardés. */ public function delayedSize(string $queue): int { @@ -74,7 +86,7 @@ public function delayedSize(string $queue): int } /** - * Get the number of reserved jobs. + * Retourne le nombre de jobs réservés. */ public function reservedSize(string $queue): int { @@ -85,7 +97,7 @@ public function reservedSize(string $queue): int } /** - * Get the pending jobs for the given queue. + * Retourne les jobs en attente de la file donnée. */ public function pendingJobs(string $queue): array { @@ -97,7 +109,7 @@ public function pendingJobs(string $queue): array } /** - * Get the delayed jobs for the given queue. + * Retourne les jobs retardés de la file donnée. */ public function delayedJobs(string $queue): array { @@ -109,7 +121,7 @@ public function delayedJobs(string $queue): array } /** - * Get the reserved jobs for the given queue. + * Retourne les jobs réservés de la file donnée. */ public function reservedJobs(string $queue): array { @@ -120,7 +132,7 @@ public function reservedJobs(string $queue): array } /** - * Get the creation timestamp of the oldest pending job, excluding delayed jobs. + * Retourne l'horodatage de création du plus ancien job en attente (hors retardés). */ public function creationTimeOfOldestPendingJob(string $queue): ?int { @@ -133,7 +145,7 @@ public function creationTimeOfOldestPendingJob(string $queue): ?int } /** - * Push a raw payload to the database with a given delay of (n) seconds. + * Insère un payload brut en base avec un délai de n secondes. */ public function pushToDatabase(array $data): mixed { @@ -143,12 +155,12 @@ public function pushToDatabase(array $data): mixed } /** - * Get the next available job for the queue. + * Retourne le prochain job disponible de la file. */ public function getNextAvailableJob(string $queue): ?object { return $this->builder() - // ->lock($this->getLockForPopping()) available only in blitz-php/database > 1.2 + // ->lock($this->getLockForPopping()) disponible uniquement avec blitz-php/database > 1.2 ->where('queue', $queue) ->where(function ($query) { $this->isAvailable($query); @@ -159,7 +171,7 @@ public function getNextAvailableJob(string $queue): ?object } /** - * Delete a reserved job from the queue. + * Supprime un job réservé de la file. * * @throws Throwable */ @@ -174,7 +186,7 @@ public function deleteReserved(string $queue, string $id): void /** - * Delete all of the jobs from the queue. + * Supprime tous les jobs de la file. */ public function clear(string $queue): bool { @@ -184,7 +196,7 @@ public function clear(string $queue): bool } /** - * Modify the query to check for available jobs. + * Restreint la requête aux jobs disponibles. */ protected function isAvailable(BaseBuilder $query): void { @@ -195,7 +207,7 @@ protected function isAvailable(BaseBuilder $query): void } /** - * Modify the query to check for jobs that are reserved but have expired. + * Inclut les jobs réservés dont le verrou a expiré. */ protected function isReservedButExpired(BaseBuilder $query): void { diff --git a/src/Providers/QueueProvider.php b/src/Providers/QueueProvider.php index 2868b2e..5d1112a 100644 --- a/src/Providers/QueueProvider.php +++ b/src/Providers/QueueProvider.php @@ -8,6 +8,9 @@ use BlitzPHP\Queue\Manager; use BlitzPHP\Queue\Worker; +/** + * Fournisseur de services : lie Factory, Monitor, Manager et Worker au conteneur. + */ class QueueProvider extends AbstractProvider { /** diff --git a/src/Queue.php b/src/Queue.php index b155cea..d384fc8 100644 --- a/src/Queue.php +++ b/src/Queue.php @@ -20,44 +20,50 @@ use RuntimeException; use Throwable; +/** + * Classe de base des connexions de file d'attente. + * + * Encapsule la création du payload JSON, les hooks de sérialisation, + * le dispatch et l'émission des événements d'enfilement. + */ abstract class Queue implements QueueContract { use InteractsWithTime; /** - * The IoC container instance. + * Conteneur d'injection de dépendances. */ protected ContainerInterface $container; - + /** - * The Queue Event Manager instance. + * Gestionnaire d'événements de la file. */ protected ?QueueEventManager $eventManager = null; /** - * The connection name for the queue. + * Nom de la connexion (clé de `queue.connections`). */ protected string $connectionName = ''; /** - * The original configuration for the queue. + * Configuration brute de la connexion. */ protected array $config; /** - * Indicates that jobs should be dispatched after all database transactions have committed. + * Indique si les jobs doivent être envoyés après le commit des transactions SQL. */ protected bool $dispatchAfterCommit; /** - * The create payload callbacks. + * Callbacks exécutés lors de la construction du payload. * * @var callable[] */ protected static array $createPayloadCallbacks = []; /** - * Push a new job onto the queue. + * Envoie un job sur une file nommée. */ public function pushOn(string $queue, string|object $job, mixed $data = ''): mixed { @@ -65,7 +71,7 @@ public function pushOn(string $queue, string|object $job, mixed $data = ''): mix } /** - * Push a new job onto a specific queue after (n) seconds. + * Envoie un job sur une file nommée, avec un délai en secondes. */ public function laterOn(string $queue, DateTimeInterface|DateInterval|int $delay, string|object $job, mixed $data = ''): mixed { @@ -73,11 +79,11 @@ public function laterOn(string $queue, DateTimeInterface|DateInterval|int $delay } /** - * Push an array of jobs onto the queue. + * Envoie plusieurs jobs sur la file. + * + * @param array $jobs * - * @param array $jobs - * - * @return void + * @return void */ public function bulk(array $jobs, mixed $data = '', ?string $queue = null) { @@ -94,11 +100,10 @@ public function clear(string $queue): bool return true; } - /** - * Create a payload string from the given job and data. - * + /** + * Construit la chaîne JSON du payload à partir du job et des données. * - * @throws InvalidPayloadException + * @throws InvalidPayloadException Si l'encodage JSON échoue. */ protected function createPayload(string|object $job, string $queue, mixed $data = '', DateTimeInterface|DateInterval|int|null $delay = null): ?string { @@ -124,7 +129,7 @@ protected function createPayload(string|object $job, string $queue, mixed $data } /** - * Create a payload array from the given job and data. + * Construit le tableau de payload (objet métier ou handler sous forme de chaîne). */ protected function createPayloadArray(string|object $job, string $queue, mixed $data = ''): array { @@ -134,9 +139,9 @@ protected function createPayloadArray(string|object $job, string $queue, mixed $ } /** - * Create a payload for an object-based queue handler. + * Construit le payload d'un handler objet (job sérialisé, éventuellement chiffré). * - * @throws RuntimeException + * @throws RuntimeException Si la sérialisation du job échoue. */ protected function createObjectPayload(object $job, string $queue): array { @@ -180,7 +185,7 @@ protected function createObjectPayload(object $job, string $queue): array } /** - * Get the display name for the given job. + * Retourne le nom d'affichage du job (méthode `displayName()` ou FQCN). */ protected function getDisplayName(object $job): string { @@ -190,7 +195,7 @@ protected function getDisplayName(object $job): string } /** - * Get the maximum number of attempts for an object-based queue handler. + * Retourne le nombre maximal de tentatives défini sur le job objet. */ public function getJobTries(object $job): mixed { @@ -204,7 +209,7 @@ public function getJobTries(object $job): mixed } /** - * Get the backoff for an object-based queue handler. + * Retourne le backoff (délai de retry) du job objet, sous forme de liste CSV. */ public function getJobBackoff(object $job): mixed { @@ -226,7 +231,7 @@ public function getJobBackoff(object $job): mixed } /** - * Get the expiration timestamp for an object-based queue handler. + * Retourne l'horodatage d'expiration (`retryUntil`) du job objet. */ public function getJobExpiration(object $job): mixed { @@ -242,7 +247,7 @@ public function getJobExpiration(object $job): mixed } /** - * Determine if the job should be encrypted. + * Indique si le job doit être chiffré avant d'être persisté. */ protected function jobShouldBeEncrypted(object $job): bool { @@ -250,7 +255,7 @@ protected function jobShouldBeEncrypted(object $job): bool } /** - * Create a typical, string based queue payload array. + * Construit un payload classique pour un handler identifié par une chaîne (`Classe@méthode`). */ protected function createStringPayload(string $job, string $queue, mixed $data): array { @@ -269,7 +274,7 @@ protected function createStringPayload(string $job, string $queue, mixed $data): } /** - * Register a callback to be executed when creating job payloads. + * Enregistre un callback exécuté à la création des payloads (`null` pour tout réinitialiser). */ public static function createPayloadUsing(?callable $callback = null): void { @@ -281,7 +286,7 @@ public static function createPayloadUsing(?callable $callback = null): void } /** - * Create the given payload using any registered payload hooks. + * Applique les hooks enregistrés au tableau de payload. */ protected function withCreatePayloadHooks(string $queue, array $payload): array { @@ -295,7 +300,7 @@ protected function withCreatePayloadHooks(string $queue, array $payload): array } /** - * Enqueue a job using the given callback. + * Enfile un job via le callback fourni, après avoir émis les événements d'enfilement. */ protected function enqueueUsing(string|object $job, string $payload, ?string $queue, DateTimeInterface|DateInterval|int|null $delay, callable $callback): mixed { @@ -329,7 +334,7 @@ function () use ($queue, $job, $payload, $delay, $callback) { } /** - * Determine if the job should be dispatched after all database transactions have committed. + * Indique si le job doit attendre le commit des transactions SQL avant d'être envoyé. */ protected function shouldDispatchAfterCommit(string|object $job): bool { @@ -341,7 +346,7 @@ protected function shouldDispatchAfterCommit(string|object $job): bool } /** - * Raise the job queueing event. + * Émet l'événement « job en cours d'enfilement ». */ protected function raiseJobQueueingEvent(?string $queue, string|object $job, string $payload, DateTimeInterface|DateInterval|int|null $delay): void { @@ -349,13 +354,16 @@ protected function raiseJobQueueingEvent(?string $queue, string|object $job, str } /** - * Raise the job queued event. + * Émet l'événement « job enfilé ». */ protected function raiseJobQueuedEvent(?string $queue, string|int|null $jobId, string|object $job, string $payload, DateTimeInterface|DateInterval|int|null $delay) { $this->eventManager()->jobQueued($this->connectionName, $queue, $jobId, $job, $payload, $delay); } + /** + * Retourne (et instancie si besoin) le gestionnaire d'événements de la file. + */ protected function eventManager(): QueueEventManager { if (! $this->eventManager) { @@ -366,7 +374,7 @@ protected function eventManager(): QueueEventManager } /** - * Get the connection name for the queue. + * Retourne le nom de la connexion. */ public function getConnectionName(): string { @@ -374,7 +382,7 @@ public function getConnectionName(): string } /** - * Set the connection name for the queue. + * Définit le nom de la connexion. */ public function setConnectionName(string $name): self { @@ -384,7 +392,7 @@ public function setConnectionName(string $name): self } /** - * Get the queue configuration array. + * Retourne le tableau de configuration de la connexion. */ public function getConfig(): array { @@ -392,7 +400,7 @@ public function getConfig(): array } /** - * Set the queue configuration array. + * Définit le tableau de configuration de la connexion. */ public function setConfig(array $config): self { @@ -402,7 +410,7 @@ public function setConfig(array $config): self } /** - * Get the container instance being used by the connection. + * Retourne le conteneur IoC utilisé par la connexion. */ public function getContainer(): ContainerInterface { @@ -410,7 +418,7 @@ public function getContainer(): ContainerInterface } /** - * Set the IoC container instance. + * Définit le conteneur IoC. */ public function setContainer(ContainerInterface $container): void { diff --git a/src/Traits/Dispatchable.php b/src/Traits/Dispatchable.php index 9dade5c..4f109bd 100644 --- a/src/Traits/Dispatchable.php +++ b/src/Traits/Dispatchable.php @@ -5,6 +5,9 @@ use DateTimeInterface; use DateInterval; +/** + * Permet de dispatcher un job via des méthodes statiques (`dispatch`, `dispatchLater`, etc.). + */ trait Dispatchable { /** diff --git a/src/Traits/InteractsWithQueue.php b/src/Traits/InteractsWithQueue.php index adaa8e9..332c0f3 100644 --- a/src/Traits/InteractsWithQueue.php +++ b/src/Traits/InteractsWithQueue.php @@ -13,17 +13,20 @@ use RuntimeException; use Throwable; +/** + * Interactions d'un job métier avec la file (delete, fail, release) et assertions de test. + */ trait InteractsWithQueue { use InteractsWithTime; /** - * The underlying queue job instance. + * Instance de job de file sous-jacente. */ public ?JobContract $job = null; /** - * Get the number of times the job has been attempted. + * Retourne le nombre de tentatives déjà effectuées. */ public function attempts(): int { @@ -31,7 +34,7 @@ public function attempts(): int } /** - * Delete the job from the queue. + * Supprime le job de la file. */ public function delete(): void { @@ -41,7 +44,7 @@ public function delete(): void } /** - * Fail the job from the queue. + * Marque le job en échec depuis la file. * * @throws InvalidArgumentException */ @@ -61,7 +64,7 @@ public function fail(Throwable|string|null $exception = null): void } /** - * Release the job back into the queue after (n) seconds. + * Relâche le job dans la file après n secondes. */ public function release(DateTimeInterface|DateInterval|int $delay = 0): void { @@ -75,7 +78,7 @@ public function release(DateTimeInterface|DateInterval|int $delay = 0): void } /** - * Indicate that queue interactions like fail, delete, and release should be faked. + * Active le mode simulé pour fail, delete et release. */ public function withFakeQueueInteractions(): self { @@ -85,7 +88,7 @@ public function withFakeQueueInteractions(): self } /** - * Assert that the job was deleted from the queue. + * Vérifie que le job a été supprimé de la file. */ public function assertDeleted(): self { @@ -100,7 +103,7 @@ public function assertDeleted(): self } /** - * Assert that the job was not deleted from the queue. + * Vérifie que le job n'a pas été supprimé de la file. */ public function assertNotDeleted(): self { @@ -115,7 +118,7 @@ public function assertNotDeleted(): self } /** - * Assert that the job was manually failed. + * Vérifie que le job a été marqué en échec manuellement. */ public function assertFailed(): self { @@ -130,7 +133,7 @@ public function assertFailed(): self } /** - * Assert that the job was manually failed with a specific exception. + * Vérifie que le job a échoué manuellement avec une exception donnée. */ public function assertFailedWith(Throwable|string $exception): self { @@ -174,7 +177,7 @@ public function assertFailedWith(Throwable|string $exception): self } /** - * Assert that the job was not manually failed. + * Vérifie que le job n'a pas été marqué en échec manuellement. */ public function assertNotFailed(): self { @@ -189,7 +192,7 @@ public function assertNotFailed(): self } /** - * Assert that the job was released back onto the queue. + * Vérifie que le job a été relâché dans la file. */ public function assertReleased(DateTimeInterface|DateInterval|int|null $delay = null): self { @@ -216,7 +219,7 @@ public function assertReleased(DateTimeInterface|DateInterval|int|null $delay = } /** - * Assert that the job was not released back onto the queue. + * Vérifie que le job n'a pas été relâché dans la file. */ public function assertNotReleased(): self { @@ -231,7 +234,7 @@ public function assertNotReleased(): self } /** - * Ensure that queue interactions have been faked. + * S'assure que les interactions de file ont été simulées. * * @throws RuntimeException */ @@ -243,7 +246,7 @@ private function ensureQueueInteractionsHaveBeenFaked(): void } /** - * Set the base queue job instance. + * Définit l'instance de job de file sous-jacente. */ public function setJob(JobContract $job): self { diff --git a/src/Traits/SerializesAndRestoresModelIdentifiers.php b/src/Traits/SerializesAndRestoresModelIdentifiers.php index 25fb5cc..797c6f1 100644 --- a/src/Traits/SerializesAndRestoresModelIdentifiers.php +++ b/src/Traits/SerializesAndRestoresModelIdentifiers.php @@ -10,10 +10,13 @@ use BlitzPHP\Wolke\Relations\Pivot; use Illuminate\Contracts\Database\ModelIdentifier; +/** + * Remplace les entités / collections Wolke par des identifiants lors de la sérialisation, puis les recharge. + */ trait SerializesAndRestoresModelIdentifiers { /** - * Get the property value prepared for serialization. + * Prépare la valeur de propriété pour la sérialisation. */ protected function getSerializedPropertyValue(mixed $value, bool $withRelations = true): mixed { @@ -43,7 +46,7 @@ protected function getSerializedPropertyValue(mixed $value, bool $withRelations } /** - * Get the restored property value after deserialization. + * Restaure la valeur de propriété après désérialisation. */ protected function getRestoredPropertyValue(mixed $value): mixed { @@ -57,7 +60,7 @@ protected function getRestoredPropertyValue(mixed $value): mixed } /** - * Restore a queueable collection instance. + * Restaure une collection enfilable. * * @param \Illuminate\Contracts\Database\ModelIdentifier $value * @return WolkeCollection @@ -92,7 +95,7 @@ protected function restoreCollection($value) } /** - * Restore the model from the model identifier instance. + * Restaure le modèle à partir de son identifiant. * * @param \Illuminate\Contracts\Database\ModelIdentifier $value * @return \BlitzPHP\Wolke\Model @@ -105,7 +108,7 @@ public function restoreModel($value) } /** - * Get the query for model restoration. + * Retourne la requête de restauration du modèle. * * @template TModel of \BlitzPHP\Wolke\Model * diff --git a/src/Traits/SerializesModels.php b/src/Traits/SerializesModels.php index 5bdbbfc..76db645 100644 --- a/src/Traits/SerializesModels.php +++ b/src/Traits/SerializesModels.php @@ -5,12 +5,15 @@ use ReflectionClass; use ReflectionProperty; +/** + * Sérialise et restaure les propriétés d'un job, y compris les modèles liés. + */ trait SerializesModels { use SerializesAndRestoresModelIdentifiers; /** - * Prepare the instance values for serialization. + * Prépare les valeurs de l'instance pour la sérialisation. */ public function __serialize(): array { @@ -60,7 +63,7 @@ public function __serialize(): array } /** - * Restore the model after serialization. + * Restaure le modèle après désérialisation. */ public function __unserialize(array $values): void { @@ -92,7 +95,7 @@ public function __unserialize(array $values): void } /** - * Get the property value for the given property. + * Retourne la valeur de la propriété donnée. */ protected function getPropertyValue(ReflectionProperty $property): mixed { diff --git a/src/Worker.php b/src/Worker.php index d37fea1..4803bfd 100644 --- a/src/Worker.php +++ b/src/Worker.php @@ -12,96 +12,105 @@ use BlitzPHP\Queue\Exceptions\TimeoutExceededException; use BlitzPHP\Utilities\Date; use Illuminate\Contracts\Debug\ExceptionHandler; -// use BlitzPHP\Database\DetectsLostConnections; // available only in blitz-php/database 1.1 +// use BlitzPHP\Database\DetectsLostConnections; // disponible uniquement dans blitz-php/database 1.1 use Throwable; +/** + * Worker de file d'attente. + * + * Prélève et exécute les jobs en boucle (daemon) ou un par un, gère les + * timeouts, les tentatives, la mémoire et les signaux POSIX. + */ class Worker { // use DetectsLostConnections; + /** Code de sortie en cas de succès. */ const EXIT_SUCCESS = EXIT_SUCCESS; + /** Code de sortie en cas d'erreur. */ const EXIT_ERROR = EXIT_ERROR; + /** Code de sortie en cas de dépassement de la limite mémoire. */ const EXIT_MEMORY_LIMIT = 12; /** - * The name of the worker. + * Nom du worker. */ protected ?string $name; /** - * The cache repository implementation. + * Implémentation du dépôt de cache. */ protected CacheInterface $cache; /** - * The exception handler instance. + * Gestionnaire d'exceptions (contrat Illuminate). * * @var \Illuminate\Contracts\Debug\ExceptionHandler */ protected $exceptions; /** - * The callback used to determine if the application is in maintenance mode. + * Callback indiquant si l'application est en maintenance. * * @var callable */ protected $isDownForMaintenance; /** - * The callback used to reset the application's scope. + * Callback de réinitialisation du périmètre applicatif entre deux jobs. * * @var callable */ protected $resetScope; /** - * Indicates if the worker should exit. + * Indique si le worker doit s'arrêter. */ public bool $shouldQuit = false; /** - * Indicates if the worker lost its connection. + * Indique si le worker a perdu sa connexion. */ public bool $lostConnection = false; /** - * Indicates if the worker is paused. + * Indique si le worker est en pause. */ public bool $paused = false; /** - * The callbacks used to pop jobs from queues. + * Callbacks utilisés pour prélever les jobs. * * @var callable[] */ protected static array $popCallbacks = []; /** - * The custom exit code to be used when memory is exceeded. + * Code de sortie personnalisé en cas de dépassement mémoire. */ public static ?int $memoryExceededExitCode = null; /** - * Indicates if the worker should report job exceptions. + * Indique si les exceptions de job doivent être journalisées. */ public static bool $reportJobExceptions = true; /** - * Indicates if the worker should check for the restart signal in the cache. + * Indique si le worker doit consulter le signal de redémarrage en cache. */ public static bool $restartable = true; /** - * Indicates if the worker should check for the paused signal in the cache. + * Indique si le worker doit consulter le signal de pause en cache. */ public static bool $pausable = true; /** - * Create a new queue worker. + * Crée un worker de file d'attente. * - * @param Manager $manager The queue manager instance. - * @param QueueEventManager $events The queue event manager instance. + * @param Manager $manager Instance du gestionnaire de files. + * @param QueueEventManager $events Instance du gestionnaire d'événements de file. * @param \Illuminate\Contracts\Debug\ExceptionHandler $exceptions */ public function __construct( @@ -117,7 +126,7 @@ public function __construct( } /** - * Listen to the given queue in a loop. + * Écoute la file donnée en boucle (mode daemon). */ public function daemon(string $connectionName, string $queue, WorkerOptions $options): int { @@ -132,9 +141,7 @@ public function daemon(string $connectionName, string $queue, WorkerOptions $opt $this->raiseWorkerStartingEvent($connectionName, $queue, $options); while (true) { - // Before reserving any jobs, we will make sure this queue is not paused and - // if it is we will just pause this worker for a given amount of time and - // make sure we do not need to kill this worker process off completely. + // Avant de réserver un job, on vérifie que la file n'est pas en pause. if (! $this->daemonShouldRun($options, $connectionName, $queue)) { [$status, $reason] = $this->pauseWorker($options, $lastRestart); @@ -149,9 +156,7 @@ public function daemon(string $connectionName, string $queue, WorkerOptions $opt ($this->resetScope)(); } - // First, we will attempt to get the next job off of the queue. We will also - // register the timeout handler and reset the alarm for this job so it is - // not stuck in a frozen state forever. Then, we can fire off this job. + // Prélèvement du prochain job, enregistrement du timeout, puis exécution. $job = $this->getNextJob( $this->manager->driver($connectionName), $queue ); @@ -160,9 +165,7 @@ public function daemon(string $connectionName, string $queue, WorkerOptions $opt $this->registerTimeoutHandler($job, $options); } - // If the daemon should run (not in maintenance mode, etc.), then we can run - // fire off this job for processing. Otherwise, we will need to sleep the - // worker so no more jobs are processed until they should be processed. + // Si un job est disponible, on le traite ; sinon on attend avant de resonder. if ($job) { $jobsProcessed++; @@ -179,9 +182,7 @@ public function daemon(string $connectionName, string $queue, WorkerOptions $opt $this->resetTimeoutHandler(); } - // Finally, we will check to see if we have exceeded our memory limits or if - // the queue should restart based on other indications. If so, we'll stop - // this worker and let whatever is "monitoring" it restart the process. + // Arrêt si limite mémoire, signal de redémarrage, file vide, max jobs/temps, etc. [$status, $reason] = $this->stopIfNecessary( $options, $lastRestart, $startTime, $jobsProcessed, $job ); @@ -193,13 +194,11 @@ public function daemon(string $connectionName, string $queue, WorkerOptions $opt } /** - * Register the worker timeout handler. + * Enregistre le gestionnaire de dépassement de délai du worker. */ protected function registerTimeoutHandler(Job $job, WorkerOptions $options): void { - // We will register a signal handler for the alarm signal so that we can kill this - // process if it is running too long because it has frozen. This uses the async - // signals supported in recent versions of PHP to accomplish it conveniently. + // Gestionnaire SIGALRM : interrompt un job bloqué trop longtemps (signaux async PHP). pcntl_signal(SIGALRM, function () use ($job, $options) { if ($job) { $this->markJobAsFailedIfWillExceedMaxAttempts( @@ -226,7 +225,7 @@ protected function registerTimeoutHandler(Job $job, WorkerOptions $options): voi } /** - * Reset the worker timeout handler. + * Réinitialise le gestionnaire de dépassement de délai. */ protected function resetTimeoutHandler(): void { @@ -234,7 +233,7 @@ protected function resetTimeoutHandler(): void } /** - * Get the appropriate timeout for the given job. + * Retourne le délai d'exécution applicable au job. */ protected function timeoutForJob(Job $job, WorkerOptions $options): int { @@ -242,7 +241,7 @@ protected function timeoutForJob(Job $job, WorkerOptions $options): int } /** - * Determine if the daemon should process on this iteration. + * Indique si le daemon doit traiter un job à cette itération. */ protected function daemonShouldRun(WorkerOptions $options, string $connectionName, string $queue): bool { @@ -251,7 +250,7 @@ protected function daemonShouldRun(WorkerOptions $options, string $connectionNam } /** - * Pause the worker for the current loop. + * Met le worker en pause pour l'itération courante. */ protected function pauseWorker(WorkerOptions $options, int $lastRestart): ?array { @@ -261,7 +260,7 @@ protected function pauseWorker(WorkerOptions $options, int $lastRestart): ?array } /** - * Determine the exit code to stop the process if necessary. + * Détermine le code de sortie si le processus doit s'arrêter. */ protected function stopIfNecessary(WorkerOptions $options, int $lastRestart, float|int $startTime = 0, int $jobsProcessed = 0, mixed $job = null): ?array { @@ -278,7 +277,7 @@ protected function stopIfNecessary(WorkerOptions $options, int $lastRestart, flo } /** - * Process the next job on the queue. + * Traite le prochain job de la file. */ public function runNextJob(string $connectionName, string $queue, WorkerOptions $options): void { @@ -286,9 +285,7 @@ public function runNextJob(string $connectionName, string $queue, WorkerOptions $this->manager->connection($connectionName), $queue ); - // If we're able to pull a job off of the stack, we will process it and then return - // from this method. If there is no job on the queue, we will "sleep" the worker - // for the specified number of seconds, then keep processing jobs after sleep. + // Job disponible : traitement immédiat. File vide : pause puis nouvelle tentative. if ($job) { $this->runJob($job, $connectionName, $options); @@ -299,7 +296,7 @@ public function runNextJob(string $connectionName, string $queue, WorkerOptions } /** - * Get the next job from the queue driver. + * Prélève le prochain job via le pilote de file. */ protected function getNextJob(Queue $driver, string $queue): ?Job { @@ -342,7 +339,7 @@ protected function getNextJob(Queue $driver, string $queue): ?Job } /** - * Determine if a given connection and queue is paused. + * Indique si la file de la connexion donnée est en pause. */ protected function queuePaused(string $connectionName, string $queue): bool { @@ -354,7 +351,7 @@ protected function queuePaused(string $connectionName, string $queue): bool } /** - * Process the given job. + * Traite le job donné. */ protected function runJob(Job $job, string $connectionName, WorkerOptions $options): void { @@ -371,7 +368,7 @@ protected function runJob(Job $job, string $connectionName, WorkerOptions $optio } /** - * Stop the worker if we have lost connection to a database. + * Arrête le worker si la connexion base de données est perdue. */ protected function stopWorkerIfLostConnection(Throwable $e): void { @@ -383,16 +380,14 @@ protected function stopWorkerIfLostConnection(Throwable $e): void } /** - * Process the given job from the queue. + * Traite le job prélevé de la file. * * @throws Throwable */ public function process(string $connectionName, Job $job, WorkerOptions $options): void { try { - // First we will raise the before job event and determine if the job has already run - // over its maximum attempt limits, which could primarily happen when this job is - // continually timing out and not actually throwing any exceptions from itself. + // Événement « avant job » puis contrôle du nombre maximal de tentatives. $this->raiseBeforeJobEvent($connectionName, $job); $this->markJobAsFailedIfAlreadyExceedsMaxAttempts( @@ -405,9 +400,7 @@ public function process(string $connectionName, Job $job, WorkerOptions $options return; } - // Here we will fire off the job and let it process. We will catch any exceptions, so - // they can be reported to the developer's logs, etc. Once the job is finished the - // proper events will be fired to let any listeners know this job has completed. + // Exécution du job ; les exceptions sont capturées pour journalisation et relâchement. $job->fire(); $this->raiseAfterJobEvent($connectionName, $job); @@ -421,16 +414,14 @@ public function process(string $connectionName, Job $job, WorkerOptions $options } /** - * Handle an exception that occurred while the job was running. + * Traite une exception survenue pendant l'exécution du job. * * @throws Throwable */ protected function handleJobException(string $connectionName, Job $job, WorkerOptions $options, Throwable $e): void { try { - // First, we will go ahead and mark the job as failed if it will exceed the maximum - // attempts it is allowed to run the next time we process it. If so we will just - // go ahead and mark it as failed now so we do not have to release this again. + // Marque le job en échec s'il dépassera le quota de tentatives à la prochaine exécution. if (! $job->hasFailed()) { $this->markJobAsFailedIfWillExceedMaxAttempts( $connectionName, $job, (int) $options->maxTries, $e @@ -445,9 +436,7 @@ protected function handleJobException(string $connectionName, Job $job, WorkerOp $connectionName, $job, $e ); } finally { - // If we catch an exception, we will attempt to release the job back onto the queue - // so it is not lost entirely. This'll let the job be retried at a later time by - // another listener (or this same one). We will re-throw this exception after. + // Relâche le job dans la file pour une tentative ultérieure, puis relance l'exception. if (! $job->isDeleted() && ! $job->isReleased() && ! $job->hasFailed()) { $backoff = $this->calculateBackoff($job, $options); @@ -461,9 +450,9 @@ protected function handleJobException(string $connectionName, Job $job, WorkerOp } /** - * Mark the given job as failed if it has exceeded the maximum allowed attempts. + * Marque le job en échec s'il a dépassé le nombre maximal de tentatives. * - * This will likely be because the job previously exceeded a timeout. + * Souvent dû à un dépassement de délai lors d'une tentative précédente. * * @throws Throwable */ @@ -487,7 +476,7 @@ protected function markJobAsFailedIfAlreadyExceedsMaxAttempts(string $connection } /** - * Mark the given job as failed if it has exceeded the maximum allowed attempts. + * Marque le job en échec s'il a dépassé le nombre maximal de tentatives. */ protected function markJobAsFailedIfWillExceedMaxAttempts(string $connectionName, Job $job, int $maxTries, Throwable $e): void { @@ -503,7 +492,7 @@ protected function markJobAsFailedIfWillExceedMaxAttempts(string $connectionName } /** - * Mark the given job as failed if it has exceeded the maximum allowed attempts. + * Marque le job en échec s'il a dépassé le nombre maximal de tentatives. */ protected function markJobAsFailedIfWillExceedMaxExceptions(string $connectionName, Job $job, Throwable $e): void { @@ -524,7 +513,7 @@ protected function markJobAsFailedIfWillExceedMaxExceptions(string $connectionNa } /** - * Mark the given job as failed if it should fail on timeouts. + * Marque le job en échec s'il doit échouer au timeout. */ protected function markJobAsFailedIfItShouldFailOnTimeout(string $connectionName, Job $job, Throwable $e): void { @@ -534,7 +523,7 @@ protected function markJobAsFailedIfItShouldFailOnTimeout(string $connectionName } /** - * Mark the given job as failed and raise the relevant event. + * Marque le job en échec et émet l'événement correspondant. */ protected function failJob(Job $job, Throwable $e): void { @@ -542,7 +531,7 @@ protected function failJob(Job $job, Throwable $e): void } /** - * Calculate the backoff for the given job. + * Calcule le délai de retry du job. */ protected function calculateBackoff(Job $job, WorkerOptions $options): int { @@ -557,7 +546,7 @@ protected function calculateBackoff(Job $job, WorkerOptions $options): int } /** - * Raise an event indicating the worker is starting. + * Émet l'événement de démarrage du worker. */ protected function raiseWorkerStartingEvent(string $connectionName, string $queue, WorkerOptions $options): void { @@ -565,7 +554,7 @@ protected function raiseWorkerStartingEvent(string $connectionName, string $queu } /** - * Raise an event indicating a job is being popped from the queue. + * Émet l'événement de prélèvement imminent. */ protected function raiseBeforeJobPopEvent(string $connectionName, ?string $queue = null): void { @@ -573,7 +562,7 @@ protected function raiseBeforeJobPopEvent(string $connectionName, ?string $queue } /** - * Raise an event indicating a job has been popped from the queue. + * Émet l'événement de job prélevé. */ protected function raiseAfterJobPopEvent(string $connectionName, ?Job $job): void { @@ -581,7 +570,7 @@ protected function raiseAfterJobPopEvent(string $connectionName, ?Job $job): voi } /** - * Raise an event indicating a job is being processed. + * Émet l'événement de traitement en cours. */ protected function raiseBeforeJobEvent(string $connectionName, Job $job): void { @@ -589,7 +578,7 @@ protected function raiseBeforeJobEvent(string $connectionName, Job $job): void } /** - * Raise an event indicating a job has been processed. + * Émet l'événement de job traité. */ protected function raiseAfterJobEvent(string $connectionName, Job $job): void { @@ -597,7 +586,7 @@ protected function raiseAfterJobEvent(string $connectionName, Job $job): void } /** - * Raise the exception occurred queue job event. + * Émet l'événement d'exception survenue sur un job. */ protected function raiseExceptionOccurredJobEvent(string $connectionName, Job $job, Throwable $e): void { @@ -605,7 +594,7 @@ protected function raiseExceptionOccurredJobEvent(string $connectionName, Job $j } /** - * Determine if the queue worker should restart. + * Indique si le worker doit redémarrer. */ protected function queueShouldRestart(?int $lastRestart): bool { @@ -617,7 +606,7 @@ protected function queueShouldRestart(?int $lastRestart): bool } /** - * Get the last queue restart timestamp, or null. + * Retourne l'horodatage du dernier signal de redémarrage, ou null. */ protected function getTimestampOfLastQueueRestart(): ?int { @@ -633,7 +622,7 @@ protected function getTimestampOfLastQueueRestart(): ?int } /** - * Enable async signals for the process. + * Active la gestion asynchrone des signaux pour le processus. */ protected function listenForSignals(): void { @@ -647,7 +636,7 @@ protected function listenForSignals(): void } /** - * Determine if "async" signals are supported. + * Indique si les signaux asynchrones sont disponibles. */ protected function supportsAsyncSignals(): bool { @@ -655,7 +644,7 @@ protected function supportsAsyncSignals(): bool } /** - * Determine if the memory limit has been exceeded. + * Indique si la limite mémoire a été dépassée. */ public function memoryExceeded(int $memoryLimit): bool { @@ -663,7 +652,7 @@ public function memoryExceeded(int $memoryLimit): bool } /** - * Stop listening and bail out of the script. + * Arrête l'écoute et quitte le script. */ public function stop(int $status = 0, ?WorkerOptions $options = null, ?WorkerStopReason $reason = null): int { @@ -673,7 +662,7 @@ public function stop(int $status = 0, ?WorkerOptions $options = null, ?WorkerSto } /** - * Kill the process. + * Termine le processus. */ public function kill(int $status = 0, ?WorkerOptions $options = null, ?WorkerStopReason $reason = null): never { @@ -687,7 +676,7 @@ public function kill(int $status = 0, ?WorkerOptions $options = null, ?WorkerSto } /** - * Create an instance of MaxAttemptsExceededException. + * Crée une instance de MaxAttemptsExceededException. */ protected function maxAttemptsExceededException(Job $job): MaxAttemptsExceededException { @@ -695,7 +684,7 @@ protected function maxAttemptsExceededException(Job $job): MaxAttemptsExceededEx } /** - * Create an instance of TimeoutExceededException. + * Crée une instance de TimeoutExceededException. */ protected function timeoutExceededException(Job $job): TimeoutExceededException { @@ -703,7 +692,7 @@ protected function timeoutExceededException(Job $job): TimeoutExceededException } /** - * Sleep the script for a given number of seconds. + * Met le script en pause pendant un nombre de secondes donné. */ public function sleep(int|float $seconds): void { @@ -715,7 +704,7 @@ public function sleep(int|float $seconds): void } /** - * Set the cache repository implementation. + * Définit l'implémentation du cache. */ public function setCache(CacheInterface $cache): self { @@ -725,7 +714,7 @@ public function setCache(CacheInterface $cache): self } /** - * Set the name of the worker. + * Définit le nom du worker. */ public function setName(string $name): self { @@ -735,7 +724,7 @@ public function setName(string $name): self } /** - * Register a callback to be executed to pick jobs. + * Enregistre un callback de prélèvement des jobs. */ public static function popUsing(string $workerName, callable $callback): void { @@ -747,7 +736,7 @@ public static function popUsing(string $workerName, callable $callback): void } /** - * Get the queue manager instance. + * Retourne le gestionnaire de files. */ public function getManager(): Manager { @@ -755,7 +744,7 @@ public function getManager(): Manager } /** - * Set the queue manager instance. + * Définit le gestionnaire de files. */ public function setManager(Manager $manager): void { diff --git a/src/WorkerOptions.php b/src/WorkerOptions.php new file mode 100644 index 0000000..51e28dd --- /dev/null +++ b/src/WorkerOptions.php @@ -0,0 +1,41 @@ + Date: Thu, 27 Aug 2026 20:22:45 +0100 Subject: [PATCH 4/5] style: cs-fix --- .php-cs-fixer.dist.php | 6 +- spec/bootstrap.php | 5 +- src/CallQueuedClosure.php | 21 +- src/CallQueuedHandler.php | 94 +++++---- src/Commands/Work.php | 96 +++++---- src/Compatibility/SignalTrait.php | 9 + src/Config/Services.php | 28 +-- src/Config/queue.php | 37 ++-- src/DTO/Config.php | 46 +++-- src/DTO/WorkerOptions.php | 35 ++-- .../2026-08-26-061438_CreateQueueTables.php | 13 +- src/Drivers/ConnectorInterface.php | 9 + src/Drivers/DatabaseDriver.php | 109 ++++++----- src/Drivers/FailoverDriver.php | 16 +- src/Drivers/NullDriver.php | 21 +- src/Drivers/SyncDriver.php | 20 +- src/Enums/WorkerStopReason.php | 41 +++- src/Events/QueueEvent.php | 29 ++- src/Events/QueueEventManager.php | 173 +++++++++-------- src/Exceptions/InvalidPayloadException.php | 9 + src/Exceptions/ManuallyFailedException.php | 10 +- .../MaxAttemptsExceededException.php | 11 +- src/Exceptions/TimeoutExceededException.php | 11 +- src/Failed/CountableFailedJobProvider.php | 12 +- src/Failed/DatabaseFailedJobProvider.php | 33 +++- src/Failed/DatabaseUuidFailedJobProvider.php | 21 +- src/Failed/FailedJobProviderInterface.php | 22 ++- src/Failed/FileFailedJobProvider.php | 34 ++-- src/Failed/NullFailedJobProvider.php | 18 +- src/Failed/PrunableFailedJobProvider.php | 12 +- src/Job.php | 13 +- src/Jobs/DatabaseJob.php | 17 +- src/Jobs/DatabaseJobRecord.php | 14 +- src/Jobs/FakeJob.php | 19 +- src/Jobs/InspectedJob.php | 21 +- src/Jobs/Job.php | 26 ++- src/Jobs/JobName.php | 11 +- src/Jobs/SyncJob.php | 15 +- src/Manager.php | 66 ++++--- src/Models/JobModel.php | 38 ++-- src/Providers/QueueProvider.php | 9 + src/Queue.php | 99 +++++----- src/Traits/Dispatchable.php | 16 +- src/Traits/InteractsWithQueue.php | 25 ++- .../SerializesAndRestoresModelIdentifiers.php | 53 +++-- src/Traits/SerializesModels.php | 19 +- src/Worker.php | 182 +++++++++++------- src/WorkerOptions.php | 35 ++-- 48 files changed, 1087 insertions(+), 592 deletions(-) diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index ade3aba..4d445eb 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -1,9 +1,9 @@ + * (c) 2026 Dimitri Sitchet Tomkeu * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. @@ -42,5 +42,5 @@ 'BlitzPHP Queue', 'Dimitri Sitchet Tomkeu', 'devcode.dst@gmail.com', - 2026 + 2026, ); diff --git a/spec/bootstrap.php b/spec/bootstrap.php index 0331466..c0fef57 100644 --- a/spec/bootstrap.php +++ b/spec/bootstrap.php @@ -1,11 +1,10 @@ + * (c) 2026 Dimitri Sitchet Tomkeu * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ - diff --git a/src/CallQueuedClosure.php b/src/CallQueuedClosure.php index 5f14d88..8217ff0 100644 --- a/src/CallQueuedClosure.php +++ b/src/CallQueuedClosure.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue; use BlitzPHP\Contracts\Container\ContainerInterface; @@ -16,12 +25,14 @@ */ class CallQueuedClosure { - use Dispatchable, InteractsWithQueue, SerializesModels; + use Dispatchable; + use InteractsWithQueue; + use SerializesModels; /** * Instance de Closure sérialisable. * - * @var \Laravel\SerializableClosure\SerializableClosure + * @var SerializableClosure */ public $closure; @@ -79,7 +90,7 @@ public function onFailure(callable $callback): self /** * Traite l'échec du job. */ - public function failed(Throwable $e):void + public function failed(Throwable $e): void { foreach ($this->failureCallbacks as $callback) { $callback($e); @@ -97,9 +108,9 @@ public function displayName(): string $reflection = new ReflectionFunction($closure); - $prefix = is_null($this->name) ? '' : "{$this->name} - "; + $prefix = null === $this->name ? '' : "{$this->name} - "; - return $prefix.'Closure ('.basename($reflection->getFileName()).':'.$reflection->getStartLine().')'; + return $prefix . 'Closure (' . basename($reflection->getFileName()) . ':' . $reflection->getStartLine() . ')'; } /** diff --git a/src/CallQueuedHandler.php b/src/CallQueuedHandler.php index 170bc13..e62e7db 100644 --- a/src/CallQueuedHandler.php +++ b/src/CallQueuedHandler.php @@ -1,10 +1,22 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue; +use __PHP_Incomplete_Class; use BlitzPHP\Contracts\Container\ContainerInterface; use BlitzPHP\Contracts\Queue\Job; use BlitzPHP\Contracts\Security\EncrypterInterface; use BlitzPHP\Queue\Exceptions\MaxAttemptsExceededException; +use BlitzPHP\Queue\Failed\FailedJobProviderInterface; use BlitzPHP\Utilities\Helpers; use BlitzPHP\Wolke\Exceptions\ModelNotFoundException; use Exception; @@ -32,9 +44,9 @@ public function call(Job $job, array $data): void try { // Récupérer la commande (le job utilisateur) $command = $this->getCommand($data); - + // Vérifier si c'est une classe incomplète - if ($command instanceof \__PHP_Incomplete_Class) { + if ($command instanceof __PHP_Incomplete_Class) { throw new Exception('Job is incomplete class: ' . json_encode($command)); } @@ -50,16 +62,16 @@ public function call(Job $job, array $data): void $this->executeCommand($command); // Si le job n'a pas été supprimé ou relâché, on le supprime - if (!$job->isDeletedOrReleased()) { + if (! $job->isDeletedOrReleased()) { $job->delete(); } - } catch (ModelNotFoundException $e) { // Gérer le cas où un modèle n'est pas trouvé $this->handleModelNotFound($job, $e); } catch (Throwable $e) { // Gérer les autres exceptions $this->handleException($job, $data, $e); + throw $e; } } @@ -69,12 +81,12 @@ public function call(Job $job, array $data): void */ protected function getCommand(array $data): mixed { - if (!isset($data['command'])) { + if (! isset($data['command'])) { throw new RuntimeException('Job data missing "command" key.'); } // Si c'est déjà un objet (pour les jobs sync) - if (is_object($data['command']) && !is_string($data['command'])) { + if (is_object($data['command']) && ! is_string($data['command'])) { return $data['command']; } @@ -92,7 +104,7 @@ protected function getCommand(array $data): mixed if ($this->container->bound(EncrypterInterface::class)) { try { $decrypted = $this->container->get(EncrypterInterface::class)->decrypt($data['command']); - $command = unserialize($decrypted); + $command = unserialize($decrypted); if ($command !== false) { return $command; } @@ -131,9 +143,9 @@ protected function setJobInstanceIfNecessary(Job $job, mixed $instance): mixed protected function usesInteractsWithQueue(object $instance): bool { $traits = Helpers::classUsesRecursive($instance); - - return isset($traits[Traits\InteractsWithQueue::class]) || - isset($traits['BlitzPHP\\Queue\\Traits\\InteractsWithQueue']); + + return isset($traits[Traits\InteractsWithQueue::class]) + || isset($traits['BlitzPHP\\Queue\\Traits\\InteractsWithQueue']); } /** @@ -144,23 +156,26 @@ protected function executeCommand(object $command): void // Si c'est un CallQueuedClosure if ($command instanceof CallQueuedClosure) { $command->handle($this->container); + return; } // Si le job a une méthode handle() (cas standard) if (method_exists($command, 'handle')) { $this->container->call([$command, 'handle']); + return; } // Si c'est callable (__invoke) if (is_callable($command)) { $this->container->call($command); + return; } throw new RuntimeException( - 'Job does not have a handle() method and is not callable: ' . get_class($command) + 'Job does not have a handle() method and is not callable: ' . $command::class, ); } @@ -176,6 +191,7 @@ protected function handleException(Job $job, array $data, Throwable $e): void // Récupérer la commande pour les métadonnées $command = null; + try { $command = $this->getCommand($data); } catch (Throwable $parseError) { @@ -189,7 +205,7 @@ protected function handleException(Job $job, array $data, Throwable $e): void if ($attempts >= $maxTries) { // Marquer comme échoué $job->markAsFailed(); - + // Appeler la méthode failed du job si elle existe if ($command && method_exists($command, 'failed')) { try { @@ -201,12 +217,12 @@ protected function handleException(Job $job, array $data, Throwable $e): void // Logger l'échec logger()->error('Job failed after max attempts', [ - 'job' => $this->getJobName($command, $data), - 'attempts' => $attempts, + 'job' => $this->getJobName($command, $data), + 'attempts' => $attempts, 'max_tries' => $maxTries, - 'error' => $e->getMessage(), - 'job_id' => $job->getJobId(), - 'queue' => $job->getQueue(), + 'error' => $e->getMessage(), + 'job_id' => $job->getJobId(), + 'queue' => $job->getQueue(), ]); // Enregistrer dans le provider de jobs échoués @@ -218,23 +234,23 @@ protected function handleException(Job $job, array $data, Throwable $e): void throw new MaxAttemptsExceededException( 'Job failed after ' . $maxTries . ' attempts: ' . $e->getMessage(), 0, - $e + $e, ); } // Calculer le backoff $backoff = $this->calculateBackoff($command, $attempts); - + // Relâcher le job avec backoff $job->release($backoff); logger()->warning('Job released for retry', [ - 'job' => $this->getJobName($command, $data), + 'job' => $this->getJobName($command, $data), 'attempts' => $attempts, - 'backoff' => $backoff, - 'error' => $e->getMessage(), - 'job_id' => $job->getJobId(), - 'queue' => $job->getQueue(), + 'backoff' => $backoff, + 'error' => $e->getMessage(), + 'job_id' => $job->getJobId(), + 'queue' => $job->getQueue(), ]); } @@ -288,7 +304,7 @@ protected function calculateBackoff(?object $command, int $attempts): int // Si le backoff est 0, on utilise un backoff exponentiel if ($backoff === 0) { - $backoff = 60 * pow(2, $attempts - 1); + $backoff = 60 * 2 ** ($attempts - 1); } return $backoff; @@ -300,7 +316,7 @@ protected function calculateBackoff(?object $command, int $attempts): int protected function getJobName(?object $command, array $data): string { if ($command !== null) { - return get_class($command); + return $command::class; } return $data['commandName'] ?? $data['displayName'] ?? 'Unknown'; @@ -312,19 +328,19 @@ protected function getJobName(?object $command, array $data): string protected function logFailedJob(Job $job, Throwable $e): void { try { - $failedProvider = $this->container->get(\BlitzPHP\Queue\Failed\FailedJobProviderInterface::class); - + $failedProvider = $this->container->get(FailedJobProviderInterface::class); + $failedProvider->log( $job->getConnectionName(), $job->getQueue(), $job->getRawBody(), - $e + $e, ); } catch (Throwable $logError) { // Ignorer les erreurs de logging logger()->error('Failed to log failed job', [ - 'error' => $logError->getMessage(), - 'job_id' => $job->getJobId() + 'error' => $logError->getMessage(), + 'job_id' => $job->getJobId(), ]); } } @@ -335,15 +351,16 @@ protected function logFailedJob(Job $job, Throwable $e): void protected function handleModelNotFound(Job $job, ModelNotFoundException $e): void { $payload = $job->payload(); - + // Vérifier si on doit supprimer le job quand les modèles sont manquants if (isset($payload['deleteWhenMissingModels']) && $payload['deleteWhenMissingModels']) { $job->delete(); logger()->warning('Job deleted because model was not found', [ 'job_id' => $job->getJobId(), - 'queue' => $job->getQueue(), - 'model' => $e->getModel(), + 'queue' => $job->getQueue(), + 'model' => $e->getModel(), ]); + return; } @@ -359,8 +376,8 @@ public function failed(array $data, Throwable $e, string $uuid, ?Job $job = null { try { $command = $this->getCommand($data); - - if ($command instanceof \__PHP_Incomplete_Class) { + + if ($command instanceof __PHP_Incomplete_Class) { return; } @@ -374,11 +391,10 @@ public function failed(array $data, Throwable $e, string $uuid, ?Job $job = null } logger()->critical('Job permanently failed', [ - 'job' => $this->getJobName($command ?? null, $data), - 'uuid' => $uuid, + 'job' => $this->getJobName($command ?? null, $data), + 'uuid' => $uuid, 'error' => $e->getMessage(), ]); - } catch (Throwable $handledError) { // Ignorer les erreurs dans failed() logger()->error('Error in CallQueuedHandler::failed', [ diff --git a/src/Commands/Work.php b/src/Commands/Work.php index 7baee5e..1a16372 100644 --- a/src/Commands/Work.php +++ b/src/Commands/Work.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Commands; use BlitzPHP\Cache\Handlers\BaseHandler; @@ -25,22 +34,32 @@ class Work extends Command { use InteractsWithTime; - - /** @var string Groupe auquel appartient la commande */ + + /** + * @var string Groupe auquel appartient la commande + */ protected $group = 'Queue'; - /** @var string Nom de la commande */ + /** + * @var string Nom de la commande + */ protected $name = 'queue:work'; - /** @var string Description de la commande */ + /** + * @var string Description de la commande + */ protected $description = 'Traite les jobs de la file d\'attente en mode daemon'; - /** @var array Arguments de la commande */ + /** + * @var array Arguments de la commande + */ protected $arguments = [ 'connection' => 'Nom de la connexion de file à traiter', ]; - /** @var array Options de la commande */ + /** + * @var array Options de la commande + */ protected $options = [ '--name' => ['Nom du worker', 'default'], '--queue' => ['Noms des files à traiter (séparés par des virgules)'], @@ -75,7 +94,6 @@ class Work extends Command */ protected EventManagerInterface $events; - /** * Horodatage de début du dernier job traité, s'il y en a un. */ @@ -128,12 +146,13 @@ public function execute(array $params) if (! $this->outputUsingJson() && static::terminalHasSttyAvailable()) { $this->info( - sprintf('Processing jobs from the [%s] %s.', $queue, (new Stringable('queue'))->plural(explode(',', $queue))) + sprintf('Processing jobs from the [%s] %s.', $queue, (new Stringable('queue'))->plural(explode(',', $queue))), ); } return $this->runWorker( - $connection, $queue + $connection, + $queue, ); } @@ -146,7 +165,9 @@ protected function runWorker(string $connection, string $queue): ?int ->setName($this->option('name')) ->setCache($this->cache) ->{$this->option('once') ? 'runNextJob' : 'daemon'}( - $connection, $queue, $this->gatherWorkerOptions() + $connection, + $queue, + $this->gatherWorkerOptions() ); } @@ -179,19 +200,19 @@ protected function listenForEvents(): void return; } - $this->events->on(QueueEventManager::JOB_PROCESSING, function(QueueEvent $event) { + $this->events->on(QueueEventManager::JOB_PROCESSING, function (QueueEvent $event) { $this->writeOutput($event->job, 'starting'); }); - - $this->events->on(QueueEventManager::JOB_PROCESSED, function(QueueEvent $event) { + + $this->events->on(QueueEventManager::JOB_PROCESSED, function (QueueEvent $event) { $this->writeOutput($event->job, 'success'); }); - $this->events->on(QueueEventManager::JOB_RELEASED_AFTER_EXCEPTION, function(QueueEvent $event) { + $this->events->on(QueueEventManager::JOB_RELEASED_AFTER_EXCEPTION, function (QueueEvent $event) { $this->writeOutput($event->job, 'released_after_exception'); }); - $this->events->on(QueueEventManager::JOB_FAILED, function(QueueEvent $event) { + $this->events->on(QueueEventManager::JOB_FAILED, function (QueueEvent $event) { $this->writeOutput($event->job, 'failed', $event->exception); $this->logFailedJob($event); @@ -221,16 +242,18 @@ protected function writeOutputForCli(Job $job, string $status): void { $isVerbose = $this->option('verbose'); - $first = sprintf('%s %s %s', + $first = sprintf( + '%s %s %s', $this->color->comment($this->now()->format('Y-m-d H:i:s')), $job->resolveName(), - ! $isVerbose ? '' : sprintf('%s %s', + ! $isVerbose ? '' : sprintf( + '%s %s', $this->color->comment($job->getJobId()), - $this->color->info($job->getConnectionName() . ' ' . $job->getQueue()) - ) + $this->color->info($job->getConnectionName() . ' ' . $job->getQueue()), + ), ); - if ($status == 'starting') { + if ($status === 'starting') { $this->latestStartedAt = microtime(true); $second = $this->color->warn('RUNNING', ['bold' => 1]); @@ -238,21 +261,23 @@ protected function writeOutputForCli(Job $job, string $status): void $runTime = (microtime(true) - $this->latestStartedAt) * 1000; $runTime = (float) number_format($runTime, 2, '.', ''); - $memory = $isVerbose ? round(memory_get_usage(true) / 1024 / 1024, 1).'MB' : ''; + $memory = $isVerbose ? round(memory_get_usage(true) / 1024 / 1024, 1) . 'MB' : ''; - $second = $this->color->comment("{$runTime} ms".($memory ? " {$memory}" : '') . " "); + $second = $this->color->comment("{$runTime} ms" . ($memory ? " {$memory}" : '') . ' '); $second .= match ($status) { - 'success' => $this->color->ok('DONE', ['bold' => 1]), + 'success' => $this->color->ok('DONE', ['bold' => 1]), 'released_after_exception' => $this->color->warn('FAIL', ['bold' => 1]), - default => $this->color->error('FAIL', ['bold' => 1]), + default => $this->color->error('FAIL', ['bold' => 1]), }; } - + $this->justify($first, $second); } /** * Affiche l'état du worker au format JSON. + * + * @param mixed $status */ protected function writeOutputAsJson(Job $job, $status, ?Throwable $exception = null): void { @@ -265,10 +290,10 @@ protected function writeOutputAsJson(Job $job, $status, ?Throwable $exception = 'job' => $job->resolveName(), 'status' => $status, 'result' => match (true) { - $job->isDeleted() => 'deleted', + $job->isDeleted() => 'deleted', $job->isReleased() => 'released', - $job->hasFailed() => 'failed', - default => '', + $job->hasFailed() => 'failed', + default => '', }, 'attempts' => $job->attempts(), 'exception' => $exception ? $exception::class : '', @@ -308,7 +333,7 @@ protected function logFailedJob(QueueEvent $event): void $event->connection, $event->job->getQueue(), $event->job->getRawBody(), - $event->exception + $event->exception, ); } @@ -318,7 +343,8 @@ protected function logFailedJob(QueueEvent $event): void protected function getQueue(string $connection): string { return $this->option('queue') ?: config( - "queue.connections.{$connection}.queue", 'default' + "queue.connections.{$connection}.queue", + 'default', ); } @@ -327,8 +353,8 @@ protected function getQueue(string $connection): string */ protected function downForMaintenance(): false { - return $this->option('force') - ? false + return $this->option('force') + ? false : config('app.maintenance.enable', false); // $this->laravel->isDownForMaintenance(); } @@ -353,7 +379,7 @@ public static function flushState(): void */ protected function isSilent(): bool { - return $this->suppress || !is_cli(); + return $this->suppress || ! is_cli(); } /** @@ -368,10 +394,10 @@ protected static function terminalHasSttyAvailable(): bool } // Pas de vérification si shell_exec est désactivé - if (!\function_exists('shell_exec')) { + if (! \function_exists('shell_exec')) { return false; } - return self::$stty = (bool) @shell_exec('stty 2> '.('\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null')); + return self::$stty = (bool) @shell_exec('stty 2> ' . ('\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null')); } } diff --git a/src/Compatibility/SignalTrait.php b/src/Compatibility/SignalTrait.php index a9b6f01..cfdf72a 100644 --- a/src/Compatibility/SignalTrait.php +++ b/src/Compatibility/SignalTrait.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Compatibility; use Closure; diff --git a/src/Config/Services.php b/src/Config/Services.php index d0c7d8e..9551cf1 100644 --- a/src/Config/Services.php +++ b/src/Config/Services.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Config; use BlitzPHP\Container\Services as BaseServices; @@ -32,7 +41,7 @@ public static function queue(array $config = [], bool $shared = true): Manager return static::$instances[Manager::class] = new Manager( static::container(), - Config::fromArray($config) + Config::fromArray($config), ); } @@ -45,9 +54,7 @@ public static function worker(bool $shared = true): Worker return static::$instances[Worker::class]; } - $isDownForMaintenance = function () { - return (bool) static::config()->get('app.maintenance.enable', false); - }; + $isDownForMaintenance = fn () => (bool) static::config()->get('app.maintenance.enable', false); $resetScope = function () { $logger = static::logger(); @@ -91,24 +98,23 @@ public static function queueFailer(array $config = [], bool $shared = true): Fai $config = $config === [] ? static::config()->get('queue.failed', []) : $config; $driver = $config['driver'] ?? 'null'; - - return static::$instances[FailedJobProviderInterface::class] =match ($driver) { + + return static::$instances[FailedJobProviderInterface::class] = match ($driver) { 'database' => new DatabaseFailedJobProvider( static::singleton(ConnectionResolverInterface::class), $config['database'] ?? 'default', - $config['table'] ?? 'queue_failed_jobs' + $config['table'] ?? 'queue_failed_jobs', ), 'database-uuids' => new DatabaseUuidFailedJobProvider( static::singleton(ConnectionResolverInterface::class), $config['database'] ?? 'default', - $config['table'] ?? 'queue_failed_jobs' + $config['table'] ?? 'queue_failed_jobs', ), 'file' => new FileFailedJobProvider( $config['path'] ?? storage_path('logs/failed_jobs.json'), - $config['limit'] ?? 100 + $config['limit'] ?? 100, ), - default => new NullFailedJobProvider() + default => new NullFailedJobProvider(), }; } - } diff --git a/src/Config/queue.php b/src/Config/queue.php index 820fb1b..d32e57d 100644 --- a/src/Config/queue.php +++ b/src/Config/queue.php @@ -1,5 +1,16 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +use BlitzPHP\Queue\Drivers\DatabaseDriver; + /** * Configuration du composant de files d'attente (queue). * @@ -59,22 +70,22 @@ */ 'table' => env('queue.database.table', 'queue_jobs'), - /** - * Nom de la file logique par défaut pour cette connexion - * (colonne `queue` en base). Utilisé si `queue:work` n'en précise pas. - */ + /** + * Nom de la file logique par défaut pour cette connexion + * (colonne `queue` en base). Utilisé si `queue:work` n'en précise pas. + */ // 'queue' => 'default', - /** - * Délai en secondes au-delà duquel un job réservé est considéré - * comme expiré et peut être repris par un autre worker. - */ + /** + * Délai en secondes au-delà duquel un job réservé est considéré + * comme expiré et peut être repris par un autre worker. + */ // 'retry_after' => 60, - /** - * Si `true`, n'envoie le job qu'après le commit des transactions - * de base de données en cours. - */ + /** + * Si `true`, n'envoie le job qu'après le commit des transactions + * de base de données en cours. + */ // 'after_commit' => false, ], @@ -203,7 +214,7 @@ /** * Pilote SQL : table `queue_jobs` (ou celle configurée). */ - 'database' => \BlitzPHP\Queue\Drivers\DatabaseDriver::class, + 'database' => DatabaseDriver::class, // 'redis' => \BlitzPHP\Queue\Drivers\Redis::class, // 'predis' => \BlitzPHP\Queue\Drivers\Predis::class, // 'rabbitmq' => \BlitzPHP\Queue\Drivers\RabbitMQ::class, diff --git a/src/DTO/Config.php b/src/DTO/Config.php index fbb3115..4412748 100644 --- a/src/DTO/Config.php +++ b/src/DTO/Config.php @@ -1,4 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\DTO; use BlitzPHP\Queue\Drivers\ConnectorInterface; @@ -13,22 +23,22 @@ class Config { /** - * @param string $default Le nom de la connexion par défaut - * @param array> $connections Les configurations des connexions - * @param array> $drivers Les drivers disponibles - * @param bool $keep_failed_jobs Garder les jobs échoués - * @param array{driver: string, database: string, table: string} $failed Configuration des jobs échoués - * @param array{database: string, table: string} $batching Configuration du batching - * @param array $raw Données brutes supplémentaires + * @param string $default Le nom de la connexion par défaut + * @param array> $connections Les configurations des connexions + * @param array> $drivers Les drivers disponibles + * @param bool $keep_failed_jobs Garder les jobs échoués + * @param array{driver: string, database: string, table: string} $failed Configuration des jobs échoués + * @param array{database: string, table: string} $batching Configuration du batching + * @param array $raw Données brutes supplémentaires */ public function __construct( public string $default, - public array $connections = [], - public array $drivers = [], - public bool $keep_failed_jobs = true, - public array $failed = [], - public array $batching = [], - private array $raw = [], + public array $connections = [], + public array $drivers = [], + public bool $keep_failed_jobs = true, + public array $failed = [], + public array $batching = [], + private array $raw = [], ) { } @@ -70,7 +80,7 @@ public function toArray(): array 'failed' => $this->failed, 'batching' => $this->batching, ], - $this->raw + $this->raw, ); } @@ -86,10 +96,10 @@ public function toArray(): array public function connection(?string $name): array { if ($name === null || $name === 'null') { - return ['driver' => 'null']; + return ['driver' => 'null']; } - if (!isset($this->connections[$name])) { + if (! isset($this->connections[$name])) { throw new InvalidArgumentException("The [{$name}] queue connection has not been configured."); } @@ -105,7 +115,7 @@ public function connection(?string $name): array * * @throws InvalidArgumentException Si le pilote n'est pas enregistré ou n'implémente pas le contrat. */ - public function driver(string $name): string + public function driver(string $name): string { $driver = $this->drivers[$name] ?? null; @@ -127,7 +137,7 @@ public function driver(string $name): string */ public function setDefaultDriver(string $name): void { - $this->default = $name; + $this->default = $name; config()->set('queue.default', $name); } diff --git a/src/DTO/WorkerOptions.php b/src/DTO/WorkerOptions.php index 124464f..f7303b8 100644 --- a/src/DTO/WorkerOptions.php +++ b/src/DTO/WorkerOptions.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\DTO; /** @@ -13,19 +22,19 @@ class WorkerOptions /** * Crée une instance d'options du worker. * - * @param string $name Nom du worker (utilisé pour les callbacks de pop personnalisés). - * @param int|int[] $backoff Secondes d'attente avant de relancer un job ayant levé une exception non gérée. - * @param int $memory Mémoire maximale autorisée (Mo) avant arrêt du worker. - * @param int $timeout Durée maximale d'exécution d'un job enfant (secondes). - * @param int $sleep Secondes d'attente entre deux sondages lorsque la file est vide. - * @param int $maxTries Nombre maximal de tentatives par job. - * @param bool $force Si `true`, le worker tourne même en mode maintenance. - * @param bool $stopWhenEmpty Si `true`, le worker s'arrête dès que la file est vide. - * @param int $maxJobs Nombre maximal de jobs à traiter (0 = illimité). - * @param int $maxTime Durée de vie maximale du worker en secondes (0 = illimitée). - * @param int $rest Secondes de pause entre deux jobs traités avec succès. + * @param string $name Nom du worker (utilisé pour les callbacks de pop personnalisés). + * @param int|list $backoff Secondes d'attente avant de relancer un job ayant levé une exception non gérée. + * @param int $memory Mémoire maximale autorisée (Mo) avant arrêt du worker. + * @param int $timeout Durée maximale d'exécution d'un job enfant (secondes). + * @param int $sleep Secondes d'attente entre deux sondages lorsque la file est vide. + * @param int $maxTries Nombre maximal de tentatives par job. + * @param bool $force Si `true`, le worker tourne même en mode maintenance. + * @param bool $stopWhenEmpty Si `true`, le worker s'arrête dès que la file est vide. + * @param int $maxJobs Nombre maximal de jobs à traiter (0 = illimité). + * @param int $maxTime Durée de vie maximale du worker en secondes (0 = illimitée). + * @param int $rest Secondes de pause entre deux jobs traités avec succès. */ - public function __construct( + public function __construct( public string $name = 'default', public array|int $backoff = 0, public int $memory = 128, @@ -38,5 +47,5 @@ public function __construct( public int $maxTime = 0, public $rest = 0, ) { - } + } } diff --git a/src/Database/Migrations/2026-08-26-061438_CreateQueueTables.php b/src/Database/Migrations/2026-08-26-061438_CreateQueueTables.php index a3fbcdd..1a26031 100644 --- a/src/Database/Migrations/2026-08-26-061438_CreateQueueTables.php +++ b/src/Database/Migrations/2026-08-26-061438_CreateQueueTables.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Database\Migrations; use BlitzPHP\Database\Migration\Migration; @@ -15,7 +24,7 @@ class CreateQueueTables extends Migration */ public function up() { - $this->create(config('queue.connections.database.table', 'queue_jobs'), function(Structure $table) { + $this->create(config('queue.connections.database.table', 'queue_jobs'), function (Structure $table) { $table->bigIncrements('id'); $table->string('queue')->index(); $table->longText('payload'); @@ -27,7 +36,7 @@ public function up() return $table; }); - $this->create(config('queue.failed.table', 'queue_failed_jobs'), function(Structure $table) { + $this->create(config('queue.failed.table', 'queue_failed_jobs'), function (Structure $table) { $table->id(); $table->string('uuid')->unique(); $table->text('connection'); diff --git a/src/Drivers/ConnectorInterface.php b/src/Drivers/ConnectorInterface.php index ff01f87..01d62ce 100644 --- a/src/Drivers/ConnectorInterface.php +++ b/src/Drivers/ConnectorInterface.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Drivers; use BlitzPHP\Contracts\Container\ContainerInterface; diff --git a/src/Drivers/DatabaseDriver.php b/src/Drivers/DatabaseDriver.php index 8070be0..c79172c 100644 --- a/src/Drivers/DatabaseDriver.php +++ b/src/Drivers/DatabaseDriver.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Drivers; use BlitzPHP\Contracts\Container\ContainerInterface; @@ -9,16 +18,16 @@ use BlitzPHP\Contracts\Queue\Queue as QueueContract; use BlitzPHP\Exceptions\CriticalError; use BlitzPHP\Queue\Events\QueueEventManager; -use BlitzPHP\Queue\Models\JobModel; -use BlitzPHP\Queue\Queue; use BlitzPHP\Queue\Jobs\DatabaseJob; use BlitzPHP\Queue\Jobs\DatabaseJobRecord; use BlitzPHP\Queue\Jobs\InspectedJob; +use BlitzPHP\Queue\Models\JobModel; +use BlitzPHP\Queue\Queue; use BlitzPHP\Utilities\Iterable\Collection; use BlitzPHP\Utilities\String\Stringable; use BlitzPHP\Utilities\String\Text; -use DateTimeInterface; use DateInterval; +use DateTimeInterface; use Throwable; /** @@ -29,21 +38,21 @@ class DatabaseDriver extends Queue implements QueueContract, ConnectorInterface /** * Type de verrou mis en cache pour le prélèvement des jobs. * - * @var string|bool|null + * @var bool|string|null */ - protected $lockForPopping = null; + protected $lockForPopping; /** * Crée une instance de file d'attente base de données. - * + * * @param string $default Nom de la file par défaut. */ public function __construct(protected JobModel $model, protected string $default = 'default', bool $dispatchAfterCommit = false) - { + { $this->dispatchAfterCommit = $dispatchAfterCommit; } - /** + /** * Établit une connexion de file d'attente. * * @param array $config Configuration de la connexion. @@ -51,29 +60,29 @@ public function __construct(protected JobModel $model, protected string $default public static function connect(ContainerInterface $container, array $config): QueueContract { try { - $connection = service('database', $config['connection'] ?? null, $config['shared'] ?? true); - - $queue = new self( - new JobModel( - $config, - $container->get(ConnectionResolverInterface::class), - $connection, - ), + $connection = service('database', $config['connection'] ?? null, $config['shared'] ?? true); + + $queue = new self( + new JobModel( + $config, + $container->get(ConnectionResolverInterface::class), + $connection, + ), $config['queue'], - $config['after_commit'] ?? false - ); + $config['after_commit'] ?? false, + ); - $container->get(QueueEventManager::class)->handlerConnectionEstablished( + $container->get(QueueEventManager::class)->handlerConnectionEstablished( connection: $queue->getConnectionName(), config: $config, ); - return $queue; + return $queue; } catch (Throwable $e) { - $container->get(QueueEventManager::class)->handlerConnectionFailed( + $container->get(QueueEventManager::class)->handlerConnectionFailed( connection: 'default', config: $config, - exception: $e, + exception: $e, ); throw new CriticalError('Queue: Database connection failed. ' . $e->getMessage()); @@ -141,7 +150,7 @@ public function delayedJobs(?string $queue = null): Collection */ public function reservedJobs(?string $queue = null): Collection { - return collect($this->model->reservedJobs($this->getQueue($queue))) + return collect($this->model->reservedJobs($this->getQueue($queue))) ->map(fn ($record) => InspectedJob::fromPayload($record->payload, $record->attempts)); } @@ -156,7 +165,7 @@ public function creationTimeOfOldestPendingJob(?string $queue = null): ?int /** * Envoie un nouveau job dans la file. */ - public function push(string|object $job, mixed $data = '', ?string $queue = null): mixed + public function push(object|string $job, mixed $data = '', ?string $queue = null): mixed { return $this->enqueueUsing( $job, @@ -178,7 +187,7 @@ public function pushRaw(string $payload, ?string $queue = null, array $options = /** * Envoie un job dans la file après n secondes. */ - public function later(DateTimeInterface|DateInterval|int $delay, string|object $job, mixed $data = '', ?string $queue = null): mixed + public function later(DateInterval|DateTimeInterface|int $delay, object|string $job, mixed $data = '', ?string $queue = null): mixed { return $this->enqueueUsing( $job, @@ -191,7 +200,7 @@ public function later(DateTimeInterface|DateInterval|int $delay, string|object $ /** * Envoie un tableau de jobs dans la file. - */ + */ public function bulk(array $jobs, mixed $data = '', ?string $queue = null): mixed { $queue = $this->getQueue($queue); @@ -199,16 +208,14 @@ public function bulk(array $jobs, mixed $data = '', ?string $queue = null): mixe $now = $this->availableAt(); $this->model->insert((new Collection((array) $jobs))->map( - function ($job) use ($queue, $data, $now) { - return $this->buildDatabaseRecord( - $queue, - $this->createPayload($job, $this->getQueue($queue), $data), - isset($job->delay) ? $this->availableAt($job->delay) : $now, - ); - } + fn ($job) => $this->buildDatabaseRecord( + $queue, + $this->createPayload($job, $this->getQueue($queue), $data), + isset($job->delay) ? $this->availableAt($job->delay) : $now, + ), )->all()); - return null; + return null; } /** @@ -222,13 +229,13 @@ public function release(string $queue, DatabaseJobRecord $job, int $delay): mixe /** * Insère un payload brut en base avec un délai de n secondes. */ - protected function pushToDatabase(?string $queue, string $payload, DateTimeInterface|DateInterval|int $delay = 0, int $attempts = 0): mixed + protected function pushToDatabase(?string $queue, string $payload, DateInterval|DateTimeInterface|int $delay = 0, int $attempts = 0): mixed { return $this->model->pushToDatabase($this->buildDatabaseRecord( $this->getQueue($queue), $payload, $this->availableAt($delay), - $attempts + $attempts, )); } @@ -269,7 +276,11 @@ public function pop(?string $queue = null): ?Job if ($jobRecord) { try { (new DatabaseJob( - $this->container, $this, $jobRecord, $this->connectionName, $queue + $this->container, + $this, + $jobRecord, + $this->connectionName, + $queue, ))->fail($e); } catch (Throwable) { // Ignore et relance l'exception d'origine. @@ -293,7 +304,7 @@ protected function getNextAvailableJob(?string $queue): ?DatabaseJobRecord /** * Retourne le verrou SQL nécessaire pour prélever le prochain job. * - * @return string|bool + * @return bool|string */ protected function getLockForPopping() { @@ -301,21 +312,21 @@ protected function getLockForPopping() return $this->lockForPopping; } - $databaseEngine= $this->model->db()->getPlatform(); - $databaseVersion= $this->model->db()->getVersion(); + $databaseEngine = $this->model->db()->getPlatform(); + $databaseVersion = $this->model->db()->getVersion(); if ((new Stringable($databaseVersion))->contains('MariaDB')) { - $databaseEngine = 'mariadb'; + $databaseEngine = 'mariadb'; $databaseVersion = Text::before(Text::after($databaseVersion, '5.5.5-'), '-'); } elseif ((new Stringable($databaseVersion))->contains(['vitess', 'PlanetScale'])) { - $databaseEngine = 'vitess'; + $databaseEngine = 'vitess'; $databaseVersion = Text::before($databaseVersion, '-'); } - if (($databaseEngine === 'mysql' && version_compare($databaseVersion, '8.0.1', '>=')) || - ($databaseEngine === 'mariadb' && version_compare($databaseVersion, '10.6.0', '>=')) || - ($databaseEngine === 'pgsql' && version_compare($databaseVersion, '9.5', '>=')) || - ($databaseEngine === 'vitess' && version_compare($databaseVersion, '19.0', '>=')) + if (($databaseEngine === 'mysql' && version_compare($databaseVersion, '8.0.1', '>=')) + || ($databaseEngine === 'mariadb' && version_compare($databaseVersion, '10.6.0', '>=')) + || ($databaseEngine === 'pgsql' && version_compare($databaseVersion, '9.5', '>=')) + || ($databaseEngine === 'vitess' && version_compare($databaseVersion, '19.0', '>=')) ) { return $this->lockForPopping = 'FOR UPDATE SKIP LOCKED'; } @@ -348,7 +359,7 @@ protected function markJobAsReserved(DatabaseJobRecord $job): DatabaseJobRecord { $this->model->where('id', $job->id)->update([ 'reserved_at' => $job->touch(), - 'attempts' => $job->increment(), + 'attempts' => $job->increment(), ]); return $job; @@ -370,9 +381,9 @@ public function deleteReserved(string $queue, string $id): void public function deleteAndRelease(string $queue, DatabaseJob $job, int $delay): void { $this->model->transaction(function () use ($queue, $job, $delay) { - $where = ['id' => $job->getJobId()]; + $where = ['id' => $job->getJobId()]; - if ($this->model/*->lockForUpdate()*/->where($where)->first()) { + if ($this->model/* ->lockForUpdate() */ ->where($where)->first()) { $this->model->where($where)->delete(); } diff --git a/src/Drivers/FailoverDriver.php b/src/Drivers/FailoverDriver.php index b9a9ef2..3b5048f 100644 --- a/src/Drivers/FailoverDriver.php +++ b/src/Drivers/FailoverDriver.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Drivers; use BlitzPHP\Contracts\Container\ContainerInterface; @@ -33,7 +42,7 @@ public function __construct(public Manager $manager, public QueueEventManager $e { } - /** + /** * Établit une connexion de file d'attente. */ public static function connect(ContainerInterface $container, array $config): QueueContract @@ -130,7 +139,7 @@ public function pushRaw(string $payload, ?string $queue = null, array $options = /** * Envoie un job dans la file après n secondes. */ - public function later(DateTimeInterface|DateInterval|int $delay, string|object $job, mixed $data = '', ?string $queue = null): mixed + public function later(DateInterval|DateTimeInterface|int $delay, object|string $job, mixed $data = '', ?string $queue = null): mixed { return $this->attemptOnAllConnections(__FUNCTION__, func_get_args(), $job); } @@ -146,7 +155,6 @@ public function pop(?string $queue = null): ?Job /** * Tente la méthode donnée sur toutes les connexions, dans l'ordre. * - * * @throws Throwable */ protected function attemptOnAllConnections(string $method, array $arguments, ?string $job = null): mixed @@ -162,7 +170,7 @@ protected function attemptOnAllConnections(string $method, array $arguments, ?st $failedQueues[] = $connection; - if ($job !== null && ! in_array($connection, $this->failingQueues)) { + if ($job !== null && ! in_array($connection, $this->failingQueues, true)) { $this->events->queueFailedOver($connection, $job, $e); } } diff --git a/src/Drivers/NullDriver.php b/src/Drivers/NullDriver.php index c95557e..872a17d 100644 --- a/src/Drivers/NullDriver.php +++ b/src/Drivers/NullDriver.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Drivers; use BlitzPHP\Contracts\Container\ContainerInterface; @@ -20,7 +29,7 @@ class NullDriver extends Queue implements QueueContract, ConnectorInterface */ public static function connect(ContainerInterface $container, array $config): QueueContract { - return new self; + return new self(); } /** @@ -60,7 +69,7 @@ public function reservedSize(?string $queue = null): int */ public function pendingJobs(?string $queue = null): Collection { - return new Collection; + return new Collection(); } /** @@ -68,7 +77,7 @@ public function pendingJobs(?string $queue = null): Collection */ public function delayedJobs(?string $queue = null): Collection { - return new Collection; + return new Collection(); } /** @@ -76,7 +85,7 @@ public function delayedJobs(?string $queue = null): Collection */ public function reservedJobs(?string $queue = null): Collection { - return new Collection; + return new Collection(); } /** @@ -90,7 +99,7 @@ public function creationTimeOfOldestPendingJob(?string $queue = null): ?int /** * Envoie un nouveau job dans la file. */ - public function push(string|object $job, mixed $data = '', ?string $queue = null): mixed + public function push(object|string $job, mixed $data = '', ?string $queue = null): mixed { return null; } @@ -106,7 +115,7 @@ public function pushRaw(string $payload, ?string $queue = null, array $options = /** * Envoie un job dans la file après n secondes. */ - public function later(DateTimeInterface|DateInterval|int $delay, string|object $job, mixed $data = '', ?string $queue = null): mixed + public function later(DateInterval|DateTimeInterface|int $delay, object|string $job, mixed $data = '', ?string $queue = null): mixed { return null; } diff --git a/src/Drivers/SyncDriver.php b/src/Drivers/SyncDriver.php index 20e8a2b..f2a807f 100644 --- a/src/Drivers/SyncDriver.php +++ b/src/Drivers/SyncDriver.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Drivers; use BlitzPHP\Contracts\Queue\Job; @@ -33,7 +42,6 @@ public static function connect(ContainerInterface $container, array $config): Qu return new self($config['after_commit'] ?? null); } - /** * Retourne le nombre total de jobs dans la file. */ @@ -71,7 +79,7 @@ public function reservedSize(?string $queue = null): int */ public function pendingJobs(?string $queue = null): Collection { - return new Collection; + return new Collection(); } /** @@ -79,7 +87,7 @@ public function pendingJobs(?string $queue = null): Collection */ public function delayedJobs(?string $queue = null): Collection { - return new Collection; + return new Collection(); } /** @@ -87,7 +95,7 @@ public function delayedJobs(?string $queue = null): Collection */ public function reservedJobs(?string $queue = null): Collection { - return new Collection; + return new Collection(); } /** @@ -103,7 +111,7 @@ public function creationTimeOfOldestPendingJob(?string $queue = null): ?int * * @throws Throwable */ - public function push(string|object $job, mixed $data = '', ?string $queue = null): mixed + public function push(object|string $job, mixed $data = '', ?string $queue = null): mixed { $job = $job instanceof Job ? $job->getJobId() : (string) $job; @@ -218,7 +226,7 @@ public function pushRaw(string $payload, ?string $queue = null, array $options = /** * Envoie un job dans la file après n secondes. */ - public function later(DateTimeInterface|DateInterval|int $delay, string|object $job, mixed $data = '', ?string $queue = null): mixed + public function later(DateInterval|DateTimeInterface|int $delay, object|string $job, mixed $data = '', ?string $queue = null): mixed { return $this->push($job, $data, $queue); } diff --git a/src/Enums/WorkerStopReason.php b/src/Enums/WorkerStopReason.php index ef5613b..7ce2439 100644 --- a/src/Enums/WorkerStopReason.php +++ b/src/Enums/WorkerStopReason.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Enums; /** @@ -7,20 +16,36 @@ */ enum WorkerStopReason: string { - /** Interruption par signal (SIGINT, SIGTERM, etc.). */ + /** + * Interruption par signal (SIGINT, SIGTERM, etc.). + */ case Interrupted = 'interrupted'; - /** Perte de connexion (base de données, courtier, etc.). */ + /** + * Perte de connexion (base de données, courtier, etc.). + */ case LostConnection = 'lost_connection'; - /** Nombre maximal de jobs atteint. */ + /** + * Nombre maximal de jobs atteint. + */ case MaxJobsExceeded = 'max_jobs'; - /** Limite mémoire dépassée. */ + /** + * Limite mémoire dépassée. + */ case MaxMemoryExceeded = 'memory'; - /** Durée de vie maximale du worker atteinte. */ + /** + * Durée de vie maximale du worker atteinte. + */ case MaxTimeExceeded = 'max_time'; - /** File vide et option `stopWhenEmpty` active. */ + /** + * File vide et option `stopWhenEmpty` active. + */ case QueueEmpty = 'empty'; - /** Signal de redémarrage reçu via le cache. */ + /** + * Signal de redémarrage reçu via le cache. + */ case ReceivedRestartSignal = 'restart_signal'; - /** Dépassement du délai d'exécution d'un job. */ + /** + * Dépassement du délai d'exécution d'un job. + */ case TimedOut = 'timed_out'; } diff --git a/src/Events/QueueEvent.php b/src/Events/QueueEvent.php index ad129da..5f1f044 100644 --- a/src/Events/QueueEvent.php +++ b/src/Events/QueueEvent.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Events; use BlitzPHP\Contracts\Queue\Job; @@ -11,10 +20,10 @@ /** * Événement du cycle de vie de la file d'attente (job, worker, connexion, opération). * - * @property mixed $job - * @property ?int $jobId * @property ?int $attempts * @property ?Throwable $exception + * @property mixed $job + * @property ?int $jobId */ class QueueEvent extends Event { @@ -37,7 +46,7 @@ public function __construct( private readonly array $metadata = [], ?Date $timestamp = null, ) { - parent::__construct($this->type); + parent::__construct($this->type); $this->timestamp = $timestamp ?? Date::now(); } @@ -176,9 +185,9 @@ public function getExceptionMessage(): ?string */ public function hasFailed(): bool { - $job = $this->job; + $job = $this->job; - return $job instanceof Job ? $job->hasFailed() : $this->getException() !== null; + return $job instanceof Job ? $job->hasFailed() : $this->getException() !== null; } /** @@ -187,11 +196,11 @@ public function hasFailed(): bool public function toArray(): array { return [ - 'type' => $this->type, - 'connection' => $this->connection, - 'queue' => $this->queue, - 'metadata' => $this->metadata, - 'timestamp' => $this->timestamp->toDateTimeString(), + 'type' => $this->type, + 'connection' => $this->connection, + 'queue' => $this->queue, + 'metadata' => $this->metadata, + 'timestamp' => $this->timestamp->toDateTimeString(), ]; } diff --git a/src/Events/QueueEventManager.php b/src/Events/QueueEventManager.php index e407cf7..422c550 100644 --- a/src/Events/QueueEventManager.php +++ b/src/Events/QueueEventManager.php @@ -1,12 +1,20 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Events; use BlitzPHP\Contracts\Event\EventManagerInterface; use BlitzPHP\Contracts\Queue\Job; -use BlitzPHP\Queue\Enums\WorkerStopReason; use BlitzPHP\Queue\DTO\WorkerOptions; -use Closure; +use BlitzPHP\Queue\Enums\WorkerStopReason; use DateInterval; use DateTimeInterface; use Throwable; @@ -16,9 +24,10 @@ */ class QueueEventManager { - /** + /** * Noms d'événements des opérations de file. */ + public const JOB_POPPING = 'queue.job.popping'; public const JOB_POPPED = 'queue.job.popped'; public const JOB_PUSHED = 'queue.job.pushed'; @@ -46,15 +55,15 @@ class QueueEventManager /** * @param EventManagerInterface $events Gestionnaire d'événements de l'application. */ - public function __construct(protected EventManagerInterface $events) - { - } + public function __construct(protected EventManagerInterface $events) + { + } - /** + /** * Émet l'événement de tentative de job. */ public function jobAttempted(string $connection, Job $job, ?Throwable $e = null): void - { + { $this->events->emit(new QueueEvent( type : self::JOB_ATTEMPTED, connection: $connection, @@ -63,11 +72,11 @@ public function jobAttempted(string $connection, Job $job, ?Throwable $e = null) )); } - /** + /** * Émet l'événement d'échec de job. */ public function jobFailed(string $connection, Job $job, ?Throwable $e): void - { + { $this->events->emit(new QueueEvent( type : self::JOB_FAILED, connection: $connection, @@ -76,11 +85,11 @@ public function jobFailed(string $connection, Job $job, ?Throwable $e): void )); } - /** + /** * Émet l'événement d'exception survenue sur un job. */ public function jobExceptionOccured(string $connection, Job $job, Throwable $e): void - { + { $this->events->emit(new QueueEvent( type : self::JOB_EXCEPTION_OCCURED, connection: $connection, @@ -89,36 +98,36 @@ public function jobExceptionOccured(string $connection, Job $job, Throwable $e): )); } - /** + /** * Émet l'événement de prélèvement imminent d'un job. */ public function jobPopping(string $connection, ?string $queue = null): void - { + { $this->events->emit(new QueueEvent( type : self::JOB_POPPING, connection: $connection, queue : $queue, - )); + )); } - /** + /** * Émet l'événement de job prélevé. */ public function jobPopped(string $connection, ?Job $job = null): void - { + { $this->events->emit(new QueueEvent( - type : self::JOB_POPPED, - connection: $connection, - queue : $job?->getQueue(), - metadata : compact('job') - )); + type : self::JOB_POPPED, + connection: $connection, + queue : $job?->getQueue(), + metadata : compact('job'), + )); } - /** + /** * Émet l'événement de traitement en cours. */ public function jobProcessing(string $connection, Job $job): void - { + { $this->events->emit(new QueueEvent( type : self::JOB_PROCESSING, connection: $connection, @@ -127,11 +136,11 @@ public function jobProcessing(string $connection, Job $job): void )); } - /** + /** * Émet l'événement de job traité. */ public function jobProcessed(string $connection, Job $job): void - { + { $this->events->emit(new QueueEvent( type : self::JOB_PROCESSED, connection: $connection, @@ -140,11 +149,11 @@ public function jobProcessed(string $connection, Job $job): void )); } - /** + /** * Émet l'événement « job enfilé ». */ - public function jobQueued(string $connection, ?string $queue, string|int|null $jobId, string|object $job, string $payload, DateTimeInterface|DateInterval|int|null $delay): void - { + public function jobQueued(string $connection, ?string $queue, int|string|null $jobId, object|string $job, string $payload, DateInterval|DateTimeInterface|int|null $delay): void + { $this->events->emit(new QueueEvent( type : self::JOB_QUEUED, connection: $connection, @@ -153,11 +162,11 @@ public function jobQueued(string $connection, ?string $queue, string|int|null $j )); } - /** + /** * Émet l'événement « job en cours d'enfilement ». */ - public function jobQueueing(string $connection, ?string $queue, string|object $job, string $payload, DateTimeInterface|DateInterval|int|null $delay): void - { + public function jobQueueing(string $connection, ?string $queue, object|string $job, string $payload, DateInterval|DateTimeInterface|int|null $delay): void + { $this->events->emit(new QueueEvent( type : self::JOB_QUEUEING, connection: $connection, @@ -166,11 +175,11 @@ public function jobQueueing(string $connection, ?string $queue, string|object $j )); } - /** + /** * Émet l'événement de relâchement après exception. */ public function jobReleasedAfterException(string $connection, Job $job, int $backoff): void - { + { $this->events->emit(new QueueEvent( type : self::JOB_RELEASED_AFTER_EXCEPTION, connection: $connection, @@ -179,11 +188,11 @@ public function jobReleasedAfterException(string $connection, Job $job, int $bac )); } - /** + /** * Émet l'événement de dépassement de délai. */ public function jobTimeout(string $connection, string $queue, Job $job, array $metadata = []): void - { + { $this->events->emit(new QueueEvent( type : self::JOB_TIMEOUT, connection: $connection, @@ -195,11 +204,11 @@ public function jobTimeout(string $connection, string $queue, Job $job, array $m )); } - /** + /** * Émet l'événement de file vidée. */ public function queueCleared(string $connection, ?string $queue = null): void - { + { $this->events->emit(new QueueEvent( type : self::QUEUE_CLEARED, connection: $connection, @@ -207,89 +216,89 @@ public function queueCleared(string $connection, ?string $queue = null): void )); } - /** + /** * Émet l'événement de file en pause. */ - public function queuePaused(string $connection, string $queue, DateTimeInterface|DateInterval|int|null $ttl = null): void - { + public function queuePaused(string $connection, string $queue, DateInterval|DateTimeInterface|int|null $ttl = null): void + { $this->events->emit(new QueueEvent( - type : self::QUEUE_PAUSED, - connection: $connection, - queue : $queue, - metadata : compact('ttl'), + type : self::QUEUE_PAUSED, + connection: $connection, + queue : $queue, + metadata : compact('ttl'), )); } - /** + /** * Émet l'événement de reprise de file. */ public function queueResumed(string $connection, string $queue): void - { - $this->events->emit(new QueueEvent( - type : self::QUEUE_RESUMED, - connection: $connection, - queue : $queue, + { + $this->events->emit(new QueueEvent( + type : self::QUEUE_RESUMED, + connection: $connection, + queue : $queue, )); } - /** + /** * Émet l'événement de bascule (failover) vers une autre connexion. */ public function queueFailedOver(string $connection, string $job, Throwable $e): void - { - $this->events->emit(new QueueEvent( + { + $this->events->emit(new QueueEvent( type : self::QUEUE_FAILED_OVER, connection: $connection, metadata : compact('job', 'e'), )); } - /** + /** * Émet l'événement de démarrage du worker. */ public function workerStarting(string $connection, string $queue, WorkerOptions $options): void - { - $this->events->emit(new QueueEvent( - type : self::WORKER_STARTING, - connection: $connection, - queue : $queue, - metadata : compact('options') - )); + { + $this->events->emit(new QueueEvent( + type : self::WORKER_STARTING, + connection: $connection, + queue : $queue, + metadata : compact('options'), + )); } /** * Émet l'événement d'arrêt du worker. */ - public function workerStopping(string $connection, int $status, ?WorkerOptions $options = null, ?WorkerStopReason $reason = null): void - { - $this->events->emit(new QueueEvent( - type : self::WORKER_STOPPING, - connection: $connection, - metadata : compact('status', 'options', 'reason') - )); + public function workerStopping(string $connection, int $status, ?WorkerOptions $options = null, ?WorkerStopReason $reason = null): void + { + $this->events->emit(new QueueEvent( + type : self::WORKER_STOPPING, + connection: $connection, + metadata : compact('status', 'options', 'reason'), + )); } - /** + /** * Émet l'événement de connexion de pilote établie. */ public function handlerConnectionEstablished(string $connection, array $config = []): void - { - $this->events->emit(new QueueEvent( - type : self::HANDLER_CONNECTION_ESTABLISHED, - connection: $connection, - metadata : compact('config') - )); + { + $this->events->emit(new QueueEvent( + type : self::HANDLER_CONNECTION_ESTABLISHED, + connection: $connection, + metadata : compact('config'), + )); } /** * Émet l'événement d'échec de connexion de pilote. */ public function handlerConnectionFailed(string $connection, Throwable $exception, array $config = []): void - { - $this->events->emit(new QueueEvent( - type : self::HANDLER_CONNECTION_FAILED, - connection: $connection, - metadata : compact('config', 'exception') - )); + { + $this->events->emit(new QueueEvent( + type : self::HANDLER_CONNECTION_FAILED, + connection: $connection, + metadata : compact('config', 'exception'), + )); } } diff --git a/src/Exceptions/InvalidPayloadException.php b/src/Exceptions/InvalidPayloadException.php index 56b5794..4ebc64f 100644 --- a/src/Exceptions/InvalidPayloadException.php +++ b/src/Exceptions/InvalidPayloadException.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Exceptions; use InvalidArgumentException; diff --git a/src/Exceptions/ManuallyFailedException.php b/src/Exceptions/ManuallyFailedException.php index 60c81e2..aa66cd3 100644 --- a/src/Exceptions/ManuallyFailedException.php +++ b/src/Exceptions/ManuallyFailedException.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Exceptions; use RuntimeException; @@ -9,5 +18,4 @@ */ class ManuallyFailedException extends RuntimeException { - // } diff --git a/src/Exceptions/MaxAttemptsExceededException.php b/src/Exceptions/MaxAttemptsExceededException.php index 0841bc8..6dcfc95 100644 --- a/src/Exceptions/MaxAttemptsExceededException.php +++ b/src/Exceptions/MaxAttemptsExceededException.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Exceptions; use BlitzPHP\Contracts\Queue\Job; @@ -20,7 +29,7 @@ class MaxAttemptsExceededException extends RuntimeException */ public static function forJob(Job $job): static { - return tap(new static($job->resolveName().' has been attempted too many times.'), function ($e) use ($job) { + return tap(new static($job->resolveName() . ' has been attempted too many times.'), function ($e) use ($job) { $e->job = $job; }); } diff --git a/src/Exceptions/TimeoutExceededException.php b/src/Exceptions/TimeoutExceededException.php index d78b513..c6b886e 100644 --- a/src/Exceptions/TimeoutExceededException.php +++ b/src/Exceptions/TimeoutExceededException.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Exceptions; use BlitzPHP\Contracts\Queue\Job; @@ -14,7 +23,7 @@ class TimeoutExceededException extends MaxAttemptsExceededException */ public static function forJob(Job $job): static { - return tap(new static($job->resolveName().' has timed out.'), function ($e) use ($job) { + return tap(new static($job->resolveName() . ' has timed out.'), function ($e) use ($job) { $e->job = $job; }); } diff --git a/src/Failed/CountableFailedJobProvider.php b/src/Failed/CountableFailedJobProvider.php index 9eb82e8..80f3200 100644 --- a/src/Failed/CountableFailedJobProvider.php +++ b/src/Failed/CountableFailedJobProvider.php @@ -1,4 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Failed; /** @@ -10,4 +20,4 @@ interface CountableFailedJobProvider * Compte les jobs échoués. */ public function count(?string $connection = null, ?string $queue = null): int; -} \ No newline at end of file +} diff --git a/src/Failed/DatabaseFailedJobProvider.php b/src/Failed/DatabaseFailedJobProvider.php index cacf5ca..7ca2032 100644 --- a/src/Failed/DatabaseFailedJobProvider.php +++ b/src/Failed/DatabaseFailedJobProvider.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Failed; use BlitzPHP\Contracts\Database\ConnectionResolverInterface; @@ -16,9 +25,9 @@ class DatabaseFailedJobProvider implements CountableFailedJobProvider, FailedJob /** * Crée un fournisseur de jobs échoués en base de données. * - * @param ConnectionResolverInterface $resolver Résolveur de connexions base de données. - * @param string $database Nom de la connexion base de données. - * @param string $table Nom de la table. + * @param ConnectionResolverInterface $resolver Résolveur de connexions base de données. + * @param string $database Nom de la connexion base de données. + * @param string $table Nom de la table. */ public function __construct(protected ConnectionResolverInterface $resolver, protected string $database, protected string $table) { @@ -34,7 +43,11 @@ public function log(string $connection, string $queue, string $payload, Throwabl $exception = (string) mb_convert_encoding($exception, 'UTF-8'); return $this->insertGetId(compact( - 'connection', 'queue', 'payload', 'exception', 'failed_at' + 'connection', + 'queue', + 'payload', + 'exception', + 'failed_at', )); } @@ -44,7 +57,7 @@ public function log(string $connection, string $queue, string $payload, Throwabl public function ids(?string $queue = null): array { return $this->getTable() - ->when(! is_null($queue), fn ($query) => $query->where('queue', $queue)) + ->when(null !== $queue, fn ($query) => $query->where('queue', $queue)) ->orderBy('id', 'desc') ->values('id'); } @@ -60,7 +73,7 @@ public function all(): array /** * Retourne un job échoué. */ - public function find(string|int $id): ?object + public function find(int|string $id): ?object { return $this->getTable()->where($this->whereId($id))->first(); } @@ -68,7 +81,7 @@ public function find(string|int $id): ?object /** * Supprime un job échoué du stockage. */ - public function forget(string|int $id): bool + public function forget(int|string $id): bool { return $this->getTable()->where($this->whereId($id))->delete() > 0; } @@ -114,7 +127,7 @@ public function count(?string $connection = null, ?string $queue = null): int /** * Retourne un constructeur de requêtes pour la table. - * + * * @return BaseBuilder */ public function getTable() @@ -125,9 +138,9 @@ public function getTable() /** * Clause WHERE selon que l'identifiant est un UUID (32 caractères) ou un entier. * - * @return array + * @return array */ - private function whereId(string|int $id): array + private function whereId(int|string $id): array { return [is_string($id) && strlen($id) === 32 ? 'uuid' : 'id' => $id]; } diff --git a/src/Failed/DatabaseUuidFailedJobProvider.php b/src/Failed/DatabaseUuidFailedJobProvider.php index b56e438..15b8427 100644 --- a/src/Failed/DatabaseUuidFailedJobProvider.php +++ b/src/Failed/DatabaseUuidFailedJobProvider.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Failed; use BlitzPHP\Contracts\Database\ConnectionResolverInterface; @@ -16,9 +25,9 @@ class DatabaseUuidFailedJobProvider implements CountableFailedJobProvider, Faile /** * Crée un fournisseur de jobs échoués en base de données. * - * @param ConnectionResolverInterface $resolver Résolveur de connexions base de données. - * @param string $database Nom de la connexion base de données. - * @param string $table Nom de la table. + * @param ConnectionResolverInterface $resolver Résolveur de connexions base de données. + * @param string $database Nom de la connexion base de données. + * @param string $table Nom de la table. */ public function __construct(protected ConnectionResolverInterface $resolver, protected string $database, protected string $table) { @@ -47,7 +56,7 @@ public function log(string $connection, string $queue, string $payload, Throwabl public function ids(?string $queue = null): array { return $this->getTable() - ->when(! is_null($queue), fn ($query) => $query->where('queue', $queue)) + ->when(null !== $queue, fn ($query) => $query->where('queue', $queue)) ->orderBy('id', 'desc') ->values('uuid'); } @@ -70,7 +79,7 @@ public function all(): array /** * Retourne un job échoué. */ - public function find(string|int $id): ?object + public function find(int|string $id): ?object { if ($record = $this->getTable()->where('uuid', $id)->first()) { $record->id = $record->uuid; @@ -83,7 +92,7 @@ public function find(string|int $id): ?object /** * Supprime un job échoué du stockage. */ - public function forget(string|int $id): bool + public function forget(int|string $id): bool { return $this->getTable()->where('uuid', $id)->delete() > 0; } diff --git a/src/Failed/FailedJobProviderInterface.php b/src/Failed/FailedJobProviderInterface.php index f8d798c..8780bd4 100644 --- a/src/Failed/FailedJobProviderInterface.php +++ b/src/Failed/FailedJobProviderInterface.php @@ -1,4 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Failed; use Throwable; @@ -10,34 +20,32 @@ interface FailedJobProviderInterface { /** * Enregistre un job échoué dans le stockage. - * - * @return string|int|null */ - public function log(string $connection, string $queue, string $payload, Throwable $exception): string|int|null; + public function log(string $connection, string $queue, string $payload, Throwable $exception): int|string|null; /** * Retourne les identifiants de tous les jobs échoués. * - * @return array + * @return array */ public function ids(?string $queue = null): array; /** * Retourne la liste de tous les jobs échoués. * - * @return array + * @return list */ public function all(): array; /** * Retourne un job échoué. */ - public function find(string|int $id): ?object; + public function find(int|string $id): ?object; /** * Supprime un job échoué du stockage. */ - public function forget(string|int $id): bool; + public function forget(int|string $id): bool; /** * Vide le stockage des jobs échoués. diff --git a/src/Failed/FileFailedJobProvider.php b/src/Failed/FileFailedJobProvider.php index df1dc60..e7b9114 100644 --- a/src/Failed/FileFailedJobProvider.php +++ b/src/Failed/FileFailedJobProvider.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Failed; use BlitzPHP\Utilities\Date; @@ -16,9 +25,9 @@ class FileFailedJobProvider implements CountableFailedJobProvider, FailedJobProv /** * Crée un fournisseur de jobs échoués sur fichier. * - * @param string $path Chemin du fichier de stockage des jobs échoués. - * @param int $limit Nombre maximal de jobs échoués à conserver. - * @param Closure|null $lockProviderResolver Résolveur du fournisseur de verrous. + * @param string $path Chemin du fichier de stockage des jobs échoués. + * @param int $limit Nombre maximal de jobs échoués à conserver. + * @param Closure|null $lockProviderResolver Résolveur du fournisseur de verrous. */ public function __construct(protected string $path, protected int $limit = 100, protected ?Closure $lockProviderResolver = null) { @@ -58,7 +67,7 @@ public function log(string $connection, string $queue, string $payload, Throwabl public function ids(?string $queue = null): array { return (new Collection($this->all())) - ->when(! is_null($queue), fn ($collect) => $collect->where('queue', $queue)) + ->when(null !== $queue, fn ($collect) => $collect->where('queue', $queue)) ->pluck('id') ->all(); } @@ -83,7 +92,7 @@ public function find(int|string $id): ?object /** * Supprime un job échoué du stockage. */ - public function forget(string|int $id): bool + public function forget(int|string $id): bool { return $this->lock(function () use ($id) { $this->write($pruned = (new Collection($jobs = $this->read())) @@ -111,10 +120,11 @@ public function prune(DateTimeInterface $before): int return $this->lock(function () use ($before) { $jobs = $this->read(); - $this->write($prunedJobs = (new Collection($jobs)) - ->reject(fn ($job) => $job->failed_at_timestamp <= $before->getTimestamp()) - ->values() - ->all() + $this->write( + $prunedJobs = (new Collection($jobs)) + ->reject(fn ($job) => $job->failed_at_timestamp <= $before->getTimestamp()) + ->values() + ->all(), ); return count($jobs) - count($prunedJobs); @@ -132,9 +142,7 @@ protected function lock(Closure $callback): mixed return ($this->lockProviderResolver)() ->lock('blitzphp-failed-jobs', 5) - ->block(10, function () use ($callback) { - return $callback(); - }); + ->block(10, fn () => $callback()); } /** @@ -164,7 +172,7 @@ protected function write(array $jobs): void { file_put_contents( $this->path, - json_encode($jobs, JSON_PRETTY_PRINT) + json_encode($jobs, JSON_PRETTY_PRINT), ); } diff --git a/src/Failed/NullFailedJobProvider.php b/src/Failed/NullFailedJobProvider.php index 54493e7..4b19408 100644 --- a/src/Failed/NullFailedJobProvider.php +++ b/src/Failed/NullFailedJobProvider.php @@ -1,4 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Failed; use Throwable; @@ -11,7 +21,7 @@ class NullFailedJobProvider implements CountableFailedJobProvider, FailedJobProv /** * {@inheritDoc} */ - public function log(string $connection, string $queue, string $payload, Throwable $exception): string|int|null + public function log(string $connection, string $queue, string $payload, Throwable $exception): int|string|null { return null; } @@ -35,7 +45,7 @@ public function all(): array /** * {@inheritDoc} */ - public function find(string|int $id): ?object + public function find(int|string $id): ?object { return null; } @@ -43,7 +53,7 @@ public function find(string|int $id): ?object /** * {@inheritDoc} */ - public function forget(string|int $id): bool + public function forget(int|string $id): bool { return true; } @@ -63,4 +73,4 @@ public function count(?string $connection = null, ?string $queue = null): int { return 0; } -} \ No newline at end of file +} diff --git a/src/Failed/PrunableFailedJobProvider.php b/src/Failed/PrunableFailedJobProvider.php index 8a736a7..13e0159 100644 --- a/src/Failed/PrunableFailedJobProvider.php +++ b/src/Failed/PrunableFailedJobProvider.php @@ -1,4 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Failed; use DateTimeInterface; @@ -12,4 +22,4 @@ interface PrunableFailedJobProvider * Purge les entrées antérieures à la date donnée. */ public function prune(DateTimeInterface $before): int; -} \ No newline at end of file +} diff --git a/src/Job.php b/src/Job.php index cdeea02..8a69717 100644 --- a/src/Job.php +++ b/src/Job.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue; use BlitzPHP\Queue\Traits\Dispatchable; @@ -15,7 +24,9 @@ */ abstract class Job { - use Dispatchable, InteractsWithQueue, SerializesModels; + use Dispatchable; + use InteractsWithQueue; + use SerializesModels; /** * Nombre maximal de tentatives avant échec définitif. diff --git a/src/Jobs/DatabaseJob.php b/src/Jobs/DatabaseJob.php index 4650828..86eca4f 100644 --- a/src/Jobs/DatabaseJob.php +++ b/src/Jobs/DatabaseJob.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Jobs; use BlitzPHP\Contracts\Container\ContainerInterface; @@ -14,13 +23,13 @@ class DatabaseJob extends Job implements JobContract /** * Crée une nouvelle instance de job. * - * @param DatabaseDriver $database Instance du pilote base de données. - * @param DatabaseJobRecord $job Enregistrement / payload du job en base. + * @param DatabaseDriver $database Instance du pilote base de données. + * @param DatabaseJobRecord $job Enregistrement / payload du job en base. */ public function __construct(ContainerInterface $container, protected DatabaseDriver $database, protected DatabaseJobRecord $job, string $connectionName, string $queue) { - $this->queue = $queue; - $this->container = $container; + $this->queue = $queue; + $this->container = $container; $this->connectionName = $connectionName; } diff --git a/src/Jobs/DatabaseJobRecord.php b/src/Jobs/DatabaseJobRecord.php index 7659098..5b80cb6 100644 --- a/src/Jobs/DatabaseJobRecord.php +++ b/src/Jobs/DatabaseJobRecord.php @@ -1,8 +1,18 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Jobs; use BlitzPHP\Traits\Support\InteractsWithTime; +use stdClass; /** * Enveloppe d'une ligne SQL représentant un job en file d'attente. @@ -14,9 +24,9 @@ class DatabaseJobRecord /** * Crée une instance d'enregistrement de job. * - * @param \stdClass $record Enregistrement sous-jacent du job. + * @param stdClass $record Enregistrement sous-jacent du job. */ - public function __construct(protected \stdClass $record) + public function __construct(protected stdClass $record) { } diff --git a/src/Jobs/FakeJob.php b/src/Jobs/FakeJob.php index 29dd848..8a59d37 100644 --- a/src/Jobs/FakeJob.php +++ b/src/Jobs/FakeJob.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Jobs; use BlitzPHP\Contracts\Queue\Job as JobContract; @@ -28,7 +37,7 @@ class FakeJob extends Job implements JobContract /** * Exception ayant provoqué l'échec du job. * - * @var \Throwable + * @var Throwable */ public $failedWith; @@ -37,7 +46,7 @@ class FakeJob extends Job implements JobContract */ public function getJobId(): string { - return (string) Text::uuid(); + return (string) Text::uuid(); } /** @@ -51,9 +60,9 @@ public function getRawBody(): string /** * Relâche le job dans la file après n secondes. */ - public function release(DateTimeInterface|DateInterval|int $delay = 0): void + public function release(DateInterval|DateTimeInterface|int $delay = 0): void { - $this->released = true; + $this->released = true; $this->releaseDelay = $delay; } @@ -78,7 +87,7 @@ public function delete(): void */ public function fail(?Throwable $e = null): void { - $this->failed = true; + $this->failed = true; $this->failedWith = $e; } } diff --git a/src/Jobs/InspectedJob.php b/src/Jobs/InspectedJob.php index fe05480..df9bbc3 100644 --- a/src/Jobs/InspectedJob.php +++ b/src/Jobs/InspectedJob.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Jobs; use BlitzPHP\Utilities\Date; @@ -12,10 +21,10 @@ class InspectedJob /** * Crée une instance de job inspecté. * - * @param string|null $uuid Identifiant unique du job. - * @param string|null $name Nom d'affichage du job. - * @param int $attempts Nombre de tentatives déjà effectuées. - * @param Date|null $createdAt Date et heure de création du job. + * @param string|null $uuid Identifiant unique du job. + * @param string|null $name Nom d'affichage du job. + * @param int $attempts Nombre de tentatives déjà effectuées. + * @param Date|null $createdAt Date et heure de création du job. */ public function __construct( public readonly ?string $uuid, @@ -28,8 +37,8 @@ public function __construct( /** * Crée une instance à partir d'un payload JSON brut. * - * @param string $payload Payload JSON brut du job. - * @param int|null $attempts Nombre de tentatives déjà effectuées. + * @param string $payload Payload JSON brut du job. + * @param int|null $attempts Nombre de tentatives déjà effectuées. */ public static function fromPayload(string $payload, ?int $attempts = null): static { diff --git a/src/Jobs/Job.php b/src/Jobs/Job.php index 0f411cf..fc01107 100644 --- a/src/Jobs/Job.php +++ b/src/Jobs/Job.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Jobs; use BlitzPHP\Contracts\Container\ContainerInterface; @@ -60,7 +69,7 @@ abstract class Job /** * Retourne l'identifiant du job. */ - abstract public function getJobId() : string|int|null; + abstract public function getJobId(): int|string|null; /** * Retourne le corps brut (JSON) du job. @@ -82,7 +91,6 @@ public function fire(): void { $payload = $this->payload(); - [$class, $method] = JobName::parse($payload['job']); ($this->instance = $this->resolve($class))->{$method}($this, $payload['data']); @@ -168,7 +176,7 @@ public function fail(?Throwable $e = null): void $this->failed($e); } finally { - $this->resolve(QueueEventManager::class)->jobFailed($this->connectionName, $this, $e ?: new ManuallyFailedException); + $this->resolve(QueueEventManager::class)->jobFailed($this->connectionName, $this, $e ?: new ManuallyFailedException()); } } @@ -177,12 +185,12 @@ public function fail(?Throwable $e = null): void */ protected function shouldRollBackDatabaseTransaction(Throwable $e): bool { - $config = config('queue.failed'); + $config = config('queue.failed'); - return $e instanceof TimeoutExceededException && - $config['database'] && - in_array($config['driver'], ['database', 'database-uuids']) && - $this->container->bound(ConnectionResolverInterface::class); + return $e instanceof TimeoutExceededException + && $config['database'] + && in_array($config['driver'], ['database', 'database-uuids'], true) + && $this->container->bound(ConnectionResolverInterface::class); } /** @@ -250,7 +258,7 @@ public function shouldFailOnTimeout(): bool /** * Secondes d'attente avant de relancer un job ayant levé une exception non gérée. * - * @return int|int[]|null + * @return int|list|null */ public function backoff() { diff --git a/src/Jobs/JobName.php b/src/Jobs/JobName.php index 290354b..66381db 100644 --- a/src/Jobs/JobName.php +++ b/src/Jobs/JobName.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Jobs; use BlitzPHP\Utilities\String\Text; @@ -32,7 +41,7 @@ public static function resolve(string $name, array $payload): string /** * Retourne le nom de classe du job enfilé. * - * @param array $payload + * @param array $payload */ public static function resolveClassName(string $name, array $payload): string { diff --git a/src/Jobs/SyncJob.php b/src/Jobs/SyncJob.php index 48b0c74..be80060 100644 --- a/src/Jobs/SyncJob.php +++ b/src/Jobs/SyncJob.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Jobs; use BlitzPHP\Contracts\Container\ContainerInterface; @@ -20,12 +29,12 @@ class SyncJob extends Job implements JobContract /** * Crée une nouvelle instance de job. * - * @param string $payload Données du message de file. + * @param string $payload Données du message de file. */ public function __construct(ContainerInterface $container, protected string $payload, string $connectionName, string $queue) { - $this->queue = $queue; - $this->container = $container; + $this->queue = $queue; + $this->container = $container; $this->connectionName = $connectionName; } diff --git a/src/Manager.php b/src/Manager.php index a51f785..c132736 100644 --- a/src/Manager.php +++ b/src/Manager.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue; use BlitzPHP\Cache\Cache; @@ -11,7 +20,6 @@ use BlitzPHP\Contracts\Queue\Queue as QueueContract; use BlitzPHP\Queue\DTO\Config; use BlitzPHP\Queue\Events\QueueEventManager; -use Closure; use DateInterval; use DateTimeInterface; use InvalidArgumentException; @@ -37,7 +45,7 @@ class Manager implements Factory, Monitor /** * Gestionnaire d'événements de la file. */ - protected QueueEventManager $queueEventManager; + protected QueueEventManager $queueEventManager; /** * Cache applicatif (pause / redémarrage des workers). @@ -71,7 +79,7 @@ public function before(callable $callback): void */ public function after(callable $callback): void { - $this->events->on(QueueEventManager::JOB_PROCESSED, $callback); + $this->events->on(QueueEventManager::JOB_PROCESSED, $callback); } /** @@ -79,7 +87,7 @@ public function after(callable $callback): void */ public function exceptionOccurred(callable $callback): void { - $this->events->on(QueueEventManager::JOB_EXCEPTION_OCCURED, $callback); + $this->events->on(QueueEventManager::JOB_EXCEPTION_OCCURED, $callback); } /** @@ -87,7 +95,7 @@ public function exceptionOccurred(callable $callback): void */ public function looping(callable $callback): void { - $this->events->on(QueueEventManager::JOB_LOOPING, $callback); + $this->events->on(QueueEventManager::JOB_LOOPING, $callback); } /** @@ -103,7 +111,7 @@ public function failing(callable $callback): void */ public function starting(callable $callback): void { - $this->events->on(QueueEventManager::WORKER_STARTING, $callback); + $this->events->on(QueueEventManager::WORKER_STARTING, $callback); } /** @@ -111,25 +119,25 @@ public function starting(callable $callback): void */ public function stopping(callable $callback): void { - $this->events->on(QueueEventManager::WORKER_STOPPING, $callback); + $this->events->on(QueueEventManager::WORKER_STOPPING, $callback); } /** * Retourne (et instancie si besoin) le gestionnaire d'événements de file. */ - protected function queueEventManager(): QueueEventManager - { - if (! $this->queueEventManager) { - $this->queueEventManager = $this->container->get(QueueEventManager::class); - } + protected function queueEventManager(): QueueEventManager + { + if (! $this->queueEventManager) { + $this->queueEventManager = $this->container->get(QueueEventManager::class); + } - return $this->queueEventManager; - } + return $this->queueEventManager; + } /** * Indique si le pilote (connexion) donné est déjà résolu. */ - public function connected(UnitEnum|string|null $name = null): bool + public function connected(string|UnitEnum|null $name = null): bool { $name = $name instanceof UnitEnum ? $name->name : ($name ?: $this->getDefaultDriver()); @@ -141,7 +149,7 @@ public function connected(UnitEnum|string|null $name = null): bool * * Les pilotes sont instanciés à la demande pour éviter les connexions inutiles. */ - public function driver(UnitEnum|string|null $name = null): QueueContract + public function driver(string|UnitEnum|null $name = null): QueueContract { $name = $name instanceof UnitEnum ? $name->name : ($name ?: $this->getDefaultDriver()); @@ -180,21 +188,21 @@ protected function resolve(string $name): Queue */ public function pause(string $connection, string $queue): void { - $this->cache->forever("blitzphp-queue-paused-{$connection}-{$queue}", true); + $this->cache->forever("blitzphp-queue-paused-{$connection}-{$queue}", true); - $this->queueEventManager()->queuePaused($connection, $queue); + $this->queueEventManager()->queuePaused($connection, $queue); } /** * Met une file en pause pendant une durée donnée. */ - public function pauseFor(string $connection, string $queue, DateTimeInterface|DateInterval|int $ttl): void + public function pauseFor(string $connection, string $queue, DateInterval|DateTimeInterface|int $ttl): void { - $convertedTtl = $ttl instanceof DateTimeInterface ? $ttl->getTimestamp() : $ttl; + $convertedTtl = $ttl instanceof DateTimeInterface ? $ttl->getTimestamp() : $ttl; - $this->cache->set("blitzphp-queue-paused-{$connection}-{$queue}", true, $convertedTtl); + $this->cache->set("blitzphp-queue-paused-{$connection}-{$queue}", true, $convertedTtl); - $this->queueEventManager()->queuePaused($connection, $queue, $ttl); + $this->queueEventManager()->queuePaused($connection, $queue, $ttl); } /** @@ -202,9 +210,9 @@ public function pauseFor(string $connection, string $queue, DateTimeInterface|Da */ public function resume(string $connection, string $queue): void { - $this->cache->delete("blitzphp-queue-paused-{$connection}-{$queue}"); + $this->cache->delete("blitzphp-queue-paused-{$connection}-{$queue}"); - $this->queueEventManager()->queueResumed($connection, $queue); + $this->queueEventManager()->queueResumed($connection, $queue); } /** @@ -224,7 +232,7 @@ public function isPaused(string $connection, string $queue): bool public function withoutInterruptionPolling(): void { Worker::$restartable = false; - Worker::$pausable = false; + Worker::$pausable = false; } /** @@ -234,13 +242,13 @@ public function getDefaultDriver(): string { return $this->config->default; } - - /** + + /** * Définit le nom de la connexion par défaut. */ public function setDefaultDriver(string $name): void { - $this->config->setDefaultDriver($name); + $this->config->setDefaultDriver($name); } /** @@ -278,6 +286,6 @@ public function setContainer(ContainerInterface $container): self */ public function __call(string $method, array $parameters = []): mixed { - return $this->driver()->$method(...$parameters); + return $this->driver()->{$method}(...$parameters); } } diff --git a/src/Models/JobModel.php b/src/Models/JobModel.php index 4c94aa6..15c7db3 100644 --- a/src/Models/JobModel.php +++ b/src/Models/JobModel.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Models; use BlitzPHP\Contracts\Database\ConnectionInterface; @@ -21,7 +30,7 @@ class JobModel extends Model /** * Format de stockage des dates (horodatage Unix). */ - protected string $dateFormat = 'int'; + protected string $dateFormat = 'int'; /** * Désactive les callbacks du modèle pendant les opérations de file. @@ -34,22 +43,22 @@ class JobModel extends Model protected ?int $retryAfter = 60; /** - * @param array $config Configuration de la connexion `database`. - * @param ConnectionResolverInterface $resolver Résolveur de connexions. - * @param ConnectionInterface $db Connexion SQL utilisée. + * @param array $config Configuration de la connexion `database`. + * @param ConnectionResolverInterface $resolver Résolveur de connexions. + * @param ConnectionInterface $db Connexion SQL utilisée. */ - public function __construct(array $config, protected ConnectionResolverInterface $resolver, ConnectionInterface $db) - { + public function __construct(array $config, protected ConnectionResolverInterface $resolver, ConnectionInterface $db) + { assert($db instanceof BaseConnection); - + $this->table = $config['table']; $this->retryAfter = $config['retry_after'] ?? 60; // Désactive le mode transaction strict $db->transStrict(false); - + parent::__construct($resolver, $db); - } + } /** * Retourne le nombre total de jobs dans la file. @@ -79,7 +88,7 @@ public function pendingSize(string $queue): int public function delayedSize(string $queue): int { return $this->builder() - ->where('queue', $$queue) + ->where('queue', ${$queue}) ->where('available_at >', $this->currentTime()) ->whereNull('reserved_at') ->count(); @@ -91,7 +100,7 @@ public function delayedSize(string $queue): int public function reservedSize(string $queue): int { return $this->builder() - ->where('queue', $$queue) + ->where('queue', ${$queue}) ->whereNotNull('reserved_at') ->count(); } @@ -149,7 +158,7 @@ public function creationTimeOfOldestPendingJob(string $queue): ?int */ public function pushToDatabase(array $data): mixed { - $this->builder()->insert($data); + $this->builder()->insert($data); return $this->db->lastId($this->table); } @@ -178,13 +187,12 @@ public function getNextAvailableJob(string $queue): ?object public function deleteReserved(string $queue, string $id): void { $this->db->transaction(function () use ($id) { - if ($this/*->lockForUpdate()*/->where('id', $id)->first()) { + if ($this/* ->lockForUpdate() */ ->where('id', $id)->first()) { $this->where('id', $id)->delete(); } }); } - /** * Supprime tous les jobs de la file. */ @@ -192,7 +200,7 @@ public function clear(string $queue): bool { $this->builder()->where('queue', $queue)->delete(); - return true; + return true; } /** diff --git a/src/Providers/QueueProvider.php b/src/Providers/QueueProvider.php index 5d1112a..4c825f3 100644 --- a/src/Providers/QueueProvider.php +++ b/src/Providers/QueueProvider.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Providers; use BlitzPHP\Container\AbstractProvider; diff --git a/src/Queue.php b/src/Queue.php index d384fc8..bbeca3e 100644 --- a/src/Queue.php +++ b/src/Queue.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue; use BlitzPHP\Contracts\Container\ContainerInterface; @@ -12,7 +21,6 @@ use BlitzPHP\Traits\Support\InteractsWithTime; use BlitzPHP\Utilities\Date; use BlitzPHP\Utilities\Iterable\Collection; -use BlitzPHP\Utilities\String\Text; use BlitzPHP\Utilities\String\Uuid; use Closure; use DateInterval; @@ -58,14 +66,14 @@ abstract class Queue implements QueueContract /** * Callbacks exécutés lors de la construction du payload. * - * @var callable[] + * @var list */ protected static array $createPayloadCallbacks = []; /** * Envoie un job sur une file nommée. */ - public function pushOn(string $queue, string|object $job, mixed $data = ''): mixed + public function pushOn(string $queue, object|string $job, mixed $data = ''): mixed { return $this->push($job, $data, $queue); } @@ -73,7 +81,7 @@ public function pushOn(string $queue, string|object $job, mixed $data = ''): mix /** * Envoie un job sur une file nommée, avec un délai en secondes. */ - public function laterOn(string $queue, DateTimeInterface|DateInterval|int $delay, string|object $job, mixed $data = ''): mixed + public function laterOn(string $queue, DateInterval|DateTimeInterface|int $delay, object|string $job, mixed $data = ''): mixed { return $this->later($delay, $job, $data, $queue); } @@ -81,7 +89,7 @@ public function laterOn(string $queue, DateTimeInterface|DateInterval|int $delay /** * Envoie plusieurs jobs sur la file. * - * @param array $jobs + * @param array $jobs * * @return void */ @@ -105,7 +113,7 @@ public function clear(string $queue): bool * * @throws InvalidPayloadException Si l'encodage JSON échoue. */ - protected function createPayload(string|object $job, string $queue, mixed $data = '', DateTimeInterface|DateInterval|int|null $delay = null): ?string + protected function createPayload(object|string $job, string $queue, mixed $data = '', DateInterval|DateTimeInterface|int|null $delay = null): ?string { if ($job instanceof Closure) { $job = CallQueuedClosure::create($job); @@ -121,7 +129,8 @@ protected function createPayload(string|object $job, string $queue, mixed $data if (json_last_error() !== JSON_ERROR_NONE) { throw new InvalidPayloadException( - 'Unable to JSON encode payload. Error ('.json_last_error().'): '.json_last_error_msg(), $value + 'Unable to JSON encode payload. Error (' . json_last_error() . '): ' . json_last_error_msg(), + $value, ); } @@ -131,7 +140,7 @@ protected function createPayload(string|object $job, string $queue, mixed $data /** * Construit le tableau de payload (objet métier ou handler sous forme de chaîne). */ - protected function createPayloadArray(string|object $job, string $queue, mixed $data = ''): array + protected function createPayloadArray(object|string $job, string $queue, mixed $data = ''): array { return is_object($job) ? $this->createObjectPayload($job, $queue) @@ -146,20 +155,20 @@ protected function createPayloadArray(string|object $job, string $queue, mixed $ protected function createObjectPayload(object $job, string $queue): array { $payload = $this->withCreatePayloadHooks($queue, [ - 'uuid' => (string) Uuid::v4(), - 'displayName' => $this->getDisplayName($job), - 'job' => 'BlitzPHP\Queue\CallQueuedHandler@call', - 'maxTries' => $this->getJobTries($job), - 'maxExceptions' => $job->maxExceptions ?? null, - 'failOnTimeout' => $job->failOnTimeout ?? false, - 'backoff' => $this->getJobBackoff($job), - 'timeout' => $job->timeout ?? null, - 'retryUntil' => $this->getJobExpiration($job), + 'uuid' => (string) Uuid::v4(), + 'displayName' => $this->getDisplayName($job), + 'job' => 'BlitzPHP\Queue\CallQueuedHandler@call', + 'maxTries' => $this->getJobTries($job), + 'maxExceptions' => $job->maxExceptions ?? null, + 'failOnTimeout' => $job->failOnTimeout ?? false, + 'backoff' => $this->getJobBackoff($job), + 'timeout' => $job->timeout ?? null, + 'retryUntil' => $this->getJobExpiration($job), 'deleteWhenMissingModels' => $job->deleteWhenMissingModels ?? false, - 'data' => [ + 'data' => [ 'commandName' => $job, - 'command' => $job, - 'batchId' => $job->batchId ?? null, + 'command' => $job, + 'batchId' => $job->batchId ?? null, ], 'createdAt' => Date::now()->getTimestamp(), ]); @@ -170,16 +179,16 @@ protected function createObjectPayload(object $job, string $queue): array : serialize(clone $job); } catch (Throwable $e) { throw new RuntimeException( - sprintf('Failed to serialize job of type [%s]: %s', get_class($job), $e->getMessage()), + sprintf('Failed to serialize job of type [%s]: %s', $job::class, $e->getMessage()), 0, - $e + $e, ); } return array_merge($payload, [ 'data' => array_merge($payload['data'], [ - 'commandName' => get_class($job), - 'command' => $command, + 'commandName' => $job::class, + 'command' => $command, ]), ]); } @@ -191,7 +200,7 @@ protected function getDisplayName(object $job): string { return method_exists($job, 'displayName') ? $job->displayName() - : get_class($job); + : $job::class; } /** @@ -217,11 +226,11 @@ public function getJobBackoff(object $job): mixed if (method_exists($job, 'backoff')) { $backoff = $job->backoff(); - } else if (property_exists($job, 'backoff')) { - $backoff = $job->backoff ?? null; - } + } elseif (property_exists($job, 'backoff')) { + $backoff = $job->backoff ?? null; + } - if (is_null($backoff)) { + if (null === $backoff) { return null; } @@ -260,16 +269,16 @@ protected function jobShouldBeEncrypted(object $job): bool protected function createStringPayload(string $job, string $queue, mixed $data): array { return $this->withCreatePayloadHooks($queue, [ - 'uuid' => (string) Uuid::v4(), - 'displayName' => is_string($job) ? explode('@', $job)[0] : null, - 'job' => $job, - 'maxTries' => null, + 'uuid' => (string) Uuid::v4(), + 'displayName' => is_string($job) ? explode('@', $job)[0] : null, + 'job' => $job, + 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, - 'backoff' => null, - 'timeout' => null, - 'data' => $data, - 'createdAt' => Date::now()->getTimestamp(), + 'backoff' => null, + 'timeout' => null, + 'data' => $data, + 'createdAt' => Date::now()->getTimestamp(), ]); } @@ -278,7 +287,7 @@ protected function createStringPayload(string $job, string $queue, mixed $data): */ public static function createPayloadUsing(?callable $callback = null): void { - if (is_null($callback)) { + if (null === $callback) { static::$createPayloadCallbacks = []; } else { static::$createPayloadCallbacks[] = $callback; @@ -302,9 +311,9 @@ protected function withCreatePayloadHooks(string $queue, array $payload): array /** * Enfile un job via le callback fourni, après avoir émis les événements d'enfilement. */ - protected function enqueueUsing(string|object $job, string $payload, ?string $queue, DateTimeInterface|DateInterval|int|null $delay, callable $callback): mixed + protected function enqueueUsing(object|string $job, string $payload, ?string $queue, DateInterval|DateTimeInterface|int|null $delay, callable $callback): mixed { - /* + /* if ($this->shouldDispatchAfterCommit($job) && $this->container->bound('db.transactions')) { if ($job->shouldBeUnique) { $this->container->make('db.transactions')->addCallbackForRollback( @@ -324,7 +333,7 @@ function () use ($queue, $job, $payload, $delay, $callback) { } ); } - */ + */ $this->raiseJobQueueingEvent($queue, $job, $payload, $delay); @@ -336,7 +345,7 @@ function () use ($queue, $job, $payload, $delay, $callback) { /** * Indique si le job doit attendre le commit des transactions SQL avant d'être envoyé. */ - protected function shouldDispatchAfterCommit(string|object $job): bool + protected function shouldDispatchAfterCommit(object|string $job): bool { if (! $job instanceof Closure && is_object($job) && isset($job->afterCommit)) { return $job->afterCommit; @@ -348,7 +357,7 @@ protected function shouldDispatchAfterCommit(string|object $job): bool /** * Émet l'événement « job en cours d'enfilement ». */ - protected function raiseJobQueueingEvent(?string $queue, string|object $job, string $payload, DateTimeInterface|DateInterval|int|null $delay): void + protected function raiseJobQueueingEvent(?string $queue, object|string $job, string $payload, DateInterval|DateTimeInterface|int|null $delay): void { $this->eventManager()->jobQueueing($this->connectionName, $queue, $job, $payload, $delay); } @@ -356,7 +365,7 @@ protected function raiseJobQueueingEvent(?string $queue, string|object $job, str /** * Émet l'événement « job enfilé ». */ - protected function raiseJobQueuedEvent(?string $queue, string|int|null $jobId, string|object $job, string $payload, DateTimeInterface|DateInterval|int|null $delay) + protected function raiseJobQueuedEvent(?string $queue, int|string|null $jobId, object|string $job, string $payload, DateInterval|DateTimeInterface|int|null $delay) { $this->eventManager()->jobQueued($this->connectionName, $queue, $jobId, $job, $payload, $delay); } @@ -369,7 +378,7 @@ protected function eventManager(): QueueEventManager if (! $this->eventManager) { $this->eventManager = new QueueEventManager($this->container->get(EventManagerInterface::class)); } - + return $this->eventManager; } diff --git a/src/Traits/Dispatchable.php b/src/Traits/Dispatchable.php index 4f109bd..8e28706 100644 --- a/src/Traits/Dispatchable.php +++ b/src/Traits/Dispatchable.php @@ -1,9 +1,19 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Traits; use BlitzPHP\Queue\Config\Services; -use DateTimeInterface; use DateInterval; +use DateTimeInterface; /** * Permet de dispatcher un job via des méthodes statiques (`dispatch`, `dispatchLater`, etc.). @@ -33,7 +43,7 @@ public static function dispatchOn(string $queue, mixed ...$parameters): mixed /** * Dispatch le job avec délai */ - public static function dispatchLater(DateTimeInterface|DateInterval|int $delay, mixed ...$parameters): mixed + public static function dispatchLater(DateInterval|DateTimeInterface|int $delay, mixed ...$parameters): mixed { $job = new static(...$parameters); @@ -43,7 +53,7 @@ public static function dispatchLater(DateTimeInterface|DateInterval|int $delay, /** * Dispatch le job sur une queue spécifique avec délai */ - public static function dispatchLaterOn(string $queue, DateTimeInterface|DateInterval|int $delay, ...$parameters): mixed + public static function dispatchLaterOn(string $queue, DateInterval|DateTimeInterface|int $delay, ...$parameters): mixed { $job = new static(...$parameters); diff --git a/src/Traits/InteractsWithQueue.php b/src/Traits/InteractsWithQueue.php index 332c0f3..b95e468 100644 --- a/src/Traits/InteractsWithQueue.php +++ b/src/Traits/InteractsWithQueue.php @@ -1,13 +1,22 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Traits; -use DateTimeInterface; use BlitzPHP\Contracts\Queue\Job as JobContract; use BlitzPHP\Queue\Exceptions\ManuallyFailedException; use BlitzPHP\Queue\Jobs\FakeJob; use BlitzPHP\Traits\Support\InteractsWithTime; use DateInterval; +use DateTimeInterface; use InvalidArgumentException; use PHPUnit\Framework\Assert as PHPUnit; use RuntimeException; @@ -48,13 +57,13 @@ public function delete(): void * * @throws InvalidArgumentException */ - public function fail(Throwable|string|null $exception = null): void + public function fail(string|Throwable|null $exception = null): void { if (is_string($exception)) { $exception = new ManuallyFailedException($exception); } - if ($exception instanceof Throwable || is_null($exception)) { + if ($exception instanceof Throwable || null === $exception) { if ($this->job) { $this->job->fail($exception); } @@ -66,7 +75,7 @@ public function fail(Throwable|string|null $exception = null): void /** * Relâche le job dans la file après n secondes. */ - public function release(DateTimeInterface|DateInterval|int $delay = 0): void + public function release(DateInterval|DateTimeInterface|int $delay = 0): void { $delay = $delay instanceof DateTimeInterface ? $this->secondsUntil($delay) @@ -82,7 +91,7 @@ public function release(DateTimeInterface|DateInterval|int $delay = 0): void */ public function withFakeQueueInteractions(): self { - $this->job = new FakeJob; + $this->job = new FakeJob(); return $this; } @@ -135,7 +144,7 @@ public function assertFailed(): self /** * Vérifie que le job a échoué manuellement avec une exception donnée. */ - public function assertFailedWith(Throwable|string $exception): self + public function assertFailedWith(string|Throwable $exception): self { $this->assertFailed(); @@ -194,7 +203,7 @@ public function assertNotFailed(): self /** * Vérifie que le job a été relâché dans la file. */ - public function assertReleased(DateTimeInterface|DateInterval|int|null $delay = null): self + public function assertReleased(DateInterval|DateTimeInterface|int|null $delay = null): self { $this->ensureQueueInteractionsHaveBeenFaked(); @@ -207,7 +216,7 @@ public function assertReleased(DateTimeInterface|DateInterval|int|null $delay = 'Job was expected to be released, but was not.' ); */ - if (! is_null($delay)) { + if (null !== $delay) { /* PHPUnit::assertSame( $delay, $this->job->releaseDelay, diff --git a/src/Traits/SerializesAndRestoresModelIdentifiers.php b/src/Traits/SerializesAndRestoresModelIdentifiers.php index 797c6f1..7762aa6 100644 --- a/src/Traits/SerializesAndRestoresModelIdentifiers.php +++ b/src/Traits/SerializesAndRestoresModelIdentifiers.php @@ -1,11 +1,22 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Traits; use BlitzPHP\Contracts\Queue\QueueableCollection; use BlitzPHP\Contracts\Queue\QueueableEntity; use BlitzPHP\Utilities\Iterable\Collection; +use BlitzPHP\Wolke\Builder; use BlitzPHP\Wolke\Collection as WolkeCollection; +use BlitzPHP\Wolke\Model; use BlitzPHP\Wolke\Relations\Concerns\AsPivot; use BlitzPHP\Wolke\Relations\Pivot; use Illuminate\Contracts\Database\ModelIdentifier; @@ -25,20 +36,20 @@ protected function getSerializedPropertyValue(mixed $value, bool $withRelations $value->getQueueableClass(), $value->getQueueableIds(), $withRelations ? $value->getQueueableRelations() : [], - $value->getQueueableConnection() + $value->getQueueableConnection(), ))->useCollectionClass( - ($collectionClass = get_class($value)) !== WolkeCollection;::class + ($collectionClass = $value::class) !== WolkeCollection::class ? $collectionClass - : null + : null, ); } if ($value instanceof QueueableEntity) { return new ModelIdentifier( - get_class($value), + $value::class, $value->getQueueableId(), $withRelations ? $value->getQueueableRelations() : [], - $value->getQueueableConnection() + $value->getQueueableConnection(), ); } @@ -62,7 +73,8 @@ protected function getRestoredPropertyValue(mixed $value): mixed /** * Restaure une collection enfilable. * - * @param \Illuminate\Contracts\Database\ModelIdentifier $value + * @param ModelIdentifier $value + * * @return WolkeCollection */ protected function restoreCollection($value) @@ -70,40 +82,43 @@ protected function restoreCollection($value) $class = $value->getClass(); if (! $class || count($value->id) === 0) { - return ! is_null($value->collectionClass ?? null) - ? new $value->collectionClass - : new WolkeCollection;; + return null !== ($value->collectionClass ?? null) + ? new $value->collectionClass() + : new WolkeCollection(); } $collection = $this->getQueryForModelRestoration( - (new $class)->setConnection($value->connection), $value->id + (new $class())->setConnection($value->connection), + $value->id, )->useWritePdo()->get(); - if (is_a($class, Pivot::class, true) || in_array(AsPivot::class, class_uses($class))) { + if (is_a($class, Pivot::class, true) || in_array(AsPivot::class, class_uses($class), true)) { return $collection; } $collection = $collection->keyBy->getKey(); - $collectionClass = get_class($collection); + $collectionClass = $collection::class; return (new $collectionClass( (new Collection($value->id)) ->map(fn ($id) => $collection[$id] ?? null) - ->filter() + ->filter(), ))->loadMissing($value->relations ?? []); } /** * Restaure le modèle à partir de son identifiant. * - * @param \Illuminate\Contracts\Database\ModelIdentifier $value - * @return \BlitzPHP\Wolke\Model + * @param ModelIdentifier $value + * + * @return Model */ public function restoreModel($value) { return $this->getQueryForModelRestoration( - (new ($value->getClass()))->setConnection($value->connection), $value->id + (new ($value->getClass()))->setConnection($value->connection), + $value->id, )->useWritePdo()->firstOrFail()->loadMissing($value->relations ?? []); } @@ -112,9 +127,9 @@ public function restoreModel($value) * * @template TModel of \BlitzPHP\Wolke\Model * - * @param TModel $model - * - * @return \BlitzPHP\Wolke\Builder + * @param TModel $model + * + * @return Builder */ protected function getQueryForModelRestoration($model, array|int $ids) { diff --git a/src/Traits/SerializesModels.php b/src/Traits/SerializesModels.php index 76db645..c724461 100644 --- a/src/Traits/SerializesModels.php +++ b/src/Traits/SerializesModels.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue\Traits; use ReflectionClass; @@ -22,7 +31,7 @@ public function __serialize(): array $reflectionClass = new ReflectionClass($this); [$class, $properties, $classLevelWithoutRelations] = [ - get_class($this), + static::class, $reflectionClass->getProperties(), property_exists($this, 'withoutRelations') && $this->withoutRelations === true, ]; @@ -56,7 +65,8 @@ public function __serialize(): array $values[$name] = $this->getSerializedPropertyValue( $value, - ! $classLevelWithoutRelations); + ! $classLevelWithoutRelations, + ); } return $values; @@ -69,7 +79,7 @@ public function __unserialize(array $values): void { $properties = (new ReflectionClass($this))->getProperties(); - $class = get_class($this); + $class = static::class; foreach ($properties as $property) { if ($property->isStatic()) { @@ -89,7 +99,8 @@ public function __unserialize(array $values): void } $property->setValue( - $this, $this->getRestoredPropertyValue($values[$name]) + $this, + $this->getRestoredPropertyValue($values[$name]), ); } } diff --git a/src/Worker.php b/src/Worker.php index 4803bfd..d0ad5cc 100644 --- a/src/Worker.php +++ b/src/Worker.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue; use BlitzPHP\Contracts\Cache\CacheInterface; @@ -25,19 +34,26 @@ class Worker { // use DetectsLostConnections; - /** Code de sortie en cas de succès. */ - const EXIT_SUCCESS = EXIT_SUCCESS; - /** Code de sortie en cas d'erreur. */ - const EXIT_ERROR = EXIT_ERROR; - /** Code de sortie en cas de dépassement de la limite mémoire. */ - const EXIT_MEMORY_LIMIT = 12; + /** + * Code de sortie en cas de succès. + */ + public const EXIT_SUCCESS = EXIT_SUCCESS; + + /** + * Code de sortie en cas d'erreur. + */ + public const EXIT_ERROR = EXIT_ERROR; + + /** + * Code de sortie en cas de dépassement de la limite mémoire. + */ + public const EXIT_MEMORY_LIMIT = 12; /** * Nom du worker. */ protected ?string $name; - /** * Implémentation du dépôt de cache. */ @@ -46,7 +62,7 @@ class Worker /** * Gestionnaire d'exceptions (contrat Illuminate). * - * @var \Illuminate\Contracts\Debug\ExceptionHandler + * @var ExceptionHandler */ protected $exceptions; @@ -82,7 +98,7 @@ class Worker /** * Callbacks utilisés pour prélever les jobs. * - * @var callable[] + * @var list */ protected static array $popCallbacks = []; @@ -109,9 +125,9 @@ class Worker /** * Crée un worker de file d'attente. * - * @param Manager $manager Instance du gestionnaire de files. - * @param QueueEventManager $events Instance du gestionnaire d'événements de file. - * @param \Illuminate\Contracts\Debug\ExceptionHandler $exceptions + * @param Manager $manager Instance du gestionnaire de files. + * @param QueueEventManager $events Instance du gestionnaire d'événements de file. + * @param ExceptionHandler $exceptions */ public function __construct( protected Manager $manager, @@ -122,7 +138,7 @@ public function __construct( ) { // $this->exceptions = $exceptions; $this->isDownForMaintenance = $isDownForMaintenance; - $this->resetScope = $resetScope; + $this->resetScope = $resetScope; } /** @@ -145,7 +161,7 @@ public function daemon(string $connectionName, string $queue, WorkerOptions $opt if (! $this->daemonShouldRun($options, $connectionName, $queue)) { [$status, $reason] = $this->pauseWorker($options, $lastRestart); - if (! is_null($status)) { + if (null !== $status) { return $this->stop($status, $options, $reason); } @@ -158,7 +174,8 @@ public function daemon(string $connectionName, string $queue, WorkerOptions $opt // Prélèvement du prochain job, enregistrement du timeout, puis exécution. $job = $this->getNextJob( - $this->manager->driver($connectionName), $queue + $this->manager->driver($connectionName), + $queue, ); if ($supportsAsyncSignals) { @@ -184,10 +201,14 @@ public function daemon(string $connectionName, string $queue, WorkerOptions $opt // Arrêt si limite mémoire, signal de redémarrage, file vide, max jobs/temps, etc. [$status, $reason] = $this->stopIfNecessary( - $options, $lastRestart, $startTime, $jobsProcessed, $job + $options, + $lastRestart, + $startTime, + $jobsProcessed, + $job, ); - if (! is_null($status)) { + if (null !== $status) { return $this->stop($status, $options, $reason); } } @@ -202,25 +223,32 @@ protected function registerTimeoutHandler(Job $job, WorkerOptions $options): voi pcntl_signal(SIGALRM, function () use ($job, $options) { if ($job) { $this->markJobAsFailedIfWillExceedMaxAttempts( - $job->getConnectionName(), $job, (int) $options->maxTries, $e = $this->timeoutExceededException($job) + $job->getConnectionName(), + $job, + (int) $options->maxTries, + $e = $this->timeoutExceededException($job), ); $this->markJobAsFailedIfWillExceedMaxExceptions( - $job->getConnectionName(), $job, $e + $job->getConnectionName(), + $job, + $e, ); $this->markJobAsFailedIfItShouldFailOnTimeout( - $job->getConnectionName(), $job, $e + $job->getConnectionName(), + $job, + $e, ); - $this->events->jobTimeout($job->getConnectionName(), $job->getQueue(), $job); + $this->events->jobTimeout($job->getConnectionName(), $job->getQueue(), $job); } $this->kill(static::EXIT_ERROR, $options, WorkerStopReason::TimedOut); }, true); pcntl_alarm( - max($this->timeoutForJob($job, $options), 0) + max($this->timeoutForJob($job, $options), 0), ); } @@ -237,7 +265,7 @@ protected function resetTimeoutHandler(): void */ protected function timeoutForJob(Job $job, WorkerOptions $options): int { - return $job && ! is_null($job->timeout()) ? $job->timeout() : $options->timeout; + return $job && null !== $job->timeout() ? $job->timeout() : $options->timeout; } /** @@ -245,8 +273,8 @@ protected function timeoutForJob(Job $job, WorkerOptions $options): int */ protected function daemonShouldRun(WorkerOptions $options, string $connectionName, string $queue): bool { - return ! (($this->isDownForMaintenance)() && ! $options->force) || - $this->paused; + return ! (($this->isDownForMaintenance)() && ! $options->force) + || $this->paused; } /** @@ -265,14 +293,14 @@ protected function pauseWorker(WorkerOptions $options, int $lastRestart): ?array protected function stopIfNecessary(WorkerOptions $options, int $lastRestart, float|int $startTime = 0, int $jobsProcessed = 0, mixed $job = null): ?array { return match (true) { - $this->lostConnection => [static::EXIT_SUCCESS, WorkerStopReason::LostConnection], - $this->shouldQuit => [static::EXIT_SUCCESS, WorkerStopReason::Interrupted], - $this->memoryExceeded($options->memory) => [static::$memoryExceededExitCode ?? static::EXIT_MEMORY_LIMIT, WorkerStopReason::MaxMemoryExceeded], - $this->queueShouldRestart($lastRestart) => [static::EXIT_SUCCESS, WorkerStopReason::ReceivedRestartSignal], - $options->stopWhenEmpty && is_null($job) => [static::EXIT_SUCCESS, WorkerStopReason::QueueEmpty], + $this->lostConnection => [static::EXIT_SUCCESS, WorkerStopReason::LostConnection], + $this->shouldQuit => [static::EXIT_SUCCESS, WorkerStopReason::Interrupted], + $this->memoryExceeded($options->memory) => [static::$memoryExceededExitCode ?? static::EXIT_MEMORY_LIMIT, WorkerStopReason::MaxMemoryExceeded], + $this->queueShouldRestart($lastRestart) => [static::EXIT_SUCCESS, WorkerStopReason::ReceivedRestartSignal], + $options->stopWhenEmpty && null === $job => [static::EXIT_SUCCESS, WorkerStopReason::QueueEmpty], $options->maxTime && hrtime(true) / 1e9 - $startTime >= $options->maxTime => [static::EXIT_SUCCESS, WorkerStopReason::MaxTimeExceeded], - $options->maxJobs && $jobsProcessed >= $options->maxJobs => [static::EXIT_SUCCESS, WorkerStopReason::MaxJobsExceeded], - default => null + $options->maxJobs && $jobsProcessed >= $options->maxJobs => [static::EXIT_SUCCESS, WorkerStopReason::MaxJobsExceeded], + default => null, }; } @@ -282,13 +310,14 @@ protected function stopIfNecessary(WorkerOptions $options, int $lastRestart, flo public function runNextJob(string $connectionName, string $queue, WorkerOptions $options): void { $job = $this->getNextJob( - $this->manager->connection($connectionName), $queue + $this->manager->connection($connectionName), + $queue, ); // Job disponible : traitement immédiat. File vide : pause puis nouvelle tentative. if ($job) { $this->runJob($job, $connectionName, $options); - + return; } @@ -300,15 +329,13 @@ public function runNextJob(string $connectionName, string $queue, WorkerOptions */ protected function getNextJob(Queue $driver, string $queue): ?Job { - $popJobCallback = function ($queue, $index = 0) use ($driver) { - return $driver->pop($queue, $index); - }; + $popJobCallback = fn ($queue, $index = 0) => $driver->pop($queue, $index); $this->raiseBeforeJobPopEvent($driver->getConnectionName(), $queue); try { if (isset(static::$popCallbacks[$this->name ?? ''])) { - if (! is_null($job = (static::$popCallbacks[$this->name ?? ''])($popJobCallback, $queue))) { + if (null !== ($job = (static::$popCallbacks[$this->name ?? ''])($popJobCallback, $queue))) { $this->raiseAfterJobPopEvent($driver->getConnectionName(), $job); } @@ -320,14 +347,14 @@ protected function getNextJob(Queue $driver, string $queue): ?Job continue; } - if (! is_null($job = $popJobCallback($queue, $index))) { + if (null !== ($job = $popJobCallback($queue, $index))) { $this->raiseAfterJobPopEvent($driver->getConnectionName(), $job); return $job; } } } catch (Throwable $e) { - logger()->error($e->getMessage()); + logger()->error($e->getMessage()); // $this->exceptions->report($e); $this->stopWorkerIfLostConnection($e); @@ -335,7 +362,7 @@ protected function getNextJob(Queue $driver, string $queue): ?Job $this->sleep(1); } - return null; + return null; } /** @@ -359,7 +386,7 @@ protected function runJob(Job $job, string $connectionName, WorkerOptions $optio $this->process($connectionName, $job, $options); } catch (Throwable $e) { if (static::$reportJobExceptions) { - logger()->error($e->getMessage()); + logger()->error($e->getMessage()); // $this->exceptions->report($e); } @@ -372,11 +399,11 @@ protected function runJob(Job $job, string $connectionName, WorkerOptions $optio */ protected function stopWorkerIfLostConnection(Throwable $e): void { - /* + /* if ($this->causedByLostConnection($e)) { $this->lostConnection = true; } - */ + */ } /** @@ -391,12 +418,14 @@ public function process(string $connectionName, Job $job, WorkerOptions $options $this->raiseBeforeJobEvent($connectionName, $job); $this->markJobAsFailedIfAlreadyExceedsMaxAttempts( - $connectionName, $job, (int) $options->maxTries + $connectionName, + $job, + (int) $options->maxTries, ); if ($job->isDeleted()) { $this->raiseAfterJobEvent($connectionName, $job); - + return; } @@ -409,7 +438,7 @@ public function process(string $connectionName, Job $job, WorkerOptions $options $this->handleJobException($connectionName, $job, $options, $e); } finally { - $this->events->jobAttempted($connectionName, $job, $exceptionOccurred ?? null); + $this->events->jobAttempted($connectionName, $job, $exceptionOccurred ?? null); } } @@ -424,16 +453,23 @@ protected function handleJobException(string $connectionName, Job $job, WorkerOp // Marque le job en échec s'il dépassera le quota de tentatives à la prochaine exécution. if (! $job->hasFailed()) { $this->markJobAsFailedIfWillExceedMaxAttempts( - $connectionName, $job, (int) $options->maxTries, $e + $connectionName, + $job, + (int) $options->maxTries, + $e, ); $this->markJobAsFailedIfWillExceedMaxExceptions( - $connectionName, $job, $e + $connectionName, + $job, + $e, ); } $this->raiseExceptionOccurredJobEvent( - $connectionName, $job, $e + $connectionName, + $job, + $e, ); } finally { // Relâche le job dans la file pour une tentative ultérieure, puis relance l'exception. @@ -442,7 +478,7 @@ protected function handleJobException(string $connectionName, Job $job, WorkerOp $job->release($backoff); - $this->events->jobReleasedAfterException($connectionName, $job, $backoff); + $this->events->jobReleasedAfterException($connectionName, $job, $backoff); } } @@ -458,7 +494,7 @@ protected function handleJobException(string $connectionName, Job $job, WorkerOp */ protected function markJobAsFailedIfAlreadyExceedsMaxAttempts(string $connectionName, Job $job, int $maxTries): void { - $maxTries = ! is_null($job->maxTries()) ? $job->maxTries() : $maxTries; + $maxTries = null !== $job->maxTries() ? $job->maxTries() : $maxTries; $retryUntil = $job->retryUntil(); @@ -480,7 +516,7 @@ protected function markJobAsFailedIfAlreadyExceedsMaxAttempts(string $connection */ protected function markJobAsFailedIfWillExceedMaxAttempts(string $connectionName, Job $job, int $maxTries, Throwable $e): void { - $maxTries = ! is_null($job->maxTries()) ? $job->maxTries() : $maxTries; + $maxTries = null !== $job->maxTries() ? $job->maxTries() : $maxTries; if ($job->retryUntil() && $job->retryUntil() <= Date::now()->getTimestamp()) { $this->failJob($job, $e); @@ -496,17 +532,17 @@ protected function markJobAsFailedIfWillExceedMaxAttempts(string $connectionName */ protected function markJobAsFailedIfWillExceedMaxExceptions(string $connectionName, Job $job, Throwable $e): void { - if (! $this->cache || is_null($uuid = $job->uuid()) || - is_null($maxExceptions = $job->maxExceptions())) { + if (! $this->cache || null === ($uuid = $job->uuid()) + || null === ($maxExceptions = $job->maxExceptions())) { return; } - if (! $this->cache->get('job-exceptions-'.$uuid)) { - $this->cache->set('job-exceptions-'.$uuid, 0, Date::now()->addDay()->getTimestamp()); + if (! $this->cache->get('job-exceptions-' . $uuid)) { + $this->cache->set('job-exceptions-' . $uuid, 0, Date::now()->addDay()->getTimestamp()); } - if ($maxExceptions <= $this->cache->increment('job-exceptions-'.$uuid)) { - $this->cache->delete('job-exceptions-'.$uuid); + if ($maxExceptions <= $this->cache->increment('job-exceptions-' . $uuid)) { + $this->cache->delete('job-exceptions-' . $uuid); $this->failJob($job, $e); } @@ -537,9 +573,9 @@ protected function calculateBackoff(Job $job, WorkerOptions $options): int { $backoff = explode( ',', - method_exists($job, 'backoff') && ! is_null($job->backoff()) + method_exists($job, 'backoff') && null !== $job->backoff() ? $job->backoff() - : $options->backoff + : $options->backoff, ); return (int) ($backoff[$job->attempts() - 1] ?? last($backoff)); @@ -550,7 +586,7 @@ protected function calculateBackoff(Job $job, WorkerOptions $options): int */ protected function raiseWorkerStartingEvent(string $connectionName, string $queue, WorkerOptions $options): void { - $this->events->workerStarting($connectionName, $queue, $options); + $this->events->workerStarting($connectionName, $queue, $options); } /** @@ -558,7 +594,7 @@ protected function raiseWorkerStartingEvent(string $connectionName, string $queu */ protected function raiseBeforeJobPopEvent(string $connectionName, ?string $queue = null): void { - $this->events->jobPopping($connectionName, $queue); + $this->events->jobPopping($connectionName, $queue); } /** @@ -566,7 +602,7 @@ protected function raiseBeforeJobPopEvent(string $connectionName, ?string $queue */ protected function raiseAfterJobPopEvent(string $connectionName, ?Job $job): void { - $this->events->jobPopped($connectionName, $job); + $this->events->jobPopped($connectionName, $job); } /** @@ -574,7 +610,7 @@ protected function raiseAfterJobPopEvent(string $connectionName, ?Job $job): voi */ protected function raiseBeforeJobEvent(string $connectionName, Job $job): void { - $this->events->jobProcessing($connectionName, $job); + $this->events->jobProcessing($connectionName, $job); } /** @@ -582,7 +618,7 @@ protected function raiseBeforeJobEvent(string $connectionName, Job $job): void */ protected function raiseAfterJobEvent(string $connectionName, Job $job): void { - $this->events->jobProcessed($connectionName, $job); + $this->events->jobProcessed($connectionName, $job); } /** @@ -590,7 +626,7 @@ protected function raiseAfterJobEvent(string $connectionName, Job $job): void */ protected function raiseExceptionOccurredJobEvent(string $connectionName, Job $job, Throwable $e): void { - $this->events->jobExceptionOccured($connectionName, $job, $e); + $this->events->jobExceptionOccured($connectionName, $job, $e); } /** @@ -602,7 +638,7 @@ protected function queueShouldRestart(?int $lastRestart): bool return false; } - return $this->getTimestampOfLastQueueRestart() != $lastRestart; + return $this->getTimestampOfLastQueueRestart() !== $lastRestart; } /** @@ -618,7 +654,7 @@ protected function getTimestampOfLastQueueRestart(): ?int return (int) $this->cache->get('blitzphp-queue-restart'); } - return null; + return null; } /** @@ -656,7 +692,7 @@ public function memoryExceeded(int $memoryLimit): bool */ public function stop(int $status = 0, ?WorkerOptions $options = null, ?WorkerStopReason $reason = null): int { - $this->events->workerStopping($this->manager->getName(), $status, $options, $reason); + $this->events->workerStopping($this->manager->getName(), $status, $options, $reason); return $status; } @@ -666,7 +702,7 @@ public function stop(int $status = 0, ?WorkerOptions $options = null, ?WorkerSto */ public function kill(int $status = 0, ?WorkerOptions $options = null, ?WorkerStopReason $reason = null): never { - $status = $this->stop($status, $options, $reason); + $status = $this->stop($status, $options, $reason); if (extension_loaded('posix')) { posix_kill(getmypid(), SIGKILL); @@ -694,7 +730,7 @@ protected function timeoutExceededException(Job $job): TimeoutExceededException /** * Met le script en pause pendant un nombre de secondes donné. */ - public function sleep(int|float $seconds): void + public function sleep(float|int $seconds): void { if ($seconds < 1) { usleep($seconds * 1_000_000); @@ -728,7 +764,7 @@ public function setName(string $name): self */ public static function popUsing(string $workerName, callable $callback): void { - if (is_null($callback)) { + if (null === $callback) { unset(static::$popCallbacks[$workerName]); } else { static::$popCallbacks[$workerName] = $callback; diff --git a/src/WorkerOptions.php b/src/WorkerOptions.php index 51e28dd..7822f8b 100644 --- a/src/WorkerOptions.php +++ b/src/WorkerOptions.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace BlitzPHP\Queue; /** @@ -12,19 +21,19 @@ class WorkerOptions /** * Crée une instance d'options du worker. * - * @param string $name Nom du worker (utilisé pour les callbacks de pop personnalisés). - * @param int|int[] $backoff Secondes d'attente avant de relancer un job ayant levé une exception non gérée. - * @param int $memory Mémoire maximale autorisée (Mo) avant arrêt du worker. - * @param int $timeout Durée maximale d'exécution d'un job enfant (secondes). - * @param int $sleep Secondes d'attente entre deux sondages lorsque la file est vide. - * @param int $maxTries Nombre maximal de tentatives par job. - * @param bool $force Si `true`, le worker tourne même en mode maintenance. - * @param bool $stopWhenEmpty Si `true`, le worker s'arrête dès que la file est vide. - * @param int $maxJobs Nombre maximal de jobs à traiter (0 = illimité). - * @param int $maxTime Durée de vie maximale du worker en secondes (0 = illimitée). - * @param int $rest Secondes de pause entre deux jobs traités avec succès. + * @param string $name Nom du worker (utilisé pour les callbacks de pop personnalisés). + * @param int|list $backoff Secondes d'attente avant de relancer un job ayant levé une exception non gérée. + * @param int $memory Mémoire maximale autorisée (Mo) avant arrêt du worker. + * @param int $timeout Durée maximale d'exécution d'un job enfant (secondes). + * @param int $sleep Secondes d'attente entre deux sondages lorsque la file est vide. + * @param int $maxTries Nombre maximal de tentatives par job. + * @param bool $force Si `true`, le worker tourne même en mode maintenance. + * @param bool $stopWhenEmpty Si `true`, le worker s'arrête dès que la file est vide. + * @param int $maxJobs Nombre maximal de jobs à traiter (0 = illimité). + * @param int $maxTime Durée de vie maximale du worker en secondes (0 = illimitée). + * @param int $rest Secondes de pause entre deux jobs traités avec succès. */ - public function __construct( + public function __construct( public string $name = 'default', public array|int $backoff = 0, public int $memory = 128, @@ -37,5 +46,5 @@ public function __construct( public int $maxTime = 0, public $rest = 0, ) { - } + } } From 0d3306e8dd68ce06b7a84c961384f79248e3d99b Mon Sep 17 00:00:00 2001 From: dimtrovich <37987162+dimtrovich@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:56:03 +0000 Subject: [PATCH 5/5] Fix styling --- src/Events/QueueEventManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Events/QueueEventManager.php b/src/Events/QueueEventManager.php index 422c550..240116d 100644 --- a/src/Events/QueueEventManager.php +++ b/src/Events/QueueEventManager.php @@ -27,8 +27,8 @@ class QueueEventManager /** * Noms d'événements des opérations de file. */ + public const JOB_POPPING = 'queue.job.popping'; - public const JOB_POPPING = 'queue.job.popping'; public const JOB_POPPED = 'queue.job.popped'; public const JOB_PUSHED = 'queue.job.pushed'; public const JOB_PUSH_FAILED = 'queue.job.push.failed';