├── ManuallyFailedException.php ├── Attributes ├── DeleteWhenMissingModels.php └── WithoutRelations.php ├── Connectors ├── ConnectorInterface.php ├── NullConnector.php ├── DeferredConnector.php ├── BackgroundConnector.php ├── SyncConnector.php ├── FailoverConnector.php ├── DatabaseConnector.php ├── BeanstalkdConnector.php ├── RedisConnector.php └── SqsConnector.php ├── Events ├── JobPopping.php ├── Looping.php ├── JobPopped.php ├── JobProcessed.php ├── JobTimedOut.php ├── JobProcessing.php ├── JobReleasedAfterException.php ├── WorkerStopping.php ├── WorkerStarting.php ├── QueueBusy.php ├── JobExceptionOccurred.php ├── JobFailed.php ├── QueueFailedOver.php ├── JobRetryRequested.php ├── JobAttempted.php ├── JobQueueing.php └── JobQueued.php ├── Failed ├── CountableFailedJobProvider.php ├── PrunableFailedJobProvider.php ├── FailedJobProviderInterface.php ├── NullFailedJobProvider.php ├── DatabaseFailedJobProvider.php ├── DatabaseUuidFailedJobProvider.php ├── DynamoDbFailedJobProvider.php └── FileFailedJobProvider.php ├── Middleware ├── SkipIfBatchCancelled.php ├── Skip.php ├── FailOnException.php ├── ThrottlesExceptionsWithRedis.php ├── RateLimitedWithRedis.php ├── WithoutOverlapping.php ├── RateLimited.php └── ThrottlesExceptions.php ├── DeferredQueue.php ├── TimeoutExceededException.php ├── Console ├── Concerns │ └── ParsesQueue.php ├── stubs │ ├── failed_jobs.stub │ ├── jobs.stub │ └── batches.stub ├── ForgetFailedCommand.php ├── FlushFailedCommand.php ├── PauseCommand.php ├── ResumeCommand.php ├── PruneFailedJobsCommand.php ├── RestartCommand.php ├── TableCommand.php ├── FailedTableCommand.php ├── BatchesTableCommand.php ├── PruneBatchesCommand.php ├── RetryBatchCommand.php ├── ClearCommand.php ├── ListFailedCommand.php ├── ListenCommand.php ├── MonitorCommand.php └── RetryCommand.php ├── InvalidPayloadException.php ├── BackgroundQueue.php ├── MaxAttemptsExceededException.php ├── ListenerOptions.php ├── LICENSE.md ├── Jobs ├── JobName.php ├── DatabaseJobRecord.php ├── SyncJob.php ├── FakeJob.php ├── DatabaseJob.php ├── SqsJob.php ├── RedisJob.php └── BeanstalkdJob.php ├── README.md ├── composer.json ├── NullQueue.php ├── WorkerOptions.php ├── SerializesModels.php ├── CallQueuedClosure.php ├── SerializesAndRestoresModelIdentifiers.php ├── LuaScripts.php ├── FailoverQueue.php ├── Capsule └── Manager.php ├── Listener.php ├── SyncQueue.php ├── InteractsWithQueue.php └── BeanstalkdQueue.php /ManuallyFailedException.php: -------------------------------------------------------------------------------- 1 | batch()?->cancelled()) { 17 | return; 18 | } 19 | 20 | $next($job); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /DeferredQueue.php: -------------------------------------------------------------------------------- 1 | parent::push($job, $data, $queue)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /TimeoutExceededException.php: -------------------------------------------------------------------------------- 1 | resolveName().' has timed out.'), function ($e) use ($job) { 16 | $e->job = $job; 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Events/JobExceptionOccurred.php: -------------------------------------------------------------------------------- 1 | laravel['config']['queue.default'], 19 | $queue ?: 'default', 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Events/QueueFailedOver.php: -------------------------------------------------------------------------------- 1 | value = $value; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /BackgroundQueue.php: -------------------------------------------------------------------------------- 1 | defer( 22 | fn () => \Illuminate\Support\Facades\Queue::connection('sync')->push($job, $data, $queue) 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MaxAttemptsExceededException.php: -------------------------------------------------------------------------------- 1 | resolveName().' has been attempted too many times.'), function ($e) use ($job) { 25 | $e->job = $job; 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Events/JobRetryRequested.php: -------------------------------------------------------------------------------- 1 | payload)) { 32 | $this->payload = json_decode($this->job->payload, true); 33 | } 34 | 35 | return $this->payload; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Connectors/FailoverConnector.php: -------------------------------------------------------------------------------- 1 | manager, 29 | $this->events, 30 | $config['connections'], 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Events/JobAttempted.php: -------------------------------------------------------------------------------- 1 | job->hasFailed() && is_null($this->exception); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Console/stubs/failed_jobs.stub: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('uuid')->unique(); 17 | $table->text('connection'); 18 | $table->text('queue'); 19 | $table->longText('payload'); 20 | $table->longText('exception'); 21 | $table->timestamp('failed_at')->useCurrent(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('{{table}}'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /Events/JobQueueing.php: -------------------------------------------------------------------------------- 1 | payload, true, flags: JSON_THROW_ON_ERROR); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Console/stubs/jobs.stub: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 16 | $table->string('queue')->index(); 17 | $table->longText('payload'); 18 | $table->unsignedTinyInteger('attempts'); 19 | $table->unsignedInteger('reserved_at')->nullable(); 20 | $table->unsignedInteger('available_at'); 21 | $table->unsignedInteger('created_at'); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('{{table}}'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /Events/JobQueued.php: -------------------------------------------------------------------------------- 1 | payload, true, flags: JSON_THROW_ON_ERROR); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ListenerOptions.php: -------------------------------------------------------------------------------- 1 | environment = $environment; 30 | 31 | parent::__construct($name, $backoff, $memory, $timeout, $sleep, $maxTries, $force, false, 0, 0, $rest); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Middleware/Skip.php: -------------------------------------------------------------------------------- 1 | skip) { 39 | return false; 40 | } 41 | 42 | return $next($job); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Console/ForgetFailedCommand.php: -------------------------------------------------------------------------------- 1 | laravel['queue.failer']->forget($this->argument('id'))) { 33 | $this->components->info('Failed job deleted successfully.'); 34 | } else { 35 | $this->components->error('No failed job matches the given ID.'); 36 | 37 | return 1; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Console/stubs/batches.stub: -------------------------------------------------------------------------------- 1 | string('id')->primary(); 16 | $table->string('name'); 17 | $table->integer('total_jobs'); 18 | $table->integer('pending_jobs'); 19 | $table->integer('failed_jobs'); 20 | $table->longText('failed_job_ids'); 21 | $table->mediumText('options')->nullable(); 22 | $table->integer('cancelled_at')->nullable(); 23 | $table->integer('created_at'); 24 | $table->integer('finished_at')->nullable(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | */ 31 | public function down(): void 32 | { 33 | Schema::dropIfExists('{{table}}'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Taylor Otwell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Console/FlushFailedCommand.php: -------------------------------------------------------------------------------- 1 | laravel['queue.failer']->flush($this->option('hours')); 33 | 34 | if ($this->option('hours')) { 35 | $this->components->info("All jobs that failed more than {$this->option('hours')} hours ago have been deleted successfully."); 36 | 37 | return; 38 | } 39 | 40 | $this->components->info('All failed jobs deleted successfully.'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Console/PauseCommand.php: -------------------------------------------------------------------------------- 1 | parseQueue($this->argument('queue')); 37 | 38 | $manager->pause($connection, $queue); 39 | 40 | $this->components->info("Job processing on queue [{$connection}:{$queue}] has been paused."); 41 | 42 | return 0; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Connectors/DatabaseConnector.php: -------------------------------------------------------------------------------- 1 | connections = $connections; 25 | } 26 | 27 | /** 28 | * Establish a queue connection. 29 | * 30 | * @param array $config 31 | * @return \Illuminate\Contracts\Queue\Queue 32 | */ 33 | public function connect(array $config) 34 | { 35 | return new DatabaseQueue( 36 | $this->connections->connection($config['connection'] ?? null), 37 | $config['table'], 38 | $config['queue'], 39 | $config['retry_after'] ?? 60, 40 | $config['after_commit'] ?? null 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Jobs/JobName.php: -------------------------------------------------------------------------------- 1 | $payload 41 | * @return string 42 | */ 43 | public static function resolveClassName($name, $payload) 44 | { 45 | if (is_string($payload['data']['commandName'] ?? null)) { 46 | return $payload['data']['commandName']; 47 | } 48 | 49 | return $name; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Connectors/BeanstalkdConnector.php: -------------------------------------------------------------------------------- 1 | pheanstalk($config), 22 | $config['queue'], 23 | $config['retry_after'] ?? Pheanstalk::DEFAULT_TTR, 24 | $config['block_for'] ?? 0, 25 | $config['after_commit'] ?? null 26 | ); 27 | } 28 | 29 | /** 30 | * Create a Pheanstalk instance. 31 | * 32 | * @param array $config 33 | * @return \Pheanstalk\Pheanstalk 34 | */ 35 | protected function pheanstalk(array $config) 36 | { 37 | return Pheanstalk::create( 38 | $config['host'], 39 | $config['port'] ?? SocketFactoryInterface::DEFAULT_PORT, 40 | isset($config['timeout']) ? new Timeout($config['timeout']) : null, 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Illuminate Queue 2 | 3 | The Laravel Queue component provides a unified API across a variety of different queue services. Queues allow you to defer the processing of a time consuming task, such as sending an e-mail, until a later time, thus drastically speeding up the web requests to your application. 4 | 5 | ### Usage Instructions 6 | 7 | First, create a new Queue `Capsule` manager instance. Similar to the "Capsule" provided for the Eloquent ORM, the queue Capsule aims to make configuring the library for usage outside of the Laravel framework as easy as possible. 8 | 9 | ```PHP 10 | use Illuminate\Queue\Capsule\Manager as Queue; 11 | 12 | $queue = new Queue; 13 | 14 | $queue->addConnection([ 15 | 'driver' => 'beanstalkd', 16 | 'host' => 'localhost', 17 | 'queue' => 'default', 18 | ]); 19 | 20 | // Make this Capsule instance available globally via static methods... (optional) 21 | $queue->setAsGlobal(); 22 | ``` 23 | 24 | Once the Capsule instance has been registered. You may use it like so: 25 | 26 | ```PHP 27 | // As an instance... 28 | $queue->push('SendEmail', ['message' => $message]); 29 | 30 | // If setAsGlobal has been called... 31 | Queue::push('SendEmail', ['message' => $message]); 32 | ``` 33 | 34 | For further documentation on using the queue, consult the [Laravel framework documentation](https://laravel.com/docs). 35 | -------------------------------------------------------------------------------- /Failed/FailedJobProviderInterface.php: -------------------------------------------------------------------------------- 1 | record = $record; 26 | } 27 | 28 | /** 29 | * Increment the number of times the job has been attempted. 30 | * 31 | * @return int 32 | */ 33 | public function increment() 34 | { 35 | $this->record->attempts++; 36 | 37 | return $this->record->attempts; 38 | } 39 | 40 | /** 41 | * Update the "reserved at" timestamp of the job. 42 | * 43 | * @return int 44 | */ 45 | public function touch() 46 | { 47 | $this->record->reserved_at = $this->currentTime(); 48 | 49 | return $this->record->reserved_at; 50 | } 51 | 52 | /** 53 | * Dynamically access the underlying job information. 54 | * 55 | * @param string $key 56 | * @return mixed 57 | */ 58 | public function __get($key) 59 | { 60 | return $this->record->{$key}; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Console/ResumeCommand.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | protected $aliases = ['queue:continue']; 28 | 29 | /** 30 | * The console command description. 31 | * 32 | * @var string 33 | */ 34 | protected $description = 'Resume job processing for a paused queue'; 35 | 36 | /** 37 | * Execute the console command. 38 | * 39 | * @return int 40 | */ 41 | public function handle(QueueManager $manager) 42 | { 43 | [$connection, $queue] = $this->parseQueue($this->argument('queue')); 44 | 45 | $manager->resume($connection, $queue); 46 | 47 | $this->components->info("Job processing on queue [{$connection}:{$queue}] has been resumed."); 48 | 49 | return 0; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Console/PruneFailedJobsCommand.php: -------------------------------------------------------------------------------- 1 | laravel['queue.failer']; 36 | 37 | if ($failer instanceof PrunableFailedJobProvider) { 38 | $count = $failer->prune(Carbon::now()->subHours($this->option('hours'))); 39 | } else { 40 | $this->components->error('The ['.class_basename($failer).'] failed job storage driver does not support pruning.'); 41 | 42 | return 1; 43 | } 44 | 45 | $this->components->info("{$count} entries deleted."); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Connectors/RedisConnector.php: -------------------------------------------------------------------------------- 1 | redis = $redis; 33 | $this->connection = $connection; 34 | } 35 | 36 | /** 37 | * Establish a queue connection. 38 | * 39 | * @param array $config 40 | * @return \Illuminate\Contracts\Queue\Queue 41 | */ 42 | public function connect(array $config) 43 | { 44 | return new RedisQueue( 45 | $this->redis, $config['queue'], 46 | $config['connection'] ?? $this->connection, 47 | $config['retry_after'] ?? 60, 48 | $config['block_for'] ?? null, 49 | $config['after_commit'] ?? null, 50 | $config['migration_batch_size'] ?? -1 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Console/RestartCommand.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 46 | } 47 | 48 | /** 49 | * Execute the console command. 50 | * 51 | * @return void 52 | */ 53 | public function handle() 54 | { 55 | $this->cache->forever('illuminate:queue:restart', $this->currentTime()); 56 | 57 | $this->components->info('Broadcasting queue restart signal.'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Connectors/SqsConnector.php: -------------------------------------------------------------------------------- 1 | getDefaultConfiguration($config); 20 | 21 | if (! empty($config['key']) && ! empty($config['secret'])) { 22 | $config['credentials'] = Arr::only($config, ['key', 'secret']); 23 | 24 | if (! empty($config['token'])) { 25 | $config['credentials']['token'] = $config['token']; 26 | } 27 | } 28 | 29 | return new SqsQueue( 30 | new SqsClient( 31 | Arr::except($config, ['token']) 32 | ), 33 | $config['queue'], 34 | $config['prefix'] ?? '', 35 | $config['suffix'] ?? '', 36 | $config['after_commit'] ?? null 37 | ); 38 | } 39 | 40 | /** 41 | * Get the default configuration for SQS. 42 | * 43 | * @param array $config 44 | * @return array 45 | */ 46 | protected function getDefaultConfiguration(array $config) 47 | { 48 | return array_merge([ 49 | 'version' => 'latest', 50 | 'http' => [ 51 | 'timeout' => 60, 52 | 'connect_timeout' => 60, 53 | ], 54 | ], $config); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Failed/NullFailedJobProvider.php: -------------------------------------------------------------------------------- 1 | > $callback 21 | */ 22 | public function __construct($callback) 23 | { 24 | if (is_array($callback)) { 25 | $callback = $this->failForExceptions($callback); 26 | } 27 | 28 | $this->callback = $callback; 29 | } 30 | 31 | /** 32 | * Indicate that the job should fail if it encounters the given exceptions. 33 | * 34 | * @param array> $exceptions 35 | * @return \Closure(\Throwable, mixed): bool 36 | */ 37 | protected function failForExceptions(array $exceptions) 38 | { 39 | return static function (Throwable $throwable) use ($exceptions) { 40 | foreach ($exceptions as $exception) { 41 | if ($throwable instanceof $exception) { 42 | return true; 43 | } 44 | } 45 | 46 | return false; 47 | }; 48 | } 49 | 50 | /** 51 | * Mark the job as failed if an exception is thrown that passes a truth-test callback. 52 | * 53 | * @param mixed $job 54 | * @param callable $next 55 | * @return mixed 56 | * 57 | * @throws Throwable 58 | */ 59 | public function handle($job, callable $next) 60 | { 61 | try { 62 | return $next($job); 63 | } catch (Throwable $e) { 64 | if (call_user_func($this->callback, $e, $job) === true) { 65 | $job->fail($e); 66 | } 67 | 68 | throw $e; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "illuminate/queue", 3 | "description": "The Illuminate Queue package.", 4 | "license": "MIT", 5 | "homepage": "https://laravel.com", 6 | "support": { 7 | "issues": "https://github.com/laravel/framework/issues", 8 | "source": "https://github.com/laravel/framework" 9 | }, 10 | "authors": [ 11 | { 12 | "name": "Taylor Otwell", 13 | "email": "taylor@laravel.com" 14 | } 15 | ], 16 | "require": { 17 | "php": "^8.3", 18 | "illuminate/collections": "^13.0", 19 | "illuminate/console": "^13.0", 20 | "illuminate/container": "^13.0", 21 | "illuminate/contracts": "^13.0", 22 | "illuminate/database": "^13.0", 23 | "illuminate/filesystem": "^13.0", 24 | "illuminate/pipeline": "^13.0", 25 | "illuminate/support": "^13.0", 26 | "laravel/serializable-closure": "^1.3|^2.0", 27 | "ramsey/uuid": "^4.7", 28 | "symfony/process": "^7.4.0|^8.0.0" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Illuminate\\Queue\\": "" 33 | } 34 | }, 35 | "extra": { 36 | "branch-alias": { 37 | "dev-master": "13.0.x-dev" 38 | } 39 | }, 40 | "suggest": { 41 | "ext-pdo": "Required to use the database queue worker.", 42 | "ext-filter": "Required to use the SQS queue worker.", 43 | "ext-mbstring": "Required to use the database failed job providers.", 44 | "ext-pcntl": "Required to use all features of the queue worker.", 45 | "ext-posix": "Required to use all features of the queue worker.", 46 | "aws/aws-sdk-php": "Required to use the SQS queue driver and DynamoDb failed job storage (^3.322.9).", 47 | "illuminate/redis": "Required to use the Redis queue driver (^13.0).", 48 | "pda/pheanstalk": "Required to use the Beanstalk queue driver (^5.0.6|^7.0.0)." 49 | }, 50 | "config": { 51 | "sort-packages": true 52 | }, 53 | "minimum-stability": "dev" 54 | } 55 | -------------------------------------------------------------------------------- /Jobs/SyncJob.php: -------------------------------------------------------------------------------- 1 | queue = $queue; 35 | $this->payload = $payload; 36 | $this->container = $container; 37 | $this->connectionName = $connectionName; 38 | } 39 | 40 | /** 41 | * Release the job back into the queue after (n) seconds. 42 | * 43 | * @param int $delay 44 | * @return void 45 | */ 46 | public function release($delay = 0) 47 | { 48 | parent::release($delay); 49 | } 50 | 51 | /** 52 | * Get the number of times the job has been attempted. 53 | * 54 | * @return int 55 | */ 56 | public function attempts() 57 | { 58 | return 1; 59 | } 60 | 61 | /** 62 | * Get the job identifier. 63 | * 64 | * @return string 65 | */ 66 | public function getJobId() 67 | { 68 | return ''; 69 | } 70 | 71 | /** 72 | * Get the raw body string for the job. 73 | * 74 | * @return string 75 | */ 76 | public function getRawBody() 77 | { 78 | return $this->payload; 79 | } 80 | 81 | /** 82 | * Get the name of the queue the job belongs to. 83 | * 84 | * @return string 85 | */ 86 | public function getQueue() 87 | { 88 | return 'sync'; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Console/TableCommand.php: -------------------------------------------------------------------------------- 1 | laravel['config']['queue.connections.database.table']; 42 | } 43 | 44 | /** 45 | * Get the path to the migration stub file. 46 | * 47 | * @return string 48 | */ 49 | protected function migrationStubFile() 50 | { 51 | return __DIR__.'/stubs/jobs.stub'; 52 | } 53 | 54 | /** 55 | * Determine whether a migration for the table already exists. 56 | * 57 | * @param string $table 58 | * @return bool 59 | */ 60 | protected function migrationExists($table) 61 | { 62 | if ($table !== 'jobs') { 63 | return parent::migrationExists($table); 64 | } 65 | 66 | foreach ([ 67 | join_paths($this->laravel->databasePath('migrations'), '*_*_*_*_create_'.$table.'_table.php'), 68 | join_paths($this->laravel->databasePath('migrations'), '0001_01_01_000002_create_jobs_table.php'), 69 | ] as $path) { 70 | if (count($this->files->glob($path)) !== 0) { 71 | return true; 72 | } 73 | } 74 | 75 | return false; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Jobs/FakeJob.php: -------------------------------------------------------------------------------- 1 | (string) Str::uuid()); 39 | } 40 | 41 | /** 42 | * Get the raw body of the job. 43 | * 44 | * @return string 45 | */ 46 | public function getRawBody() 47 | { 48 | return ''; 49 | } 50 | 51 | /** 52 | * Release the job back into the queue after (n) seconds. 53 | * 54 | * @param \DateTimeInterface|\DateInterval|int $delay 55 | * @return void 56 | */ 57 | public function release($delay = 0) 58 | { 59 | $this->released = true; 60 | $this->releaseDelay = $delay; 61 | } 62 | 63 | /** 64 | * Get the number of times the job has been attempted. 65 | * 66 | * @return int 67 | */ 68 | public function attempts() 69 | { 70 | return $this->attempts; 71 | } 72 | 73 | /** 74 | * Delete the job from the queue. 75 | * 76 | * @return void 77 | */ 78 | public function delete() 79 | { 80 | $this->deleted = true; 81 | } 82 | 83 | /** 84 | * Delete the job, call the "failed" method, and raise the failed job event. 85 | * 86 | * @param \Throwable|null $exception 87 | * @return void 88 | */ 89 | public function fail($exception = null) 90 | { 91 | $this->failed = true; 92 | $this->failedWith = $exception; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Console/FailedTableCommand.php: -------------------------------------------------------------------------------- 1 | laravel['config']['queue.failed.table']; 42 | } 43 | 44 | /** 45 | * Get the path to the migration stub file. 46 | * 47 | * @return string 48 | */ 49 | protected function migrationStubFile() 50 | { 51 | return __DIR__.'/stubs/failed_jobs.stub'; 52 | } 53 | 54 | /** 55 | * Determine whether a migration for the table already exists. 56 | * 57 | * @param string $table 58 | * @return bool 59 | */ 60 | protected function migrationExists($table) 61 | { 62 | if ($table !== 'failed_jobs') { 63 | return parent::migrationExists($table); 64 | } 65 | 66 | foreach ([ 67 | join_paths($this->laravel->databasePath('migrations'), '*_*_*_*_create_'.$table.'_table.php'), 68 | join_paths($this->laravel->databasePath('migrations'), '0001_01_01_000002_create_jobs_table.php'), 69 | ] as $path) { 70 | if (count($this->files->glob($path)) !== 0) { 71 | return true; 72 | } 73 | } 74 | 75 | return false; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Console/BatchesTableCommand.php: -------------------------------------------------------------------------------- 1 | laravel['config']['queue.batching.table'] ?? 'job_batches'; 42 | } 43 | 44 | /** 45 | * Get the path to the migration stub file. 46 | * 47 | * @return string 48 | */ 49 | protected function migrationStubFile() 50 | { 51 | return __DIR__.'/stubs/batches.stub'; 52 | } 53 | 54 | /** 55 | * Determine whether a migration for the table already exists. 56 | * 57 | * @param string $table 58 | * @return bool 59 | */ 60 | protected function migrationExists($table) 61 | { 62 | if ($table !== 'job_batches') { 63 | return parent::migrationExists($table); 64 | } 65 | 66 | foreach ([ 67 | join_paths($this->laravel->databasePath('migrations'), '*_*_*_*_create_'.$table.'_table.php'), 68 | join_paths($this->laravel->databasePath('migrations'), '0001_01_01_000002_create_jobs_table.php'), 69 | ] as $path) { 70 | if (count($this->files->glob($path)) !== 0) { 71 | return true; 72 | } 73 | } 74 | 75 | return false; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Middleware/ThrottlesExceptionsWithRedis.php: -------------------------------------------------------------------------------- 1 | redis = Container::getInstance()->make(Redis::class); 39 | 40 | $this->limiter = new DurationLimiter( 41 | $this->redis, $this->getKey($job), $this->maxAttempts, $this->decaySeconds 42 | ); 43 | 44 | if ($this->limiter->tooManyAttempts()) { 45 | return $job->release($this->limiter->decaysAt - $this->currentTime()); 46 | } 47 | 48 | try { 49 | $next($job); 50 | 51 | $this->limiter->clear(); 52 | } catch (Throwable $throwable) { 53 | if ($this->whenCallback && ! call_user_func($this->whenCallback, $throwable, $this->limiter)) { 54 | throw $throwable; 55 | } 56 | 57 | if ($this->reportCallback && call_user_func($this->reportCallback, $throwable, $this->limiter)) { 58 | report($throwable); 59 | } 60 | 61 | if ($this->shouldDelete($throwable)) { 62 | return $job->delete(); 63 | } 64 | 65 | if ($this->shouldFail($throwable)) { 66 | return $job->fail($throwable); 67 | } 68 | 69 | $this->limiter->acquire(); 70 | 71 | return $job->release($this->retryAfterMinutes * 60); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Console/PruneBatchesCommand.php: -------------------------------------------------------------------------------- 1 | laravel[BatchRepository::class]; 40 | 41 | $count = 0; 42 | 43 | if ($repository instanceof PrunableBatchRepository) { 44 | $count = $repository->prune(Carbon::now()->subHours($this->option('hours'))); 45 | } 46 | 47 | $this->components->info("{$count} entries deleted."); 48 | 49 | if ($this->option('unfinished') !== null) { 50 | $count = 0; 51 | 52 | if ($repository instanceof DatabaseBatchRepository) { 53 | $count = $repository->pruneUnfinished(Carbon::now()->subHours($this->option('unfinished'))); 54 | } 55 | 56 | $this->components->info("{$count} unfinished entries deleted."); 57 | } 58 | 59 | if ($this->option('cancelled') !== null) { 60 | $count = 0; 61 | 62 | if ($repository instanceof DatabaseBatchRepository) { 63 | $count = $repository->pruneCancelled(Carbon::now()->subHours($this->option('cancelled'))); 64 | } 65 | 66 | $this->components->info("{$count} cancelled entries deleted."); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Console/RetryBatchCommand.php: -------------------------------------------------------------------------------- 1 | getBatchJobIds()) > 0; 36 | 37 | if ($batchesFound) { 38 | $this->components->info('Pushing failed batch jobs back onto the queue.'); 39 | } 40 | 41 | foreach ($ids as $batchId) { 42 | $batch = $this->laravel[BatchRepository::class]->find($batchId); 43 | 44 | if (! $batch) { 45 | $this->components->error("Unable to find a batch with ID [{$batchId}]."); 46 | } elseif (empty($batch->failedJobIds)) { 47 | $this->components->error('The given batch does not contain any failed jobs.'); 48 | } 49 | 50 | $this->components->info("Pushing failed queue jobs of the batch [$batchId] back onto the queue."); 51 | 52 | foreach ($batch->failedJobIds as $failedJobId) { 53 | $this->components->task( 54 | $failedJobId, 55 | fn () => $this->callSilent('queue:retry', ['id' => $failedJobId]) == 0 56 | ); 57 | } 58 | 59 | $this->newLine(); 60 | } 61 | } 62 | 63 | /** 64 | * Get the custom mutex name for an isolated command. 65 | * 66 | * @return string 67 | */ 68 | public function isolatableId() 69 | { 70 | return $this->argument('id'); 71 | } 72 | 73 | /** 74 | * Get the batch IDs to be retried. 75 | * 76 | * @return array 77 | */ 78 | protected function getBatchJobIds() 79 | { 80 | $ids = (array) $this->argument('id'); 81 | 82 | return array_values(array_filter(array_unique($ids))); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Jobs/DatabaseJob.php: -------------------------------------------------------------------------------- 1 | job = $job; 37 | $this->queue = $queue; 38 | $this->database = $database; 39 | $this->container = $container; 40 | $this->connectionName = $connectionName; 41 | } 42 | 43 | /** 44 | * Release the job back into the queue after (n) seconds. 45 | * 46 | * @param int $delay 47 | * @return void 48 | */ 49 | public function release($delay = 0) 50 | { 51 | parent::release($delay); 52 | 53 | $this->database->deleteAndRelease($this->queue, $this, $delay); 54 | } 55 | 56 | /** 57 | * Delete the job from the queue. 58 | * 59 | * @return void 60 | */ 61 | public function delete() 62 | { 63 | parent::delete(); 64 | 65 | $this->database->deleteReserved($this->queue, $this->job->id); 66 | } 67 | 68 | /** 69 | * Get the number of times the job has been attempted. 70 | * 71 | * @return int 72 | */ 73 | public function attempts() 74 | { 75 | return (int) $this->job->attempts; 76 | } 77 | 78 | /** 79 | * Get the job identifier. 80 | * 81 | * @return string|int 82 | */ 83 | public function getJobId() 84 | { 85 | return $this->job->id; 86 | } 87 | 88 | /** 89 | * Get the raw body string for the job. 90 | * 91 | * @return string 92 | */ 93 | public function getRawBody() 94 | { 95 | return $this->job->payload; 96 | } 97 | 98 | /** 99 | * Get the database job record. 100 | * 101 | * @return \Illuminate\Queue\Jobs\DatabaseJobRecord 102 | */ 103 | public function getJobRecord() 104 | { 105 | return $this->job; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /NullQueue.php: -------------------------------------------------------------------------------- 1 | redis = Container::getInstance()->make(Redis::class); 38 | } 39 | 40 | /** 41 | * Handle a rate limited job. 42 | * 43 | * @param mixed $job 44 | * @param callable $next 45 | * @param array $limits 46 | * @return mixed 47 | */ 48 | protected function handleJob($job, $next, array $limits) 49 | { 50 | foreach ($limits as $limit) { 51 | if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decaySeconds)) { 52 | return $this->shouldRelease 53 | ? $job->release($this->releaseAfter ?: $this->getTimeUntilNextRetry($limit->key)) 54 | : false; 55 | } 56 | } 57 | 58 | return $next($job); 59 | } 60 | 61 | /** 62 | * Determine if the given key has been "accessed" too many times. 63 | * 64 | * @param string $key 65 | * @param int $maxAttempts 66 | * @param int $decaySeconds 67 | * @return bool 68 | */ 69 | protected function tooManyAttempts($key, $maxAttempts, $decaySeconds) 70 | { 71 | $limiter = new DurationLimiter( 72 | $this->redis, $key, $maxAttempts, $decaySeconds 73 | ); 74 | 75 | return tap(! $limiter->acquire(), function () use ($key, $limiter) { 76 | $this->decaysAt[$key] = $limiter->decaysAt; 77 | }); 78 | } 79 | 80 | /** 81 | * Get the number of seconds that should elapse before the job is retried. 82 | * 83 | * @param string $key 84 | * @return int 85 | */ 86 | protected function getTimeUntilNextRetry($key) 87 | { 88 | return ($this->decaysAt[$key] - $this->currentTime()) + 3; 89 | } 90 | 91 | /** 92 | * Prepare the object after unserialization. 93 | * 94 | * @return void 95 | */ 96 | public function __wakeup() 97 | { 98 | parent::__wakeup(); 99 | 100 | $this->redis = Container::getInstance()->make(Redis::class); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /WorkerOptions.php: -------------------------------------------------------------------------------- 1 | name = $name; 113 | $this->backoff = $backoff; 114 | $this->sleep = $sleep; 115 | $this->rest = $rest; 116 | $this->force = $force; 117 | $this->memory = $memory; 118 | $this->timeout = $timeout; 119 | $this->maxTries = $maxTries; 120 | $this->stopWhenEmpty = $stopWhenEmpty; 121 | $this->maxJobs = $maxJobs; 122 | $this->maxTime = $maxTime; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Jobs/SqsJob.php: -------------------------------------------------------------------------------- 1 | sqs = $sqs; 37 | $this->job = $job; 38 | $this->queue = $queue; 39 | $this->container = $container; 40 | $this->connectionName = $connectionName; 41 | } 42 | 43 | /** 44 | * Release the job back into the queue after (n) seconds. 45 | * 46 | * @param int $delay 47 | * @return void 48 | */ 49 | public function release($delay = 0) 50 | { 51 | parent::release($delay); 52 | 53 | $this->sqs->changeMessageVisibility([ 54 | 'QueueUrl' => $this->queue, 55 | 'ReceiptHandle' => $this->job['ReceiptHandle'], 56 | 'VisibilityTimeout' => $delay, 57 | ]); 58 | } 59 | 60 | /** 61 | * Delete the job from the queue. 62 | * 63 | * @return void 64 | */ 65 | public function delete() 66 | { 67 | parent::delete(); 68 | 69 | $this->sqs->deleteMessage([ 70 | 'QueueUrl' => $this->queue, 'ReceiptHandle' => $this->job['ReceiptHandle'], 71 | ]); 72 | } 73 | 74 | /** 75 | * Get the number of times the job has been attempted. 76 | * 77 | * @return int 78 | */ 79 | public function attempts() 80 | { 81 | return (int) $this->job['Attributes']['ApproximateReceiveCount']; 82 | } 83 | 84 | /** 85 | * Get the job identifier. 86 | * 87 | * @return string 88 | */ 89 | public function getJobId() 90 | { 91 | return $this->job['MessageId']; 92 | } 93 | 94 | /** 95 | * Get the raw body string for the job. 96 | * 97 | * @return string 98 | */ 99 | public function getRawBody() 100 | { 101 | return $this->job['Body']; 102 | } 103 | 104 | /** 105 | * Get the underlying SQS client instance. 106 | * 107 | * @return \Aws\Sqs\SqsClient 108 | */ 109 | public function getSqs() 110 | { 111 | return $this->sqs; 112 | } 113 | 114 | /** 115 | * Get the underlying raw SQS job. 116 | * 117 | * @return array 118 | */ 119 | public function getSqsJob() 120 | { 121 | return $this->job; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Console/ClearCommand.php: -------------------------------------------------------------------------------- 1 | confirmToProceed()) { 41 | return 1; 42 | } 43 | 44 | $connection = $this->argument('connection') 45 | ?: $this->laravel['config']['queue.default']; 46 | 47 | // We need to get the right queue for the connection which is set in the queue 48 | // configuration file for the application. We will pull it based on the set 49 | // connection being run for the queue operation currently being executed. 50 | $queueName = $this->getQueue($connection); 51 | 52 | $queue = $this->laravel['queue']->connection($connection); 53 | 54 | if ($queue instanceof ClearableQueue) { 55 | $count = $queue->clear($queueName); 56 | 57 | $this->components->info('Cleared '.$count.' '.Str::plural('job', $count).' from the ['.$queueName.'] queue'); 58 | } else { 59 | $this->components->error('Clearing queues is not supported on ['.(new ReflectionClass($queue))->getShortName().']'); 60 | 61 | return 1; 62 | } 63 | 64 | return 0; 65 | } 66 | 67 | /** 68 | * Get the queue name to clear. 69 | * 70 | * @param string $connection 71 | * @return string 72 | */ 73 | protected function getQueue($connection) 74 | { 75 | return $this->option('queue') ?: $this->laravel['config']->get( 76 | "queue.connections.{$connection}.queue", 'default' 77 | ); 78 | } 79 | 80 | /** 81 | * Get the console command arguments. 82 | * 83 | * @return array 84 | */ 85 | protected function getArguments() 86 | { 87 | return [ 88 | ['connection', InputArgument::OPTIONAL, 'The name of the queue connection to clear'], 89 | ]; 90 | } 91 | 92 | /** 93 | * Get the console command options. 94 | * 95 | * @return array 96 | */ 97 | protected function getOptions() 98 | { 99 | return [ 100 | ['queue', null, InputOption::VALUE_OPTIONAL, 'The name of the queue to clear'], 101 | 102 | ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'], 103 | ]; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /SerializesModels.php: -------------------------------------------------------------------------------- 1 | getProperties(), 27 | ! empty($reflectionClass->getAttributes(WithoutRelations::class)), 28 | ]; 29 | 30 | foreach ($properties as $property) { 31 | if ($property->isStatic()) { 32 | continue; 33 | } 34 | 35 | if (! $property->isInitialized($this)) { 36 | continue; 37 | } 38 | 39 | if (method_exists($property, 'isVirtual') && $property->isVirtual()) { 40 | continue; 41 | } 42 | 43 | $value = $this->getPropertyValue($property); 44 | 45 | if ($property->hasDefaultValue() && $value === $property->getDefaultValue()) { 46 | continue; 47 | } 48 | 49 | $name = $property->getName(); 50 | 51 | if ($property->isPrivate()) { 52 | $name = "\0{$class}\0{$name}"; 53 | } elseif ($property->isProtected()) { 54 | $name = "\0*\0{$name}"; 55 | } 56 | 57 | $values[$name] = $this->getSerializedPropertyValue( 58 | $value, 59 | ! $classLevelWithoutRelations && 60 | empty($property->getAttributes(WithoutRelations::class)) 61 | ); 62 | } 63 | 64 | return $values; 65 | } 66 | 67 | /** 68 | * Restore the model after serialization. 69 | * 70 | * @param array $values 71 | * @return void 72 | */ 73 | public function __unserialize(array $values) 74 | { 75 | $properties = (new ReflectionClass($this))->getProperties(); 76 | 77 | $class = get_class($this); 78 | 79 | foreach ($properties as $property) { 80 | if ($property->isStatic()) { 81 | continue; 82 | } 83 | 84 | $name = $property->getName(); 85 | 86 | if ($property->isPrivate()) { 87 | $name = "\0{$class}\0{$name}"; 88 | } elseif ($property->isProtected()) { 89 | $name = "\0*\0{$name}"; 90 | } 91 | 92 | if (! array_key_exists($name, $values)) { 93 | continue; 94 | } 95 | 96 | $property->setValue( 97 | $this, $this->getRestoredPropertyValue($values[$name]) 98 | ); 99 | } 100 | } 101 | 102 | /** 103 | * Get the property value for the given property. 104 | * 105 | * @param \ReflectionProperty $property 106 | * @return mixed 107 | */ 108 | protected function getPropertyValue(ReflectionProperty $property) 109 | { 110 | return $property->getValue($this); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Console/ListFailedCommand.php: -------------------------------------------------------------------------------- 1 | getFailedJobs()) === 0) { 42 | return $this->components->info('No failed jobs found.'); 43 | } 44 | 45 | $this->newLine(); 46 | $this->displayFailedJobs($jobs); 47 | $this->newLine(); 48 | } 49 | 50 | /** 51 | * Compile the failed jobs into a displayable format. 52 | * 53 | * @return array 54 | */ 55 | protected function getFailedJobs() 56 | { 57 | $failed = $this->laravel['queue.failer']->all(); 58 | 59 | return (new Collection($failed)) 60 | ->map(fn ($failed) => $this->parseFailedJob((array) $failed)) 61 | ->filter() 62 | ->all(); 63 | } 64 | 65 | /** 66 | * Parse the failed job row. 67 | * 68 | * @param array $failed 69 | * @return array 70 | */ 71 | protected function parseFailedJob(array $failed) 72 | { 73 | $row = array_values(Arr::except($failed, ['payload', 'exception'])); 74 | 75 | array_splice($row, 3, 0, $this->extractJobName($failed['payload']) ?: ''); 76 | 77 | return $row; 78 | } 79 | 80 | /** 81 | * Extract the failed job name from payload. 82 | * 83 | * @param string $payload 84 | * @return string|null 85 | */ 86 | private function extractJobName($payload) 87 | { 88 | $payload = json_decode($payload, true); 89 | 90 | if ($payload && (! isset($payload['data']['command']))) { 91 | return $payload['job'] ?? null; 92 | } elseif ($payload && isset($payload['data']['command'])) { 93 | return $this->matchJobName($payload); 94 | } 95 | } 96 | 97 | /** 98 | * Match the job name from the payload. 99 | * 100 | * @param array $payload 101 | * @return string|null 102 | */ 103 | protected function matchJobName($payload) 104 | { 105 | preg_match('/"([^"]+)"/', $payload['data']['command'], $matches); 106 | 107 | return $matches[1] ?? $payload['job'] ?? null; 108 | } 109 | 110 | /** 111 | * Display the failed jobs in the console. 112 | * 113 | * @param array $jobs 114 | * @return void 115 | */ 116 | protected function displayFailedJobs(array $jobs) 117 | { 118 | (new Collection($jobs))->each( 119 | fn ($job) => $this->components->twoColumnDetail( 120 | sprintf('%s %s', $job[4], $job[0]), 121 | sprintf('%s@%s %s', $job[1], $job[2], $job[3]) 122 | ), 123 | ); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Jobs/RedisJob.php: -------------------------------------------------------------------------------- 1 | job = $job; 55 | $this->redis = $redis; 56 | $this->queue = $queue; 57 | $this->reserved = $reserved; 58 | $this->container = $container; 59 | $this->connectionName = $connectionName; 60 | 61 | $this->decoded = $this->payload(); 62 | } 63 | 64 | /** 65 | * Get the raw body string for the job. 66 | * 67 | * @return string 68 | */ 69 | public function getRawBody() 70 | { 71 | return $this->job; 72 | } 73 | 74 | /** 75 | * Delete the job from the queue. 76 | * 77 | * @return void 78 | */ 79 | public function delete() 80 | { 81 | parent::delete(); 82 | 83 | $this->redis->deleteReserved($this->queue, $this); 84 | } 85 | 86 | /** 87 | * Release the job back into the queue after (n) seconds. 88 | * 89 | * @param int $delay 90 | * @return void 91 | */ 92 | public function release($delay = 0) 93 | { 94 | parent::release($delay); 95 | 96 | $this->redis->deleteAndRelease($this->queue, $this, $delay); 97 | } 98 | 99 | /** 100 | * Get the number of times the job has been attempted. 101 | * 102 | * @return int 103 | */ 104 | public function attempts() 105 | { 106 | return ($this->decoded['attempts'] ?? null) + 1; 107 | } 108 | 109 | /** 110 | * Get the job identifier. 111 | * 112 | * @return string|null 113 | */ 114 | public function getJobId() 115 | { 116 | return $this->decoded['id'] ?? null; 117 | } 118 | 119 | /** 120 | * Get the underlying Redis factory implementation. 121 | * 122 | * @return \Illuminate\Queue\RedisQueue 123 | */ 124 | public function getRedisQueue() 125 | { 126 | return $this->redis; 127 | } 128 | 129 | /** 130 | * Get the underlying reserved Redis job. 131 | * 132 | * @return string 133 | */ 134 | public function getReservedJob() 135 | { 136 | return $this->reserved; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /CallQueuedClosure.php: -------------------------------------------------------------------------------- 1 | closure = $closure; 54 | } 55 | 56 | /** 57 | * Create a new job instance. 58 | * 59 | * @param \Closure $job 60 | * @return self 61 | */ 62 | public static function create(Closure $job) 63 | { 64 | return new self(new SerializableClosure($job)); 65 | } 66 | 67 | /** 68 | * Execute the job. 69 | * 70 | * @param \Illuminate\Contracts\Container\Container $container 71 | * @return void 72 | */ 73 | public function handle(Container $container) 74 | { 75 | $container->call($this->closure->getClosure(), ['job' => $this]); 76 | } 77 | 78 | /** 79 | * Add a callback to be executed if the job fails. 80 | * 81 | * @param callable $callback 82 | * @return $this 83 | */ 84 | public function onFailure($callback) 85 | { 86 | $this->failureCallbacks[] = $callback instanceof Closure 87 | ? new SerializableClosure($callback) 88 | : $callback; 89 | 90 | return $this; 91 | } 92 | 93 | /** 94 | * Handle a job failure. 95 | * 96 | * @param \Throwable $e 97 | * @return void 98 | */ 99 | public function failed($e) 100 | { 101 | foreach ($this->failureCallbacks as $callback) { 102 | $callback($e); 103 | } 104 | } 105 | 106 | /** 107 | * Get the display name for the queued job. 108 | * 109 | * @return string 110 | */ 111 | public function displayName() 112 | { 113 | $closure = $this->closure instanceof SerializableClosure 114 | ? $this->closure->getClosure() 115 | : $this->closure; 116 | 117 | $reflection = new ReflectionFunction($closure); 118 | 119 | $prefix = is_null($this->name) ? '' : "{$this->name} - "; 120 | 121 | return $prefix.'Closure ('.basename($reflection->getFileName()).':'.$reflection->getStartLine().')'; 122 | } 123 | 124 | /** 125 | * Assign a name to the job. 126 | * 127 | * @param string $name 128 | * @return $this 129 | */ 130 | public function name($name) 131 | { 132 | $this->name = $name; 133 | 134 | return $this; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Jobs/BeanstalkdJob.php: -------------------------------------------------------------------------------- 1 | job = $job; 38 | $this->queue = $queue; 39 | $this->container = $container; 40 | $this->pheanstalk = $pheanstalk; 41 | $this->connectionName = $connectionName; 42 | } 43 | 44 | /** 45 | * Release the job back into the queue after (n) seconds. 46 | * 47 | * @param int $delay 48 | * @return void 49 | */ 50 | public function release($delay = 0) 51 | { 52 | parent::release($delay); 53 | 54 | $priority = Pheanstalk::DEFAULT_PRIORITY; 55 | 56 | $this->pheanstalk->release($this->job, $priority, $delay); 57 | } 58 | 59 | /** 60 | * Bury the job in the queue. 61 | * 62 | * @return void 63 | */ 64 | public function bury() 65 | { 66 | parent::release(); 67 | 68 | $this->pheanstalk->bury($this->job); 69 | } 70 | 71 | /** 72 | * Delete the job from the queue. 73 | * 74 | * @return void 75 | */ 76 | public function delete() 77 | { 78 | parent::delete(); 79 | 80 | $this->pheanstalk->delete($this->job); 81 | } 82 | 83 | /** 84 | * Get the number of times the job has been attempted. 85 | * 86 | * @return int 87 | */ 88 | public function attempts() 89 | { 90 | $stats = $this->pheanstalk->statsJob($this->job); 91 | 92 | return (int) $stats->reserves; 93 | } 94 | 95 | /** 96 | * Get the job identifier. 97 | * 98 | * @return int 99 | */ 100 | public function getJobId() 101 | { 102 | return $this->job->getId(); 103 | } 104 | 105 | /** 106 | * Get the raw body string for the job. 107 | * 108 | * @return string 109 | */ 110 | public function getRawBody() 111 | { 112 | return $this->job->getData(); 113 | } 114 | 115 | /** 116 | * Get the underlying Pheanstalk instance. 117 | * 118 | * @return \Pheanstalk\Contract\PheanstalkManagerInterface&\Pheanstalk\Contract\PheanstalkPublisherInterface&\Pheanstalk\Contract\PheanstalkSubscriberInterface 119 | */ 120 | public function getPheanstalk() 121 | { 122 | return $this->pheanstalk; 123 | } 124 | 125 | /** 126 | * Get the underlying Pheanstalk job. 127 | * 128 | * @return \Pheanstalk\Contract\JobIdInterface 129 | */ 130 | public function getPheanstalkJob() 131 | { 132 | return $this->job; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Middleware/WithoutOverlapping.php: -------------------------------------------------------------------------------- 1 | key = $key; 58 | $this->releaseAfter = $releaseAfter; 59 | $this->expiresAfter = $this->secondsUntil($expiresAfter); 60 | } 61 | 62 | /** 63 | * Process the job. 64 | * 65 | * @param mixed $job 66 | * @param callable $next 67 | * @return mixed 68 | */ 69 | public function handle($job, $next) 70 | { 71 | $lock = Container::getInstance()->make(Cache::class)->lock( 72 | $this->getLockKey($job), $this->expiresAfter 73 | ); 74 | 75 | if ($lock->get()) { 76 | try { 77 | $next($job); 78 | } finally { 79 | $lock->release(); 80 | } 81 | } elseif (! is_null($this->releaseAfter)) { 82 | $job->release($this->releaseAfter); 83 | } 84 | } 85 | 86 | /** 87 | * Set the delay (in seconds) to release the job back to the queue. 88 | * 89 | * @param \DateTimeInterface|int $releaseAfter 90 | * @return $this 91 | */ 92 | public function releaseAfter($releaseAfter) 93 | { 94 | $this->releaseAfter = $releaseAfter; 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * Do not release the job back to the queue if no lock can be acquired. 101 | * 102 | * @return $this 103 | */ 104 | public function dontRelease() 105 | { 106 | $this->releaseAfter = null; 107 | 108 | return $this; 109 | } 110 | 111 | /** 112 | * Set the maximum number of seconds that can elapse before the lock is released. 113 | * 114 | * @param \DateTimeInterface|\DateInterval|int $expiresAfter 115 | * @return $this 116 | */ 117 | public function expireAfter($expiresAfter) 118 | { 119 | $this->expiresAfter = $this->secondsUntil($expiresAfter); 120 | 121 | return $this; 122 | } 123 | 124 | /** 125 | * Set the prefix of the lock key. 126 | * 127 | * @param string $prefix 128 | * @return $this 129 | */ 130 | public function withPrefix(string $prefix) 131 | { 132 | $this->prefix = $prefix; 133 | 134 | return $this; 135 | } 136 | 137 | /** 138 | * Indicate that the lock key should be shared across job classes. 139 | * 140 | * @return $this 141 | */ 142 | public function shared() 143 | { 144 | $this->shareKey = true; 145 | 146 | return $this; 147 | } 148 | 149 | /** 150 | * Get the lock key for the given job. 151 | * 152 | * @param mixed $job 153 | * @return string 154 | */ 155 | public function getLockKey($job) 156 | { 157 | if ($this->shareKey) { 158 | return $this->prefix.$this->key; 159 | } 160 | 161 | $jobName = method_exists($job, 'displayName') 162 | ? $job->displayName() 163 | : get_class($job); 164 | 165 | return $this->prefix.$jobName.':'.$this->key; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /SerializesAndRestoresModelIdentifiers.php: -------------------------------------------------------------------------------- 1 | getQueueableClass(), 27 | $value->getQueueableIds(), 28 | $withRelations ? $value->getQueueableRelations() : [], 29 | $value->getQueueableConnection() 30 | ))->useCollectionClass( 31 | ($collectionClass = get_class($value)) !== EloquentCollection::class 32 | ? $collectionClass 33 | : null 34 | ); 35 | } 36 | 37 | if ($value instanceof QueueableEntity) { 38 | return new ModelIdentifier( 39 | get_class($value), 40 | $value->getQueueableId(), 41 | $withRelations ? $value->getQueueableRelations() : [], 42 | $value->getQueueableConnection() 43 | ); 44 | } 45 | 46 | return $value; 47 | } 48 | 49 | /** 50 | * Get the restored property value after deserialization. 51 | * 52 | * @param mixed $value 53 | * @return mixed 54 | */ 55 | protected function getRestoredPropertyValue($value) 56 | { 57 | if (! $value instanceof ModelIdentifier) { 58 | return $value; 59 | } 60 | 61 | return is_array($value->id) 62 | ? $this->restoreCollection($value) 63 | : $this->restoreModel($value); 64 | } 65 | 66 | /** 67 | * Restore a queueable collection instance. 68 | * 69 | * @param \Illuminate\Contracts\Database\ModelIdentifier $value 70 | * @return \Illuminate\Database\Eloquent\Collection 71 | */ 72 | protected function restoreCollection($value) 73 | { 74 | if (! $value->class || count($value->id) === 0) { 75 | return ! is_null($value->collectionClass ?? null) 76 | ? new $value->collectionClass 77 | : new EloquentCollection; 78 | } 79 | 80 | $collection = $this->getQueryForModelRestoration( 81 | (new $value->class)->setConnection($value->connection), $value->id 82 | )->useWritePdo()->get(); 83 | 84 | if (is_a($value->class, Pivot::class, true) || 85 | in_array(AsPivot::class, class_uses($value->class))) { 86 | return $collection; 87 | } 88 | 89 | $collection = $collection->keyBy->getKey(); 90 | 91 | $collectionClass = get_class($collection); 92 | 93 | return new $collectionClass( 94 | (new Collection($value->id)) 95 | ->map(fn ($id) => $collection[$id] ?? null) 96 | ->filter() 97 | ); 98 | } 99 | 100 | /** 101 | * Restore the model from the model identifier instance. 102 | * 103 | * @param \Illuminate\Contracts\Database\ModelIdentifier $value 104 | * @return \Illuminate\Database\Eloquent\Model 105 | */ 106 | public function restoreModel($value) 107 | { 108 | return $this->getQueryForModelRestoration( 109 | (new $value->class)->setConnection($value->connection), $value->id 110 | )->useWritePdo()->firstOrFail()->loadMissing($value->relations ?? []); 111 | } 112 | 113 | /** 114 | * Get the query for model restoration. 115 | * 116 | * @template TModel of \Illuminate\Database\Eloquent\Model 117 | * 118 | * @param TModel $model 119 | * @param array|int $ids 120 | * @return \Illuminate\Database\Eloquent\Builder 121 | */ 122 | protected function getQueryForModelRestoration($model, $ids) 123 | { 124 | return $model->newQueryForRestoration($ids); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Middleware/RateLimited.php: -------------------------------------------------------------------------------- 1 | limiter = Container::getInstance()->make(RateLimiter::class); 50 | 51 | $this->limiterName = (string) enum_value($limiterName); 52 | } 53 | 54 | /** 55 | * Process the job. 56 | * 57 | * @param mixed $job 58 | * @param callable $next 59 | * @return mixed 60 | */ 61 | public function handle($job, $next) 62 | { 63 | if (is_null($limiter = $this->limiter->limiter($this->limiterName))) { 64 | return $next($job); 65 | } 66 | 67 | $limiterResponse = $limiter($job); 68 | 69 | if ($limiterResponse instanceof Unlimited) { 70 | return $next($job); 71 | } 72 | 73 | return $this->handleJob( 74 | $job, 75 | $next, 76 | Collection::wrap($limiterResponse)->map(function ($limit) { 77 | return (object) [ 78 | 'key' => md5($this->limiterName.$limit->key), 79 | 'maxAttempts' => $limit->maxAttempts, 80 | 'decaySeconds' => $limit->decaySeconds, 81 | ]; 82 | })->all() 83 | ); 84 | } 85 | 86 | /** 87 | * Handle a rate limited job. 88 | * 89 | * @param mixed $job 90 | * @param callable $next 91 | * @param array $limits 92 | * @return mixed 93 | */ 94 | protected function handleJob($job, $next, array $limits) 95 | { 96 | foreach ($limits as $limit) { 97 | if ($this->limiter->tooManyAttempts($limit->key, $limit->maxAttempts)) { 98 | return $this->shouldRelease 99 | ? $job->release($this->releaseAfter ?: $this->getTimeUntilNextRetry($limit->key)) 100 | : false; 101 | } 102 | 103 | $this->limiter->hit($limit->key, $limit->decaySeconds); 104 | } 105 | 106 | return $next($job); 107 | } 108 | 109 | /** 110 | * Set the delay (in seconds) to release the job back to the queue. 111 | * 112 | * @param \DateTimeInterface|int $releaseAfter 113 | * @return $this 114 | */ 115 | public function releaseAfter($releaseAfter) 116 | { 117 | $this->releaseAfter = $releaseAfter; 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * Do not release the job back to the queue if the limit is exceeded. 124 | * 125 | * @return $this 126 | */ 127 | public function dontRelease() 128 | { 129 | $this->shouldRelease = false; 130 | 131 | return $this; 132 | } 133 | 134 | /** 135 | * Get the number of seconds that should elapse before the job is retried. 136 | * 137 | * @param string $key 138 | * @return int 139 | */ 140 | protected function getTimeUntilNextRetry($key) 141 | { 142 | return $this->limiter->availableIn($key) + 3; 143 | } 144 | 145 | /** 146 | * Prepare the object for serialization. 147 | * 148 | * @return array 149 | */ 150 | public function __sleep() 151 | { 152 | return [ 153 | 'limiterName', 154 | 'shouldRelease', 155 | ]; 156 | } 157 | 158 | /** 159 | * Prepare the object after unserialization. 160 | * 161 | * @return void 162 | */ 163 | public function __wakeup() 164 | { 165 | $this->limiter = Container::getInstance()->make(RateLimiter::class); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Console/ListenCommand.php: -------------------------------------------------------------------------------- 1 | setOutputHandler($this->listener = $listener); 56 | } 57 | 58 | /** 59 | * Execute the console command. 60 | * 61 | * @return void 62 | */ 63 | public function handle() 64 | { 65 | // We need to get the right queue for the connection which is set in the queue 66 | // configuration file for the application. We will pull it based on the set 67 | // connection being run for the queue operation currently being executed. 68 | $queue = $this->getQueue( 69 | $connection = $this->input->getArgument('connection') 70 | ); 71 | 72 | $this->components->info(sprintf('Processing jobs from the [%s] %s.', $queue, (new Stringable('queue'))->plural(explode(',', $queue)))); 73 | 74 | $this->listener->listen( 75 | $connection, $queue, $this->gatherOptions() 76 | ); 77 | } 78 | 79 | /** 80 | * Get the name of the queue connection to listen on. 81 | * 82 | * @param string $connection 83 | * @return string 84 | */ 85 | protected function getQueue($connection) 86 | { 87 | $connection = $connection ?: $this->laravel['config']['queue.default']; 88 | 89 | return $this->input->getOption('queue') ?: $this->laravel['config']->get( 90 | "queue.connections.{$connection}.queue", 'default' 91 | ); 92 | } 93 | 94 | /** 95 | * Get the listener options for the command. 96 | * 97 | * @return \Illuminate\Queue\ListenerOptions 98 | */ 99 | protected function gatherOptions() 100 | { 101 | $backoff = $this->hasOption('backoff') 102 | ? $this->option('backoff') 103 | : $this->option('delay'); 104 | 105 | return new ListenerOptions( 106 | name: $this->option('name'), 107 | environment: $this->option('env'), 108 | backoff: $backoff, 109 | memory: $this->option('memory'), 110 | timeout: $this->option('timeout'), 111 | sleep: $this->option('sleep'), 112 | rest: $this->option('rest'), 113 | maxTries: $this->option('tries'), 114 | force: $this->option('force') 115 | ); 116 | } 117 | 118 | /** 119 | * Set the options on the queue listener. 120 | * 121 | * @param \Illuminate\Queue\Listener $listener 122 | * @return void 123 | */ 124 | protected function setOutputHandler(Listener $listener) 125 | { 126 | $listener->setOutputHandler(function ($type, $line) { 127 | $this->output->write($line); 128 | }); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Failed/DatabaseFailedJobProvider.php: -------------------------------------------------------------------------------- 1 | table = $table; 42 | $this->resolver = $resolver; 43 | $this->database = $database; 44 | } 45 | 46 | /** 47 | * Log a failed job into storage. 48 | * 49 | * @param string $connection 50 | * @param string $queue 51 | * @param string $payload 52 | * @param \Throwable $exception 53 | * @return int|null 54 | */ 55 | public function log($connection, $queue, $payload, $exception) 56 | { 57 | $failed_at = Date::now(); 58 | 59 | $exception = (string) mb_convert_encoding($exception, 'UTF-8'); 60 | 61 | return $this->getTable()->insertGetId(compact( 62 | 'connection', 'queue', 'payload', 'exception', 'failed_at' 63 | )); 64 | } 65 | 66 | /** 67 | * Get the IDs of all of the failed jobs. 68 | * 69 | * @param string|null $queue 70 | * @return array 71 | */ 72 | public function ids($queue = null) 73 | { 74 | return $this->getTable() 75 | ->when(! is_null($queue), fn ($query) => $query->where('queue', $queue)) 76 | ->orderBy('id', 'desc') 77 | ->pluck('id') 78 | ->all(); 79 | } 80 | 81 | /** 82 | * Get a list of all of the failed jobs. 83 | * 84 | * @return array 85 | */ 86 | public function all() 87 | { 88 | return $this->getTable()->orderBy('id', 'desc')->get()->all(); 89 | } 90 | 91 | /** 92 | * Get a single failed job. 93 | * 94 | * @param mixed $id 95 | * @return object|null 96 | */ 97 | public function find($id) 98 | { 99 | return $this->getTable()->find($id); 100 | } 101 | 102 | /** 103 | * Delete a single failed job from storage. 104 | * 105 | * @param mixed $id 106 | * @return bool 107 | */ 108 | public function forget($id) 109 | { 110 | return $this->getTable()->where('id', $id)->delete() > 0; 111 | } 112 | 113 | /** 114 | * Flush all of the failed jobs from storage. 115 | * 116 | * @param int|null $hours 117 | * @return void 118 | */ 119 | public function flush($hours = null) 120 | { 121 | $this->getTable()->when($hours, function ($query, $hours) { 122 | $query->where('failed_at', '<=', Date::now()->subHours($hours)); 123 | })->delete(); 124 | } 125 | 126 | /** 127 | * Prune all of the entries older than the given date. 128 | * 129 | * @param \DateTimeInterface $before 130 | * @return int 131 | */ 132 | public function prune(DateTimeInterface $before) 133 | { 134 | $query = $this->getTable()->where('failed_at', '<', $before); 135 | 136 | $totalDeleted = 0; 137 | 138 | do { 139 | $deleted = $query->limit(1000)->delete(); 140 | 141 | $totalDeleted += $deleted; 142 | } while ($deleted !== 0); 143 | 144 | return $totalDeleted; 145 | } 146 | 147 | /** 148 | * Count the failed jobs. 149 | * 150 | * @param string|null $connection 151 | * @param string|null $queue 152 | * @return int 153 | */ 154 | public function count($connection = null, $queue = null) 155 | { 156 | return $this->getTable() 157 | ->when($connection, fn ($builder) => $builder->whereConnection($connection)) 158 | ->when($queue, fn ($builder) => $builder->whereQueue($queue)) 159 | ->count(); 160 | } 161 | 162 | /** 163 | * Get a new query builder instance for the table. 164 | * 165 | * @return \Illuminate\Database\Query\Builder 166 | */ 167 | public function getTable() 168 | { 169 | return $this->resolver->connection($this->database)->table($this->table); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /LuaScripts.php: -------------------------------------------------------------------------------- 1 | manager->connection($this->connections[0])->size($queue); 32 | } 33 | 34 | /** 35 | * Get the number of pending jobs. 36 | * 37 | * @param string|null $queue 38 | * @return int 39 | */ 40 | public function pendingSize($queue = null) 41 | { 42 | return $this->manager->connection($this->connections[0])->pendingSize($queue); 43 | } 44 | 45 | /** 46 | * Get the number of delayed jobs. 47 | * 48 | * @param string|null $queue 49 | * @return int 50 | */ 51 | public function delayedSize($queue = null) 52 | { 53 | return $this->manager->connection($this->connections[0])->delayedSize($queue); 54 | } 55 | 56 | /** 57 | * Get the number of reserved jobs. 58 | * 59 | * @param string|null $queue 60 | * @return int 61 | */ 62 | public function reservedSize($queue = null) 63 | { 64 | return $this->manager->connection($this->connections[0])->reservedSize($queue); 65 | } 66 | 67 | /** 68 | * Get the creation timestamp of the oldest pending job, excluding delayed jobs. 69 | * 70 | * @param string|null $queue 71 | * @return int|null 72 | */ 73 | public function creationTimeOfOldestPendingJob($queue = null) 74 | { 75 | return $this->manager 76 | ->connection($this->connections[0]) 77 | ->creationTimeOfOldestPendingJob($queue); 78 | } 79 | 80 | /** 81 | * Push a new job onto the queue. 82 | * 83 | * @param object|string $job 84 | * @param mixed $data 85 | * @param string|null $queue 86 | * @return mixed 87 | */ 88 | public function push($job, $data = '', $queue = null) 89 | { 90 | $lastException = null; 91 | 92 | foreach ($this->connections as $connection) { 93 | try { 94 | return $this->manager->connection($connection)->push($job, $data, $queue); 95 | } catch (Throwable $e) { 96 | $lastException = $e; 97 | 98 | $this->events->dispatch(new QueueFailedOver($connection, $job, $e)); 99 | } 100 | } 101 | 102 | throw $lastException ?? new RuntimeException('All failover queue connections failed.'); 103 | } 104 | 105 | /** 106 | * Push a raw payload onto the queue. 107 | * 108 | * @param string $payload 109 | * @param string|null $queue 110 | * @return mixed 111 | */ 112 | public function pushRaw($payload, $queue = null, array $options = []) 113 | { 114 | $lastException = null; 115 | 116 | foreach ($this->connections as $connection) { 117 | try { 118 | return $this->manager->connection($connection)->pushRaw($payload, $queue, $options); 119 | } catch (Throwable $e) { 120 | $lastException = $e; 121 | } 122 | } 123 | 124 | throw $lastException ?? new RuntimeException('All failover queue connections failed.'); 125 | } 126 | 127 | /** 128 | * Push a new job onto the queue after (n) seconds. 129 | * 130 | * @param \DateTimeInterface|\DateInterval|int $delay 131 | * @param string $job 132 | * @param mixed $data 133 | * @param string|null $queue 134 | * @return mixed 135 | */ 136 | public function later($delay, $job, $data = '', $queue = null) 137 | { 138 | $lastException = null; 139 | 140 | foreach ($this->connections as $connection) { 141 | try { 142 | return $this->manager->connection($connection)->later($delay, $job, $data, $queue); 143 | } catch (Throwable $e) { 144 | $lastException = $e; 145 | 146 | $this->events->dispatch(new QueueFailedOver($connection, $job, $e)); 147 | } 148 | } 149 | 150 | throw $lastException ?? new RuntimeException('All failover queue connections failed.'); 151 | } 152 | 153 | /** 154 | * Pop the next job off of the queue. 155 | * 156 | * @param string|null $queue 157 | * @return \Illuminate\Contracts\Queue\Job|null 158 | */ 159 | public function pop($queue = null) 160 | { 161 | return $this->manager->connection($this->connections[0])->pop($queue); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /Failed/DatabaseUuidFailedJobProvider.php: -------------------------------------------------------------------------------- 1 | table = $table; 42 | $this->resolver = $resolver; 43 | $this->database = $database; 44 | } 45 | 46 | /** 47 | * Log a failed job into storage. 48 | * 49 | * @param string $connection 50 | * @param string $queue 51 | * @param string $payload 52 | * @param \Throwable $exception 53 | * @return string|null 54 | */ 55 | public function log($connection, $queue, $payload, $exception) 56 | { 57 | $this->getTable()->insert([ 58 | 'uuid' => $uuid = json_decode($payload, true)['uuid'], 59 | 'connection' => $connection, 60 | 'queue' => $queue, 61 | 'payload' => $payload, 62 | 'exception' => (string) mb_convert_encoding($exception, 'UTF-8'), 63 | 'failed_at' => Date::now(), 64 | ]); 65 | 66 | return $uuid; 67 | } 68 | 69 | /** 70 | * Get the IDs of all of the failed jobs. 71 | * 72 | * @param string|null $queue 73 | * @return array 74 | */ 75 | public function ids($queue = null) 76 | { 77 | return $this->getTable() 78 | ->when(! is_null($queue), fn ($query) => $query->where('queue', $queue)) 79 | ->orderBy('id', 'desc') 80 | ->pluck('uuid') 81 | ->all(); 82 | } 83 | 84 | /** 85 | * Get a list of all of the failed jobs. 86 | * 87 | * @return array 88 | */ 89 | public function all() 90 | { 91 | return $this->getTable()->orderBy('id', 'desc')->get()->map(function ($record) { 92 | $record->id = $record->uuid; 93 | unset($record->uuid); 94 | 95 | return $record; 96 | })->all(); 97 | } 98 | 99 | /** 100 | * Get a single failed job. 101 | * 102 | * @param mixed $id 103 | * @return object|null 104 | */ 105 | public function find($id) 106 | { 107 | if ($record = $this->getTable()->where('uuid', $id)->first()) { 108 | $record->id = $record->uuid; 109 | unset($record->uuid); 110 | } 111 | 112 | return $record; 113 | } 114 | 115 | /** 116 | * Delete a single failed job from storage. 117 | * 118 | * @param mixed $id 119 | * @return bool 120 | */ 121 | public function forget($id) 122 | { 123 | return $this->getTable()->where('uuid', $id)->delete() > 0; 124 | } 125 | 126 | /** 127 | * Flush all of the failed jobs from storage. 128 | * 129 | * @param int|null $hours 130 | * @return void 131 | */ 132 | public function flush($hours = null) 133 | { 134 | $this->getTable()->when($hours, function ($query, $hours) { 135 | $query->where('failed_at', '<=', Date::now()->subHours($hours)); 136 | })->delete(); 137 | } 138 | 139 | /** 140 | * Prune all of the entries older than the given date. 141 | * 142 | * @param \DateTimeInterface $before 143 | * @return int 144 | */ 145 | public function prune(DateTimeInterface $before) 146 | { 147 | $query = $this->getTable()->where('failed_at', '<', $before); 148 | 149 | $totalDeleted = 0; 150 | 151 | do { 152 | $deleted = $query->limit(1000)->delete(); 153 | 154 | $totalDeleted += $deleted; 155 | } while ($deleted !== 0); 156 | 157 | return $totalDeleted; 158 | } 159 | 160 | /** 161 | * Count the failed jobs. 162 | * 163 | * @param string|null $connection 164 | * @param string|null $queue 165 | * @return int 166 | */ 167 | public function count($connection = null, $queue = null) 168 | { 169 | return $this->getTable() 170 | ->when($connection, fn ($builder) => $builder->whereConnection($connection)) 171 | ->when($queue, fn ($builder) => $builder->whereQueue($queue)) 172 | ->count(); 173 | } 174 | 175 | /** 176 | * Get a new query builder instance for the table. 177 | * 178 | * @return \Illuminate\Database\Query\Builder 179 | */ 180 | public function getTable() 181 | { 182 | return $this->resolver->connection($this->database)->table($this->table); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /Capsule/Manager.php: -------------------------------------------------------------------------------- 1 | setupContainer($container ?: new Container); 33 | 34 | // Once we have the container setup, we will set up the default configuration 35 | // options in the container "config" bindings. This'll just make the queue 36 | // manager behave correctly since all the correct bindings are in place. 37 | $this->setupDefaultConfiguration(); 38 | 39 | $this->setupManager(); 40 | 41 | $this->registerConnectors(); 42 | } 43 | 44 | /** 45 | * Setup the default queue configuration options. 46 | * 47 | * @return void 48 | */ 49 | protected function setupDefaultConfiguration() 50 | { 51 | $this->container['config']['queue.default'] = 'default'; 52 | } 53 | 54 | /** 55 | * Build the queue manager instance. 56 | * 57 | * @return void 58 | */ 59 | protected function setupManager() 60 | { 61 | $this->manager = new QueueManager($this->container); 62 | } 63 | 64 | /** 65 | * Register the default connectors that the component ships with. 66 | * 67 | * @return void 68 | */ 69 | protected function registerConnectors() 70 | { 71 | $provider = new QueueServiceProvider($this->container); 72 | 73 | $provider->registerConnectors($this->manager); 74 | } 75 | 76 | /** 77 | * Get a connection instance from the global manager. 78 | * 79 | * @param string|null $connection 80 | * @return \Illuminate\Contracts\Queue\Queue 81 | */ 82 | public static function connection($connection = null) 83 | { 84 | return static::$instance->getConnection($connection); 85 | } 86 | 87 | /** 88 | * Push a new job onto the queue. 89 | * 90 | * @param string $job 91 | * @param mixed $data 92 | * @param string|null $queue 93 | * @param string|null $connection 94 | * @return mixed 95 | */ 96 | public static function push($job, $data = '', $queue = null, $connection = null) 97 | { 98 | return static::$instance->connection($connection)->push($job, $data, $queue); 99 | } 100 | 101 | /** 102 | * Push a new an array of jobs onto the queue. 103 | * 104 | * @param array $jobs 105 | * @param mixed $data 106 | * @param string|null $queue 107 | * @param string|null $connection 108 | * @return mixed 109 | */ 110 | public static function bulk($jobs, $data = '', $queue = null, $connection = null) 111 | { 112 | return static::$instance->connection($connection)->bulk($jobs, $data, $queue); 113 | } 114 | 115 | /** 116 | * Push a new job onto the queue after (n) seconds. 117 | * 118 | * @param \DateTimeInterface|\DateInterval|int $delay 119 | * @param string $job 120 | * @param mixed $data 121 | * @param string|null $queue 122 | * @param string|null $connection 123 | * @return mixed 124 | */ 125 | public static function later($delay, $job, $data = '', $queue = null, $connection = null) 126 | { 127 | return static::$instance->connection($connection)->later($delay, $job, $data, $queue); 128 | } 129 | 130 | /** 131 | * Get a registered connection instance. 132 | * 133 | * @param string|null $name 134 | * @return \Illuminate\Contracts\Queue\Queue 135 | */ 136 | public function getConnection($name = null) 137 | { 138 | return $this->manager->connection($name); 139 | } 140 | 141 | /** 142 | * Register a connection with the manager. 143 | * 144 | * @param array $config 145 | * @param string $name 146 | * @return void 147 | */ 148 | public function addConnection(array $config, $name = 'default') 149 | { 150 | $this->container['config']["queue.connections.{$name}"] = $config; 151 | } 152 | 153 | /** 154 | * Get the queue manager instance. 155 | * 156 | * @return \Illuminate\Queue\QueueManager 157 | */ 158 | public function getQueueManager() 159 | { 160 | return $this->manager; 161 | } 162 | 163 | /** 164 | * Pass dynamic instance methods to the manager. 165 | * 166 | * @param string $method 167 | * @param array $parameters 168 | * @return mixed 169 | */ 170 | public function __call($method, $parameters) 171 | { 172 | return $this->manager->$method(...$parameters); 173 | } 174 | 175 | /** 176 | * Dynamically pass methods to the default connection. 177 | * 178 | * @param string $method 179 | * @param array $parameters 180 | * @return mixed 181 | */ 182 | public static function __callStatic($method, $parameters) 183 | { 184 | return static::connection()->$method(...$parameters); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /Console/MonitorCommand.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 57 | $this->events = $events; 58 | } 59 | 60 | /** 61 | * Execute the console command. 62 | * 63 | * @return void 64 | */ 65 | public function handle() 66 | { 67 | $queues = $this->parseQueues($this->argument('queues')); 68 | 69 | if ($this->option('json')) { 70 | $this->output->writeln((new Collection($queues))->map(function ($queue) { 71 | return array_merge($queue, [ 72 | 'status' => str_contains($queue['status'], 'ALERT') ? 'ALERT' : 'OK', 73 | ]); 74 | })->toJson()); 75 | } else { 76 | $this->displaySizes($queues); 77 | } 78 | 79 | $this->dispatchEvents($queues); 80 | } 81 | 82 | /** 83 | * Parse the queues into an array of the connections and queues. 84 | * 85 | * @param string $queues 86 | * @return \Illuminate\Support\Collection 87 | */ 88 | protected function parseQueues($queues) 89 | { 90 | return (new Collection(explode(',', $queues)))->map(function ($queue) { 91 | [$connection, $queue] = array_pad(explode(':', $queue, 2), 2, null); 92 | 93 | if (! isset($queue)) { 94 | $queue = $connection; 95 | $connection = $this->laravel['config']['queue.default']; 96 | } 97 | 98 | return [ 99 | 'connection' => $connection, 100 | 'queue' => $queue, 101 | 'size' => $size = $this->manager->connection($connection)->size($queue), 102 | 'pending' => method_exists($this->manager->connection($connection), 'pendingSize') 103 | ? $this->manager->connection($connection)->pendingSize($queue) 104 | : null, 105 | 'delayed' => method_exists($this->manager->connection($connection), 'delayedSize') 106 | ? $this->manager->connection($connection)->delayedSize($queue) 107 | : null, 108 | 'reserved' => method_exists($this->manager->connection($connection), 'reservedSize') 109 | ? $this->manager->connection($connection)->reservedSize($queue) 110 | : null, 111 | 'oldest_pending' => method_exists($this->manager->connection($connection), 'oldestPending') 112 | ? $this->manager->connection($connection)->creationTimeOfOldestPendingJob($queue) 113 | : null, 114 | 'status' => $size >= $this->option('max') ? 'ALERT' : 'OK', 115 | ]; 116 | }); 117 | } 118 | 119 | /** 120 | * Display the queue sizes in the console. 121 | * 122 | * @param \Illuminate\Support\Collection $queues 123 | * @return void 124 | */ 125 | protected function displaySizes(Collection $queues) 126 | { 127 | $this->newLine(); 128 | 129 | $this->components->twoColumnDetail('Queue name', 'Size / Status'); 130 | 131 | $queues->each(function ($queue) { 132 | $name = '['.$queue['connection'].'] '.$queue['queue']; 133 | $status = '['.$queue['size'].'] '.$queue['status']; 134 | 135 | $this->components->twoColumnDetail($name, $status); 136 | $this->components->twoColumnDetail('Pending jobs', $queue['pending'] ?? 'N/A'); 137 | $this->components->twoColumnDetail('Delayed jobs', $queue['delayed'] ?? 'N/A'); 138 | $this->components->twoColumnDetail('Reserved jobs', $queue['reserved'] ?? 'N/A'); 139 | $this->line(''); 140 | }); 141 | 142 | $this->newLine(); 143 | } 144 | 145 | /** 146 | * Fire the monitoring events. 147 | * 148 | * @param \Illuminate\Support\Collection $queues 149 | * @return void 150 | */ 151 | protected function dispatchEvents(Collection $queues) 152 | { 153 | foreach ($queues as $queue) { 154 | if ($queue['status'] == 'OK') { 155 | continue; 156 | } 157 | 158 | $this->events->dispatch( 159 | new QueueBusy( 160 | $queue['connection'], 161 | $queue['queue'], 162 | $queue['size'], 163 | ) 164 | ); 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Failed/DynamoDbFailedJobProvider.php: -------------------------------------------------------------------------------- 1 | table = $table; 45 | $this->dynamo = $dynamo; 46 | $this->applicationName = $applicationName; 47 | } 48 | 49 | /** 50 | * Log a failed job into storage. 51 | * 52 | * @param string $connection 53 | * @param string $queue 54 | * @param string $payload 55 | * @param \Throwable $exception 56 | * @return string|int|null 57 | */ 58 | public function log($connection, $queue, $payload, $exception) 59 | { 60 | $id = json_decode($payload, true)['uuid']; 61 | 62 | $failedAt = Date::now(); 63 | 64 | $this->dynamo->putItem([ 65 | 'TableName' => $this->table, 66 | 'Item' => [ 67 | 'application' => ['S' => $this->applicationName], 68 | 'uuid' => ['S' => $id], 69 | 'connection' => ['S' => $connection], 70 | 'queue' => ['S' => $queue], 71 | 'payload' => ['S' => $payload], 72 | 'exception' => ['S' => (string) $exception], 73 | 'failed_at' => ['N' => (string) $failedAt->getTimestamp()], 74 | 'expires_at' => ['N' => (string) $failedAt->addDays(7)->getTimestamp()], 75 | ], 76 | ]); 77 | 78 | return $id; 79 | } 80 | 81 | /** 82 | * Get the IDs of all of the failed jobs. 83 | * 84 | * @param string|null $queue 85 | * @return array 86 | */ 87 | public function ids($queue = null) 88 | { 89 | return (new Collection($this->all())) 90 | ->when(! is_null($queue), fn ($collect) => $collect->where('queue', $queue)) 91 | ->pluck('id') 92 | ->all(); 93 | } 94 | 95 | /** 96 | * Get a list of all of the failed jobs. 97 | * 98 | * @return array 99 | */ 100 | public function all() 101 | { 102 | $results = $this->dynamo->query([ 103 | 'TableName' => $this->table, 104 | 'Select' => 'ALL_ATTRIBUTES', 105 | 'KeyConditionExpression' => 'application = :application', 106 | 'ExpressionAttributeValues' => [ 107 | ':application' => ['S' => $this->applicationName], 108 | ], 109 | 'ScanIndexForward' => false, 110 | ]); 111 | 112 | return (new Collection($results['Items'])) 113 | ->sortByDesc(fn ($result) => (int) $result['failed_at']['N']) 114 | ->map(function ($result) { 115 | return (object) [ 116 | 'id' => $result['uuid']['S'], 117 | 'connection' => $result['connection']['S'], 118 | 'queue' => $result['queue']['S'], 119 | 'payload' => $result['payload']['S'], 120 | 'exception' => $result['exception']['S'], 121 | 'failed_at' => Carbon::createFromTimestamp( 122 | (int) $result['failed_at']['N'], date_default_timezone_get() 123 | )->format(DateTimeInterface::ISO8601), 124 | ]; 125 | }) 126 | ->all(); 127 | } 128 | 129 | /** 130 | * Get a single failed job. 131 | * 132 | * @param mixed $id 133 | * @return object|null 134 | */ 135 | public function find($id) 136 | { 137 | $result = $this->dynamo->getItem([ 138 | 'TableName' => $this->table, 139 | 'Key' => [ 140 | 'application' => ['S' => $this->applicationName], 141 | 'uuid' => ['S' => $id], 142 | ], 143 | ]); 144 | 145 | if (! isset($result['Item'])) { 146 | return; 147 | } 148 | 149 | return (object) [ 150 | 'id' => $result['Item']['uuid']['S'], 151 | 'connection' => $result['Item']['connection']['S'], 152 | 'queue' => $result['Item']['queue']['S'], 153 | 'payload' => $result['Item']['payload']['S'], 154 | 'exception' => $result['Item']['exception']['S'], 155 | 'failed_at' => Carbon::createFromTimestamp( 156 | (int) $result['Item']['failed_at']['N'], date_default_timezone_get() 157 | )->format(DateTimeInterface::ISO8601), 158 | ]; 159 | } 160 | 161 | /** 162 | * Delete a single failed job from storage. 163 | * 164 | * @param mixed $id 165 | * @return bool 166 | */ 167 | public function forget($id) 168 | { 169 | $this->dynamo->deleteItem([ 170 | 'TableName' => $this->table, 171 | 'Key' => [ 172 | 'application' => ['S' => $this->applicationName], 173 | 'uuid' => ['S' => $id], 174 | ], 175 | ]); 176 | 177 | return true; 178 | } 179 | 180 | /** 181 | * Flush all of the failed jobs from storage. 182 | * 183 | * @param int|null $hours 184 | * @return void 185 | * 186 | * @throws \Exception 187 | */ 188 | public function flush($hours = null) 189 | { 190 | throw new Exception("DynamoDb failed job storage may not be flushed. Please use DynamoDb's TTL features on your expires_at attribute."); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /Console/RetryCommand.php: -------------------------------------------------------------------------------- 1 | getJobIds()) > 0; 42 | 43 | if ($jobsFound) { 44 | $this->components->info('Pushing failed queue jobs back onto the queue.'); 45 | } 46 | 47 | foreach ($ids as $id) { 48 | $job = $this->laravel['queue.failer']->find($id); 49 | 50 | if (is_null($job)) { 51 | $this->components->error("Unable to find failed job with ID [{$id}]."); 52 | } else { 53 | $this->laravel['events']->dispatch(new JobRetryRequested($job)); 54 | 55 | $this->components->task($id, fn () => $this->retryJob($job)); 56 | 57 | $this->laravel['queue.failer']->forget($id); 58 | } 59 | } 60 | 61 | $jobsFound ? $this->newLine() : $this->components->info('No retryable jobs found.'); 62 | } 63 | 64 | /** 65 | * Get the job IDs to be retried. 66 | * 67 | * @return array 68 | */ 69 | protected function getJobIds() 70 | { 71 | $ids = (array) $this->argument('id'); 72 | 73 | if (count($ids) === 1 && $ids[0] === 'all') { 74 | $failer = $this->laravel['queue.failer']; 75 | 76 | return method_exists($failer, 'ids') 77 | ? $failer->ids() 78 | : Arr::pluck($failer->all(), 'id'); 79 | } 80 | 81 | if ($queue = $this->option('queue')) { 82 | return $this->getJobIdsByQueue($queue); 83 | } 84 | 85 | if ($ranges = (array) $this->option('range')) { 86 | $ids = array_merge($ids, $this->getJobIdsByRanges($ranges)); 87 | } 88 | 89 | return array_values(array_filter(array_unique($ids))); 90 | } 91 | 92 | /** 93 | * Get the job IDs by queue, if applicable. 94 | * 95 | * @param string $queue 96 | * @return array 97 | */ 98 | protected function getJobIdsByQueue($queue) 99 | { 100 | $failer = $this->laravel['queue.failer']; 101 | 102 | $ids = method_exists($failer, 'ids') 103 | ? $failer->ids($queue) 104 | : (new Collection($failer->all())) 105 | ->where('queue', $queue) 106 | ->pluck('id') 107 | ->toArray(); 108 | 109 | if (count($ids) === 0) { 110 | $this->components->error("Unable to find failed jobs for queue [{$queue}]."); 111 | } 112 | 113 | return $ids; 114 | } 115 | 116 | /** 117 | * Get the job IDs ranges, if applicable. 118 | * 119 | * @param array $ranges 120 | * @return array 121 | */ 122 | protected function getJobIdsByRanges(array $ranges) 123 | { 124 | $ids = []; 125 | 126 | foreach ($ranges as $range) { 127 | if (preg_match('/^[0-9]+\-[0-9]+$/', $range)) { 128 | $ids = array_merge($ids, range(...explode('-', $range))); 129 | } 130 | } 131 | 132 | return $ids; 133 | } 134 | 135 | /** 136 | * Retry the queue job. 137 | * 138 | * @param \stdClass $job 139 | * @return void 140 | */ 141 | protected function retryJob($job) 142 | { 143 | $this->laravel['queue']->connection($job->connection)->pushRaw( 144 | $this->refreshRetryUntil($this->resetAttempts($job->payload)), $job->queue 145 | ); 146 | } 147 | 148 | /** 149 | * Reset the payload attempts. 150 | * 151 | * Applicable to Redis and other jobs which store attempts in their payload. 152 | * 153 | * @param string $payload 154 | * @return string 155 | */ 156 | protected function resetAttempts($payload) 157 | { 158 | $payload = json_decode($payload, true); 159 | 160 | if (isset($payload['attempts'])) { 161 | $payload['attempts'] = 0; 162 | } 163 | 164 | return json_encode($payload); 165 | } 166 | 167 | /** 168 | * Refresh the "retry until" timestamp for the job. 169 | * 170 | * @param string $payload 171 | * @return string 172 | * 173 | * @throws \RuntimeException 174 | */ 175 | protected function refreshRetryUntil($payload) 176 | { 177 | $payload = json_decode($payload, true); 178 | 179 | if (! isset($payload['data']['command'])) { 180 | return json_encode($payload); 181 | } 182 | 183 | if (str_starts_with($payload['data']['command'], 'O:')) { 184 | $instance = unserialize($payload['data']['command']); 185 | } elseif ($this->laravel->bound(Encrypter::class)) { 186 | $instance = unserialize($this->laravel->make(Encrypter::class)->decrypt($payload['data']['command'])); 187 | } 188 | 189 | if (! isset($instance)) { 190 | throw new RuntimeException('Unable to extract job payload.'); 191 | } 192 | 193 | if (is_object($instance) && ! $instance instanceof \__PHP_Incomplete_Class && method_exists($instance, 'retryUntil')) { 194 | $retryUntil = $instance->retryUntil(); 195 | 196 | $payload['retryUntil'] = $retryUntil instanceof DateTimeInterface 197 | ? $retryUntil->getTimestamp() 198 | : $retryUntil; 199 | } 200 | 201 | return json_encode($payload); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /Listener.php: -------------------------------------------------------------------------------- 1 | commandPath = $commandPath; 56 | } 57 | 58 | /** 59 | * Get the PHP binary. 60 | * 61 | * @return string 62 | */ 63 | protected function phpBinary() 64 | { 65 | return php_binary(); 66 | } 67 | 68 | /** 69 | * Get the Artisan binary. 70 | * 71 | * @return string 72 | */ 73 | protected function artisanBinary() 74 | { 75 | return artisan_binary(); 76 | } 77 | 78 | /** 79 | * Listen to the given queue connection. 80 | * 81 | * @param string $connection 82 | * @param string $queue 83 | * @param \Illuminate\Queue\ListenerOptions $options 84 | * @return void 85 | */ 86 | public function listen($connection, $queue, ListenerOptions $options) 87 | { 88 | $process = $this->makeProcess($connection, $queue, $options); 89 | 90 | while (true) { 91 | $this->runProcess($process, $options->memory); 92 | 93 | if ($options->rest) { 94 | sleep($options->rest); 95 | } 96 | } 97 | } 98 | 99 | /** 100 | * Create a new Symfony process for the worker. 101 | * 102 | * @param string $connection 103 | * @param string $queue 104 | * @param \Illuminate\Queue\ListenerOptions $options 105 | * @return \Symfony\Component\Process\Process 106 | */ 107 | public function makeProcess($connection, $queue, ListenerOptions $options) 108 | { 109 | $command = $this->createCommand( 110 | $connection, 111 | $queue, 112 | $options 113 | ); 114 | 115 | // If the environment is set, we will append it to the command array so the 116 | // workers will run under the specified environment. Otherwise, they will 117 | // just run under the production environment which is not always right. 118 | if (isset($options->environment)) { 119 | $command = $this->addEnvironment($command, $options); 120 | } 121 | 122 | return new Process( 123 | $command, 124 | $this->commandPath, 125 | null, 126 | null, 127 | $options->timeout 128 | ); 129 | } 130 | 131 | /** 132 | * Add the environment option to the given command. 133 | * 134 | * @param array $command 135 | * @param \Illuminate\Queue\ListenerOptions $options 136 | * @return array 137 | */ 138 | protected function addEnvironment($command, ListenerOptions $options) 139 | { 140 | return array_merge($command, ["--env={$options->environment}"]); 141 | } 142 | 143 | /** 144 | * Create the command with the listener options. 145 | * 146 | * @param string $connection 147 | * @param string $queue 148 | * @param \Illuminate\Queue\ListenerOptions $options 149 | * @return array 150 | */ 151 | protected function createCommand($connection, $queue, ListenerOptions $options) 152 | { 153 | return array_filter([ 154 | $this->phpBinary(), 155 | $this->artisanBinary(), 156 | 'queue:work', 157 | $connection, 158 | '--once', 159 | "--name={$options->name}", 160 | "--queue={$queue}", 161 | "--backoff={$options->backoff}", 162 | "--memory={$options->memory}", 163 | "--sleep={$options->sleep}", 164 | "--tries={$options->maxTries}", 165 | $options->force ? '--force' : null, 166 | ], function ($value) { 167 | return ! is_null($value); 168 | }); 169 | } 170 | 171 | /** 172 | * Run the given process. 173 | * 174 | * @param \Symfony\Component\Process\Process $process 175 | * @param int $memory 176 | * @return void 177 | */ 178 | public function runProcess(Process $process, $memory) 179 | { 180 | $process->run(function ($type, $line) { 181 | $this->handleWorkerOutput($type, $line); 182 | }); 183 | 184 | // Once we have run the job we'll go check if the memory limit has been exceeded 185 | // for the script. If it has, we will kill this script so the process manager 186 | // will restart this with a clean slate of memory automatically on exiting. 187 | if ($this->memoryExceeded($memory)) { 188 | $this->stop(); 189 | } 190 | } 191 | 192 | /** 193 | * Handle output from the worker process. 194 | * 195 | * @param int $type 196 | * @param string $line 197 | * @return void 198 | */ 199 | protected function handleWorkerOutput($type, $line) 200 | { 201 | if (isset($this->outputHandler)) { 202 | call_user_func($this->outputHandler, $type, $line); 203 | } 204 | } 205 | 206 | /** 207 | * Determine if the memory limit has been exceeded. 208 | * 209 | * @param int $memoryLimit 210 | * @return bool 211 | */ 212 | public function memoryExceeded($memoryLimit) 213 | { 214 | return (memory_get_usage(true) / 1024 / 1024) >= $memoryLimit; 215 | } 216 | 217 | /** 218 | * Stop listening and bail out of the script. 219 | * 220 | * @return never 221 | */ 222 | public function stop() 223 | { 224 | exit; 225 | } 226 | 227 | /** 228 | * Set the output handler callback. 229 | * 230 | * @param \Closure $outputHandler 231 | * @return void 232 | */ 233 | public function setOutputHandler(Closure $outputHandler) 234 | { 235 | $this->outputHandler = $outputHandler; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /Failed/FileFailedJobProvider.php: -------------------------------------------------------------------------------- 1 | path = $path; 43 | $this->limit = $limit; 44 | $this->lockProviderResolver = $lockProviderResolver; 45 | } 46 | 47 | /** 48 | * Log a failed job into storage. 49 | * 50 | * @param string $connection 51 | * @param string $queue 52 | * @param string $payload 53 | * @param \Throwable $exception 54 | * @return int|null 55 | */ 56 | public function log($connection, $queue, $payload, $exception) 57 | { 58 | return $this->lock(function () use ($connection, $queue, $payload, $exception) { 59 | $id = json_decode($payload, true)['uuid']; 60 | 61 | $jobs = $this->read(); 62 | 63 | $failedAt = Date::now(); 64 | 65 | array_unshift($jobs, [ 66 | 'id' => $id, 67 | 'connection' => $connection, 68 | 'queue' => $queue, 69 | 'payload' => $payload, 70 | 'exception' => (string) mb_convert_encoding($exception, 'UTF-8'), 71 | 'failed_at' => $failedAt->format('Y-m-d H:i:s'), 72 | 'failed_at_timestamp' => $failedAt->getTimestamp(), 73 | ]); 74 | 75 | $this->write(array_slice($jobs, 0, $this->limit)); 76 | 77 | return $id; 78 | }); 79 | } 80 | 81 | /** 82 | * Get the IDs of all of the failed jobs. 83 | * 84 | * @param string|null $queue 85 | * @return array 86 | */ 87 | public function ids($queue = null) 88 | { 89 | return (new Collection($this->all())) 90 | ->when(! is_null($queue), fn ($collect) => $collect->where('queue', $queue)) 91 | ->pluck('id') 92 | ->all(); 93 | } 94 | 95 | /** 96 | * Get a list of all of the failed jobs. 97 | * 98 | * @return array 99 | */ 100 | public function all() 101 | { 102 | return $this->read(); 103 | } 104 | 105 | /** 106 | * Get a single failed job. 107 | * 108 | * @param mixed $id 109 | * @return object|null 110 | */ 111 | public function find($id) 112 | { 113 | return (new Collection($this->read())) 114 | ->first(fn ($job) => $job->id === $id); 115 | } 116 | 117 | /** 118 | * Delete a single failed job from storage. 119 | * 120 | * @param mixed $id 121 | * @return bool 122 | */ 123 | public function forget($id) 124 | { 125 | return $this->lock(function () use ($id) { 126 | $this->write($pruned = (new Collection($jobs = $this->read())) 127 | ->reject(fn ($job) => $job->id === $id) 128 | ->values() 129 | ->all()); 130 | 131 | return count($jobs) !== count($pruned); 132 | }); 133 | } 134 | 135 | /** 136 | * Flush all of the failed jobs from storage. 137 | * 138 | * @param int|null $hours 139 | * @return void 140 | */ 141 | public function flush($hours = null) 142 | { 143 | $this->prune(Date::now()->subHours($hours ?: 0)); 144 | } 145 | 146 | /** 147 | * Prune all of the entries older than the given date. 148 | * 149 | * @param \DateTimeInterface $before 150 | * @return int 151 | */ 152 | public function prune(DateTimeInterface $before) 153 | { 154 | return $this->lock(function () use ($before) { 155 | $jobs = $this->read(); 156 | 157 | $this->write($prunedJobs = (new Collection($jobs)) 158 | ->reject(fn ($job) => $job->failed_at_timestamp <= $before->getTimestamp()) 159 | ->values() 160 | ->all() 161 | ); 162 | 163 | return count($jobs) - count($prunedJobs); 164 | }); 165 | } 166 | 167 | /** 168 | * Execute the given callback while holding a lock. 169 | * 170 | * @param \Closure $callback 171 | * @return mixed 172 | */ 173 | protected function lock(Closure $callback) 174 | { 175 | if (! $this->lockProviderResolver) { 176 | return $callback(); 177 | } 178 | 179 | return ($this->lockProviderResolver)() 180 | ->lock('laravel-failed-jobs', 5) 181 | ->block(10, function () use ($callback) { 182 | return $callback(); 183 | }); 184 | } 185 | 186 | /** 187 | * Read the failed jobs file. 188 | * 189 | * @return array 190 | */ 191 | protected function read() 192 | { 193 | if (! file_exists($this->path)) { 194 | return []; 195 | } 196 | 197 | $content = file_get_contents($this->path); 198 | 199 | if (empty(trim($content))) { 200 | return []; 201 | } 202 | 203 | $content = json_decode($content); 204 | 205 | return is_array($content) ? $content : []; 206 | } 207 | 208 | /** 209 | * Write the given array of jobs to the failed jobs file. 210 | * 211 | * @param array $jobs 212 | * @return void 213 | */ 214 | protected function write(array $jobs) 215 | { 216 | file_put_contents( 217 | $this->path, 218 | json_encode($jobs, JSON_PRETTY_PRINT) 219 | ); 220 | } 221 | 222 | /** 223 | * Count the failed jobs. 224 | * 225 | * @param string|null $connection 226 | * @param string|null $queue 227 | * @return int 228 | */ 229 | public function count($connection = null, $queue = null) 230 | { 231 | if (($connection ?? $queue) === null) { 232 | return count($this->read()); 233 | } 234 | 235 | return (new Collection($this->read())) 236 | ->filter(fn ($job) => $job->connection === ($connection ?? $job->connection) && $job->queue === ($queue ?? $job->queue)) 237 | ->count(); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /SyncQueue.php: -------------------------------------------------------------------------------- 1 | dispatchAfterCommit = $dispatchAfterCommit; 26 | } 27 | 28 | /** 29 | * Get the size of the queue. 30 | * 31 | * @param string|null $queue 32 | * @return int 33 | */ 34 | public function size($queue = null) 35 | { 36 | return 0; 37 | } 38 | 39 | /** 40 | * Get the number of pending jobs. 41 | * 42 | * @param string|null $queue 43 | * @return int 44 | */ 45 | public function pendingSize($queue = null) 46 | { 47 | return 0; 48 | } 49 | 50 | /** 51 | * Get the number of delayed jobs. 52 | * 53 | * @param string|null $queue 54 | * @return int 55 | */ 56 | public function delayedSize($queue = null) 57 | { 58 | return 0; 59 | } 60 | 61 | /** 62 | * Get the number of reserved jobs. 63 | * 64 | * @param string|null $queue 65 | * @return int 66 | */ 67 | public function reservedSize($queue = null) 68 | { 69 | return 0; 70 | } 71 | 72 | /** 73 | * Get the creation timestamp of the oldest pending job, excluding delayed jobs. 74 | * 75 | * @param string|null $queue 76 | * @return int|null 77 | */ 78 | public function creationTimeOfOldestPendingJob($queue = null) 79 | { 80 | return null; 81 | } 82 | 83 | /** 84 | * Push a new job onto the queue. 85 | * 86 | * @param string $job 87 | * @param mixed $data 88 | * @param string|null $queue 89 | * @return mixed 90 | * 91 | * @throws \Throwable 92 | */ 93 | public function push($job, $data = '', $queue = null) 94 | { 95 | if ($this->shouldDispatchAfterCommit($job) && 96 | $this->container->bound('db.transactions')) { 97 | if ($job instanceof ShouldBeUnique) { 98 | $this->container->make('db.transactions')->addCallbackForRollback( 99 | function () use ($job) { 100 | (new UniqueLock($this->container->make(Cache::class)))->release($job); 101 | } 102 | ); 103 | } 104 | 105 | return $this->container->make('db.transactions')->addCallback( 106 | fn () => $this->executeJob($job, $data, $queue) 107 | ); 108 | } 109 | 110 | return $this->executeJob($job, $data, $queue); 111 | } 112 | 113 | /** 114 | * Execute a given job synchronously. 115 | * 116 | * @param string $job 117 | * @param mixed $data 118 | * @param string|null $queue 119 | * @return int 120 | * 121 | * @throws \Throwable 122 | */ 123 | protected function executeJob($job, $data = '', $queue = null) 124 | { 125 | $queueJob = $this->resolveJob($this->createPayload($job, $queue, $data), $queue); 126 | 127 | try { 128 | $this->raiseBeforeJobEvent($queueJob); 129 | 130 | $queueJob->fire(); 131 | 132 | $this->raiseAfterJobEvent($queueJob); 133 | } catch (Throwable $e) { 134 | $this->handleException($queueJob, $e); 135 | } 136 | 137 | return 0; 138 | } 139 | 140 | /** 141 | * Resolve a Sync job instance. 142 | * 143 | * @param string $payload 144 | * @param string $queue 145 | * @return \Illuminate\Queue\Jobs\SyncJob 146 | */ 147 | protected function resolveJob($payload, $queue) 148 | { 149 | return new SyncJob($this->container, $payload, $this->connectionName, $queue); 150 | } 151 | 152 | /** 153 | * Raise the before queue job event. 154 | * 155 | * @param \Illuminate\Contracts\Queue\Job $job 156 | * @return void 157 | */ 158 | protected function raiseBeforeJobEvent(Job $job) 159 | { 160 | if ($this->container->bound('events')) { 161 | $this->container['events']->dispatch(new JobProcessing($this->connectionName, $job)); 162 | } 163 | } 164 | 165 | /** 166 | * Raise the after queue job event. 167 | * 168 | * @param \Illuminate\Contracts\Queue\Job $job 169 | * @return void 170 | */ 171 | protected function raiseAfterJobEvent(Job $job) 172 | { 173 | if ($this->container->bound('events')) { 174 | $this->container['events']->dispatch(new JobProcessed($this->connectionName, $job)); 175 | } 176 | } 177 | 178 | /** 179 | * Raise the exception occurred queue job event. 180 | * 181 | * @param \Illuminate\Contracts\Queue\Job $job 182 | * @param \Throwable $e 183 | * @return void 184 | */ 185 | protected function raiseExceptionOccurredJobEvent(Job $job, Throwable $e) 186 | { 187 | if ($this->container->bound('events')) { 188 | $this->container['events']->dispatch(new JobExceptionOccurred($this->connectionName, $job, $e)); 189 | } 190 | } 191 | 192 | /** 193 | * Handle an exception that occurred while processing a job. 194 | * 195 | * @param \Illuminate\Contracts\Queue\Job $queueJob 196 | * @param \Throwable $e 197 | * @return void 198 | * 199 | * @throws \Throwable 200 | */ 201 | protected function handleException(Job $queueJob, Throwable $e) 202 | { 203 | $this->raiseExceptionOccurredJobEvent($queueJob, $e); 204 | 205 | $queueJob->fail($e); 206 | 207 | throw $e; 208 | } 209 | 210 | /** 211 | * Push a raw payload onto the queue. 212 | * 213 | * @param string $payload 214 | * @param string|null $queue 215 | * @param array $options 216 | * @return mixed 217 | */ 218 | public function pushRaw($payload, $queue = null, array $options = []) 219 | { 220 | // 221 | } 222 | 223 | /** 224 | * Push a new job onto the queue after (n) seconds. 225 | * 226 | * @param \DateTimeInterface|\DateInterval|int $delay 227 | * @param string $job 228 | * @param mixed $data 229 | * @param string|null $queue 230 | * @return mixed 231 | */ 232 | public function later($delay, $job, $data = '', $queue = null) 233 | { 234 | return $this->push($job, $data, $queue); 235 | } 236 | 237 | /** 238 | * Pop the next job off of the queue. 239 | * 240 | * @param string|null $queue 241 | * @return \Illuminate\Contracts\Queue\Job|null 242 | */ 243 | public function pop($queue = null) 244 | { 245 | // 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /InteractsWithQueue.php: -------------------------------------------------------------------------------- 1 | job ? $this->job->attempts() : 1; 33 | } 34 | 35 | /** 36 | * Delete the job from the queue. 37 | * 38 | * @return void 39 | */ 40 | public function delete() 41 | { 42 | if ($this->job) { 43 | return $this->job->delete(); 44 | } 45 | } 46 | 47 | /** 48 | * Fail the job from the queue. 49 | * 50 | * @param \Throwable|string|null $exception 51 | * @return void 52 | */ 53 | public function fail($exception = null) 54 | { 55 | if (is_string($exception)) { 56 | $exception = new ManuallyFailedException($exception); 57 | } 58 | 59 | if ($exception instanceof Throwable || is_null($exception)) { 60 | if ($this->job) { 61 | return $this->job->fail($exception); 62 | } 63 | } else { 64 | throw new InvalidArgumentException('The fail method requires a string or an instance of Throwable.'); 65 | } 66 | } 67 | 68 | /** 69 | * Release the job back into the queue after (n) seconds. 70 | * 71 | * @param \DateTimeInterface|\DateInterval|int $delay 72 | * @return void 73 | */ 74 | public function release($delay = 0) 75 | { 76 | $delay = $delay instanceof DateTimeInterface 77 | ? $this->secondsUntil($delay) 78 | : $delay; 79 | 80 | if ($this->job) { 81 | return $this->job->release($delay); 82 | } 83 | } 84 | 85 | /** 86 | * Indicate that queue interactions like fail, delete, and release should be faked. 87 | * 88 | * @return $this 89 | */ 90 | public function withFakeQueueInteractions() 91 | { 92 | $this->job = new FakeJob; 93 | 94 | return $this; 95 | } 96 | 97 | /** 98 | * Assert that the job was deleted from the queue. 99 | * 100 | * @return $this 101 | */ 102 | public function assertDeleted() 103 | { 104 | $this->ensureQueueInteractionsHaveBeenFaked(); 105 | 106 | PHPUnit::assertTrue( 107 | $this->job->isDeleted(), 108 | 'Job was expected to be deleted, but was not.' 109 | ); 110 | 111 | return $this; 112 | } 113 | 114 | /** 115 | * Assert that the job was not deleted from the queue. 116 | * 117 | * @return $this 118 | */ 119 | public function assertNotDeleted() 120 | { 121 | $this->ensureQueueInteractionsHaveBeenFaked(); 122 | 123 | PHPUnit::assertTrue( 124 | ! $this->job->isDeleted(), 125 | 'Job was unexpectedly deleted.' 126 | ); 127 | 128 | return $this; 129 | } 130 | 131 | /** 132 | * Assert that the job was manually failed. 133 | * 134 | * @return $this 135 | */ 136 | public function assertFailed() 137 | { 138 | $this->ensureQueueInteractionsHaveBeenFaked(); 139 | 140 | PHPUnit::assertTrue( 141 | $this->job->hasFailed(), 142 | 'Job was expected to be manually failed, but was not.' 143 | ); 144 | 145 | return $this; 146 | } 147 | 148 | /** 149 | * Assert that the job was manually failed with a specific exception. 150 | * 151 | * @param \Throwable|string $exception 152 | * @return $this 153 | */ 154 | public function assertFailedWith($exception) 155 | { 156 | $this->assertFailed(); 157 | 158 | if (is_string($exception) && class_exists($exception)) { 159 | PHPUnit::assertInstanceOf( 160 | $exception, 161 | $this->job->failedWith, 162 | 'Expected job to be manually failed with ['.$exception.'] but job failed with ['.get_class($this->job->failedWith).'].' 163 | ); 164 | 165 | return $this; 166 | } 167 | 168 | if (is_string($exception)) { 169 | $exception = new ManuallyFailedException($exception); 170 | } 171 | 172 | if ($exception instanceof Throwable) { 173 | PHPUnit::assertInstanceOf( 174 | get_class($exception), 175 | $this->job->failedWith, 176 | 'Expected job to be manually failed with ['.get_class($exception).'] but job failed with ['.get_class($this->job->failedWith).'].' 177 | ); 178 | 179 | PHPUnit::assertEquals( 180 | $exception->getCode(), 181 | $this->job->failedWith->getCode(), 182 | 'Expected exception code ['.$exception->getCode().'] but job failed with exception code ['.$this->job->failedWith->getCode().'].' 183 | ); 184 | 185 | PHPUnit::assertEquals( 186 | $exception->getMessage(), 187 | $this->job->failedWith->getMessage(), 188 | 'Expected exception message ['.$exception->getMessage().'] but job failed with exception message ['.$this->job->failedWith->getMessage().'].'); 189 | } 190 | 191 | return $this; 192 | } 193 | 194 | /** 195 | * Assert that the job was not manually failed. 196 | * 197 | * @return $this 198 | */ 199 | public function assertNotFailed() 200 | { 201 | $this->ensureQueueInteractionsHaveBeenFaked(); 202 | 203 | PHPUnit::assertTrue( 204 | ! $this->job->hasFailed(), 205 | 'Job was unexpectedly failed manually.' 206 | ); 207 | 208 | return $this; 209 | } 210 | 211 | /** 212 | * Assert that the job was released back onto the queue. 213 | * 214 | * @param \DateTimeInterface|\DateInterval|int|null $delay 215 | * @return $this 216 | */ 217 | public function assertReleased($delay = null) 218 | { 219 | $this->ensureQueueInteractionsHaveBeenFaked(); 220 | 221 | $delay = $delay instanceof DateTimeInterface 222 | ? $this->secondsUntil($delay) 223 | : $delay; 224 | 225 | PHPUnit::assertTrue( 226 | $this->job->isReleased(), 227 | 'Job was expected to be released, but was not.' 228 | ); 229 | 230 | if (! is_null($delay)) { 231 | PHPUnit::assertSame( 232 | $delay, 233 | $this->job->releaseDelay, 234 | "Expected job to be released with delay of [{$delay}] seconds, but was released with delay of [{$this->job->releaseDelay}] seconds." 235 | ); 236 | } 237 | 238 | return $this; 239 | } 240 | 241 | /** 242 | * Assert that the job was not released back onto the queue. 243 | * 244 | * @return $this 245 | */ 246 | public function assertNotReleased() 247 | { 248 | $this->ensureQueueInteractionsHaveBeenFaked(); 249 | 250 | PHPUnit::assertTrue( 251 | ! $this->job->isReleased(), 252 | 'Job was unexpectedly released.' 253 | ); 254 | 255 | return $this; 256 | } 257 | 258 | /** 259 | * Ensure that queue interactions have been faked. 260 | * 261 | * @return void 262 | */ 263 | private function ensureQueueInteractionsHaveBeenFaked() 264 | { 265 | if (! $this->job instanceof FakeJob) { 266 | throw new RuntimeException('Queue interactions have not been faked.'); 267 | } 268 | } 269 | 270 | /** 271 | * Set the base queue job instance. 272 | * 273 | * @param \Illuminate\Contracts\Queue\Job $job 274 | * @return $this 275 | */ 276 | public function setJob(JobContract $job) 277 | { 278 | $this->job = $job; 279 | 280 | return $this; 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /BeanstalkdQueue.php: -------------------------------------------------------------------------------- 1 | default = $default; 60 | $this->blockFor = $blockFor; 61 | $this->timeToRun = $timeToRun; 62 | $this->pheanstalk = $pheanstalk; 63 | $this->dispatchAfterCommit = $dispatchAfterCommit; 64 | } 65 | 66 | /** 67 | * Get the size of the queue. 68 | * 69 | * @param string|null $queue 70 | * @return int 71 | */ 72 | public function size($queue = null) 73 | { 74 | $stats = $this->pheanstalk->statsTube(new TubeName($this->getQueue($queue))); 75 | 76 | return $stats->currentJobsReady 77 | + $stats->currentJobsDelayed 78 | + $stats->currentJobsReserved; 79 | } 80 | 81 | /** 82 | * Get the number of pending jobs. 83 | * 84 | * @param string|null $queue 85 | * @return int 86 | */ 87 | public function pendingSize($queue = null) 88 | { 89 | return $this->pheanstalk->statsTube(new TubeName($this->getQueue($queue)))->currentJobsReady; 90 | } 91 | 92 | /** 93 | * Get the number of delayed jobs. 94 | * 95 | * @param string|null $queue 96 | * @return int 97 | */ 98 | public function delayedSize($queue = null) 99 | { 100 | return $this->pheanstalk->statsTube(new TubeName($this->getQueue($queue)))->currentJobsDelayed; 101 | } 102 | 103 | /** 104 | * Get the number of reserved jobs. 105 | * 106 | * @param string|null $queue 107 | * @return int 108 | */ 109 | public function reservedSize($queue = null) 110 | { 111 | return $this->pheanstalk->statsTube(new TubeName($this->getQueue($queue)))->currentJobsReserved; 112 | } 113 | 114 | /** 115 | * Get the creation timestamp of the oldest pending job, excluding delayed jobs. 116 | * 117 | * @param string|null $queue 118 | * @return int|null 119 | */ 120 | public function creationTimeOfOldestPendingJob($queue = null) 121 | { 122 | // Not supported by Beanstalkd... 123 | return null; 124 | } 125 | 126 | /** 127 | * Push a new job onto the queue. 128 | * 129 | * @param string $job 130 | * @param mixed $data 131 | * @param string|null $queue 132 | * @return mixed 133 | */ 134 | public function push($job, $data = '', $queue = null) 135 | { 136 | return $this->enqueueUsing( 137 | $job, 138 | $this->createPayload($job, $this->getQueue($queue), $data), 139 | $queue, 140 | null, 141 | function ($payload, $queue) { 142 | return $this->pushRaw($payload, $queue); 143 | } 144 | ); 145 | } 146 | 147 | /** 148 | * Push a raw payload onto the queue. 149 | * 150 | * @param string $payload 151 | * @param string|null $queue 152 | * @param array $options 153 | * @return mixed 154 | */ 155 | public function pushRaw($payload, $queue = null, array $options = []) 156 | { 157 | $this->pheanstalk->useTube(new TubeName($this->getQueue($queue))); 158 | 159 | return $this->pheanstalk->put( 160 | $payload, Pheanstalk::DEFAULT_PRIORITY, Pheanstalk::DEFAULT_DELAY, $this->timeToRun 161 | ); 162 | } 163 | 164 | /** 165 | * Push a new job onto the queue after (n) seconds. 166 | * 167 | * @param \DateTimeInterface|\DateInterval|int $delay 168 | * @param string $job 169 | * @param mixed $data 170 | * @param string|null $queue 171 | * @return mixed 172 | */ 173 | public function later($delay, $job, $data = '', $queue = null) 174 | { 175 | return $this->enqueueUsing( 176 | $job, 177 | $this->createPayload($job, $this->getQueue($queue), $data, $delay), 178 | $queue, 179 | $delay, 180 | function ($payload, $queue, $delay) { 181 | $this->pheanstalk->useTube(new TubeName($this->getQueue($queue))); 182 | 183 | return $this->pheanstalk->put( 184 | $payload, 185 | Pheanstalk::DEFAULT_PRIORITY, 186 | $this->secondsUntil($delay), 187 | $this->timeToRun 188 | ); 189 | } 190 | ); 191 | } 192 | 193 | /** 194 | * Push an array of jobs onto the queue. 195 | * 196 | * @param array $jobs 197 | * @param mixed $data 198 | * @param string|null $queue 199 | * @return void 200 | */ 201 | public function bulk($jobs, $data = '', $queue = null) 202 | { 203 | foreach ((array) $jobs as $job) { 204 | if (isset($job->delay)) { 205 | $this->later($job->delay, $job, $data, $queue); 206 | } else { 207 | $this->push($job, $data, $queue); 208 | } 209 | } 210 | } 211 | 212 | /** 213 | * Pop the next job off of the queue. 214 | * 215 | * @param string|null $queue 216 | * @return \Illuminate\Contracts\Queue\Job|null 217 | */ 218 | public function pop($queue = null) 219 | { 220 | $this->pheanstalk->watch( 221 | $tube = new TubeName($queue = $this->getQueue($queue)) 222 | ); 223 | 224 | foreach ($this->pheanstalk->listTubesWatched() as $watched) { 225 | if ($watched->value !== $tube->value) { 226 | $this->pheanstalk->ignore($watched); 227 | } 228 | } 229 | 230 | $job = $this->pheanstalk->reserveWithTimeout($this->blockFor); 231 | 232 | if ($job instanceof JobIdInterface) { 233 | return new BeanstalkdJob( 234 | $this->container, $this->pheanstalk, $job, $this->connectionName, $queue 235 | ); 236 | } 237 | } 238 | 239 | /** 240 | * Delete a message from the Beanstalk queue. 241 | * 242 | * @param string $queue 243 | * @param string|int $id 244 | * @return void 245 | */ 246 | public function deleteMessage($queue, $id) 247 | { 248 | $this->pheanstalk->useTube(new TubeName($this->getQueue($queue))); 249 | 250 | $this->pheanstalk->delete(new Job(new JobId($id), '')); 251 | } 252 | 253 | /** 254 | * Get the queue or return the default. 255 | * 256 | * @param string|null $queue 257 | * @return string 258 | */ 259 | public function getQueue($queue) 260 | { 261 | return $queue ?: $this->default; 262 | } 263 | 264 | /** 265 | * Get the underlying Pheanstalk instance. 266 | * 267 | * @return \Pheanstalk\Contract\PheanstalkManagerInterface&\Pheanstalk\Contract\PheanstalkPublisherInterface&\Pheanstalk\Contract\PheanstalkSubscriberInterface 268 | */ 269 | public function getPheanstalk() 270 | { 271 | return $this->pheanstalk; 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /Middleware/ThrottlesExceptions.php: -------------------------------------------------------------------------------- 1 | maxAttempts = $maxAttempts; 97 | $this->decaySeconds = $decaySeconds; 98 | } 99 | 100 | /** 101 | * Process the job. 102 | * 103 | * @param mixed $job 104 | * @param callable $next 105 | * @return mixed 106 | */ 107 | public function handle($job, $next) 108 | { 109 | $this->limiter = Container::getInstance()->make(RateLimiter::class); 110 | 111 | if ($this->limiter->tooManyAttempts($jobKey = $this->getKey($job), $this->maxAttempts)) { 112 | return $job->release($this->getTimeUntilNextRetry($jobKey)); 113 | } 114 | 115 | try { 116 | $next($job); 117 | 118 | $this->limiter->clear($jobKey); 119 | } catch (Throwable $throwable) { 120 | if ($this->whenCallback && ! call_user_func($this->whenCallback, $throwable, $this->limiter)) { 121 | throw $throwable; 122 | } 123 | 124 | if ($this->reportCallback && call_user_func($this->reportCallback, $throwable, $this->limiter)) { 125 | report($throwable); 126 | } 127 | 128 | if ($this->shouldDelete($throwable)) { 129 | return $job->delete(); 130 | } 131 | 132 | if ($this->shouldFail($throwable)) { 133 | return $job->fail($throwable); 134 | } 135 | 136 | $this->limiter->hit($jobKey, $this->decaySeconds); 137 | 138 | return $job->release($this->retryAfterMinutes * 60); 139 | } 140 | } 141 | 142 | /** 143 | * Specify a callback that should determine if rate limiting behavior should apply. 144 | * 145 | * @param callable $callback 146 | * @return $this 147 | */ 148 | public function when(callable $callback) 149 | { 150 | $this->whenCallback = $callback; 151 | 152 | return $this; 153 | } 154 | 155 | /** 156 | * Add a callback that should determine if the job should be deleted. 157 | * 158 | * @param callable|string $callback 159 | * @return $this 160 | */ 161 | public function deleteWhen(callable|string $callback) 162 | { 163 | $this->deleteWhenCallbacks[] = is_string($callback) 164 | ? fn (Throwable $e) => $e instanceof $callback 165 | : $callback; 166 | 167 | return $this; 168 | } 169 | 170 | /** 171 | * Add a callback that should determine if the job should be failed. 172 | * 173 | * @param callable|string $callback 174 | * @return $this 175 | */ 176 | public function failWhen(callable|string $callback) 177 | { 178 | $this->failWhenCallbacks[] = is_string($callback) 179 | ? fn (Throwable $e) => $e instanceof $callback 180 | : $callback; 181 | 182 | return $this; 183 | } 184 | 185 | /** 186 | * Run the skip / delete callbacks to determine if the job should be deleted for the given exception. 187 | * 188 | * @param Throwable $throwable 189 | * @return bool 190 | */ 191 | protected function shouldDelete(Throwable $throwable): bool 192 | { 193 | foreach ($this->deleteWhenCallbacks as $callback) { 194 | if (call_user_func($callback, $throwable)) { 195 | return true; 196 | } 197 | } 198 | 199 | return false; 200 | } 201 | 202 | /** 203 | * Run the skip / fail callbacks to determine if the job should be failed for the given exception. 204 | * 205 | * @param Throwable $throwable 206 | * @return bool 207 | */ 208 | protected function shouldFail(Throwable $throwable): bool 209 | { 210 | foreach ($this->failWhenCallbacks as $callback) { 211 | if (call_user_func($callback, $throwable)) { 212 | return true; 213 | } 214 | } 215 | 216 | return false; 217 | } 218 | 219 | /** 220 | * Set the prefix of the rate limiter key. 221 | * 222 | * @param string $prefix 223 | * @return $this 224 | */ 225 | public function withPrefix(string $prefix) 226 | { 227 | $this->prefix = $prefix; 228 | 229 | return $this; 230 | } 231 | 232 | /** 233 | * Specify the number of minutes a job should be delayed when it is released (before it has reached its max exceptions). 234 | * 235 | * @param int $backoff 236 | * @return $this 237 | */ 238 | public function backoff($backoff) 239 | { 240 | $this->retryAfterMinutes = $backoff; 241 | 242 | return $this; 243 | } 244 | 245 | /** 246 | * Get the cache key associated for the rate limiter. 247 | * 248 | * @param mixed $job 249 | * @return string 250 | */ 251 | protected function getKey($job) 252 | { 253 | if ($this->key) { 254 | return $this->prefix.$this->key; 255 | } elseif ($this->byJob) { 256 | return $this->prefix.$job->job->uuid(); 257 | } 258 | 259 | $jobName = method_exists($job, 'displayName') 260 | ? $job->displayName() 261 | : get_class($job); 262 | 263 | return $this->prefix.hash('xxh128', $jobName); 264 | } 265 | 266 | /** 267 | * Set the value that the rate limiter should be keyed by. 268 | * 269 | * @param string $key 270 | * @return $this 271 | */ 272 | public function by($key) 273 | { 274 | $this->key = $key; 275 | 276 | return $this; 277 | } 278 | 279 | /** 280 | * Indicate that the throttle key should use the job's UUID. 281 | * 282 | * @return $this 283 | */ 284 | public function byJob() 285 | { 286 | $this->byJob = true; 287 | 288 | return $this; 289 | } 290 | 291 | /** 292 | * Report exceptions and optionally specify a callback that determines if the exception should be reported. 293 | * 294 | * @param callable|null $callback 295 | * @return $this 296 | */ 297 | public function report(?callable $callback = null) 298 | { 299 | $this->reportCallback = $callback ?? fn () => true; 300 | 301 | return $this; 302 | } 303 | 304 | /** 305 | * Get the number of seconds that should elapse before the job is retried. 306 | * 307 | * @param string $key 308 | * @return int 309 | */ 310 | protected function getTimeUntilNextRetry($key) 311 | { 312 | return $this->limiter->availableIn($key) + 3; 313 | } 314 | } 315 | --------------------------------------------------------------------------------