├── .editorconfig ├── .github └── workflows │ ├── build-laravel.yml │ ├── build.yml │ └── try-installation.yml ├── .gitignore ├── .php-cs-fixer.php ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Database │ └── Migrations │ │ └── 2022_03_31_17_00_00_create_priority_jobs_table.php ├── PriorityQueueServiceProvider.php ├── Queue │ ├── DatabasePriorityConnector.php │ └── DatabasePriorityQueue.php └── Traits │ └── UseJobPrioritization.php └── tests ├── Feature └── PriorityQueueWorkCommandTest.php ├── TestCase.php └── Unit └── Queue ├── DatabasePriorityConnectorTest.php └── DatabasePriorityQueueTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | 17 | [composer.json] 18 | indent_size = 4 19 | indent_style = space -------------------------------------------------------------------------------- /.github/workflows/build-laravel.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test (Laravel 10, 11) 2 | env: 3 | TESTING_ENV: ${{ secrets.TESTING_ENV }} 4 | 5 | on: 6 | pull_request: 7 | branches: 8 | - 'main' 9 | types: [ opened, synchronize, reopened, ready_for_review ] 10 | push: 11 | branches: 12 | - 'main' 13 | 14 | jobs: 15 | build: 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | version: ['10', '11'] 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | if: success() 24 | 25 | - name: Setup PHP with coverage driver 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: 8.2 29 | coverage: pcov 30 | 31 | - name: Setup 32 | if: success() 33 | run: | 34 | sudo service mysql start 35 | php -v 36 | mysql -uroot -proot -e "CREATE DATABASE priority_queue;" 37 | composer install --no-interaction 38 | echo "$TESTING_ENV" > .env.testing 39 | 40 | - name: Laravel 10 composition 41 | if: matrix.version == '10' 42 | run: | 43 | composer require "laravel/framework" "^10" \ 44 | "orchestra/testbench" "^8" \ 45 | --with-all-dependencies 46 | 47 | - name: Laravel 11 composition 48 | if: matrix.version == '11' 49 | run: | 50 | composer require "laravel/framework" "^11" \ 51 | "orchestra/testbench" "^9" \ 52 | --with-all-dependencies 53 | 54 | - name: PHPUnit tests 55 | if: success() 56 | run: | 57 | composer test 58 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test (PHP 8.2, 8.3) 2 | env: 3 | TESTING_ENV: ${{ secrets.TESTING_ENV }} 4 | 5 | on: 6 | pull_request: 7 | branches: 8 | - 'main' 9 | types: [ opened, synchronize, reopened, ready_for_review ] 10 | push: 11 | branches: 12 | - 'main' 13 | 14 | jobs: 15 | build: 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | version: [ '8.2', '8.3' ] 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | if: success() 24 | 25 | - name: Setup PHP with coverage driver 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: ${{ matrix.version }} 29 | coverage: pcov 30 | 31 | - name: Setup 32 | if: success() 33 | run: | 34 | sudo service mysql start 35 | php -v 36 | mysql -uroot -proot -e "CREATE DATABASE priority_queue;" 37 | composer install --no-interaction 38 | echo "$TESTING_ENV" > .env.testing 39 | 40 | - name: PHPUnit tests with coverage 41 | if: success() 42 | run: | 43 | composer test-coverage 44 | 45 | - name: upload coverage to codecov.io 46 | if: success() && matrix.version == '8.2' 47 | uses: codecov/codecov-action@v3 48 | with: 49 | token: ${{ secrets.CODECOV_TOKEN }} 50 | file: ./coverage.xml 51 | -------------------------------------------------------------------------------- /.github/workflows/try-installation.yml: -------------------------------------------------------------------------------- 1 | name: Try Install Package (Laravel 9, 10, 11) 2 | env: 3 | LOCAL_ENV: ${{ secrets.LOCAL_ENV }} 4 | 5 | on: 6 | release: 7 | types: [published] 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | version: [ '^10.0', '^11.0' ] 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Setup PHP 18 | uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: 8.2 21 | coverage: pcov 22 | 23 | - name: Setup and install package on Laravel 24 | if: success() 25 | run: | 26 | sudo service mysql start 27 | mysql -uroot -proot -e "CREATE DATABASE priority_queue;" 28 | composer create-project laravel/laravel:${{ matrix.version }} laravel 29 | cd laravel 30 | composer require shipsaas/laravel-priority-queue 31 | php artisan vendor:publish --tag=priority-queue-migrations 32 | echo "$LOCAL_ENV" > .env 33 | php artisan migrate 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | node_modules/ 3 | npm-debug.log 4 | yarn-error.log 5 | 6 | # Laravel 4 specific 7 | bootstrap/compiled.php 8 | app/storage/ 9 | 10 | # Laravel 5 & Lumen specific 11 | public/storage 12 | public/hot 13 | 14 | # Laravel 5 & Lumen specific with changed public path 15 | public_html/storage 16 | public_html/hot 17 | 18 | storage/*.key 19 | .env 20 | Homestead.yaml 21 | Homestead.json 22 | /.vagrant 23 | .phpunit.result.cache 24 | 25 | .idea/ 26 | .php-cs-fixer.cache 27 | .phpunit.cache 28 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in([ 8 | __DIR__ . '/src/', 9 | __DIR__ . '/tests/', 10 | ]); 11 | 12 | $config = new Config(); 13 | 14 | return $config->setFinder($finder) 15 | ->setRules([ 16 | '@PSR12' => true, 17 | 'array_syntax' => ['syntax' => 'short'], 18 | 'combine_consecutive_unsets' => true, 19 | 'multiline_whitespace_before_semicolons' => true, 20 | 'single_quote' => true, 21 | 'binary_operator_spaces' => ['default' => 'single_space'], 22 | 'blank_line_before_statement' => ['statements' => ['return']], 23 | 'braces' => [ 24 | 'allow_single_line_closure' => true, 25 | 'position_after_anonymous_constructs' => 'same', 26 | 'position_after_control_structures' => 'same', 27 | 'position_after_functions_and_oop_constructs' => 'next', 28 | ], 29 | 'combine_consecutive_issets' => true, 30 | 'class_attributes_separation' => ['elements' => ['method' => 'one']], 31 | 'concat_space' => ['spacing' => 'one'], 32 | 'include' => true, 33 | 'no_extra_blank_lines' => [ 34 | 'tokens' => [ 35 | 'curly_brace_block', 36 | 'extra', 37 | 'parenthesis_brace_block', 38 | 'square_brace_block', 39 | 'throw', 40 | 'use', 41 | ], 42 | ], 43 | 'no_multiline_whitespace_around_double_arrow' => true, 44 | 'no_spaces_around_offset' => true, 45 | 'no_unused_imports' => true, 46 | 'no_whitespace_before_comma_in_array' => true, 47 | 'object_operator_without_whitespace' => true, 48 | 'php_unit_fqcn_annotation' => true, 49 | 'phpdoc_no_package' => true, 50 | 'phpdoc_scalar' => true, 51 | 'phpdoc_single_line_var_spacing' => true, 52 | 'protected_to_private' => true, 53 | 'return_assignment' => true, 54 | 'no_useless_return' => true, 55 | 'simplified_null_return' => true, 56 | 'single_line_after_imports' => true, 57 | 'single_line_comment_style' => ['comment_types' => ['hash']], 58 | 'single_class_element_per_statement' => true, 59 | 'trailing_comma_in_multiline' => ['elements' => ['arrays']], 60 | 'trim_array_spaces' => true, 61 | 'unary_operator_spaces' => true, 62 | 'whitespace_after_comma_in_array' => true, 63 | 'no_null_property_initialization' => true, 64 | 65 | 'function_typehint_space' => true, 66 | 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], 67 | 'no_empty_statement' => true, 68 | 'no_leading_namespace_whitespace' => true, 69 | 'return_type_declaration' => ['space_before' => 'none'], 70 | 71 | 'method_chaining_indentation' => true, 72 | 'align_multiline_comment' => ['comment_type' => 'all_multiline'], 73 | 'no_superfluous_phpdoc_tags' => [ 74 | 'allow_mixed' => false, 75 | 'remove_inheritdoc' => false, 76 | 'allow_unused_params' => false, 77 | ], 78 | 'phpdoc_trim_consecutive_blank_line_separation' => true, 79 | 'phpdoc_trim' => true, 80 | 'no_empty_phpdoc' => true, 81 | 'clean_namespace' => true, 82 | 'array_indentation' => true, 83 | 'elseif' => true, 84 | 'phpdoc_order' => true, 85 | 'global_namespace_import' => [ 86 | 'import_classes' => true, 87 | 'import_constants' => false, 88 | 'import_functions' => false, 89 | ], 90 | 'fully_qualified_strict_types' => true, 91 | 'no_leading_import_slash' => true, 92 | ]) 93 | ->setLineEnding("\n"); 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Seth Phat 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ShipSaaS - Laravel Priority Queue Driver 2 | 3 | [![Latest Version](http://poser.pugx.org/shipsaas/laravel-priority-queue/v)](https://packagist.org/packages/shipsaas/laravel-priority-queue) 4 | [![Total Downloads](http://poser.pugx.org/shipsaas/laravel-priority-queue/downloads)](https://packagist.org/packages/shipsaas/laravel-priority-queue) 5 | [![codecov](https://codecov.io/gh/shipsaas/laravel-priority-queue/branch/main/graph/badge.svg?token=V3HOOR12HA)](https://codecov.io/gh/shipsaas/laravel-priority-queue) 6 | [![Build & Test](https://github.com/shipsaas/laravel-priority-queue/actions/workflows/build.yml/badge.svg)](https://github.com/shipsaas/laravel-priority-queue/actions/workflows/build.yml) 7 | [![Build & Test (Laravel 10, 11)](https://github.com/shipsaas/laravel-priority-queue/actions/workflows/build-laravel.yml/badge.svg)](https://github.com/shipsaas/laravel-priority-queue/actions/workflows/build-laravel.yml) 8 | 9 | A simple Priority Queue Driver for your Laravel Applications to serve your priority messages and 10 | makes users happy 🔋. 11 | 12 | With the famous Repository Pattern of Laravel, Priority Queue Driver is easily get injected into 13 | Laravel's Lifecycle without any hassle/hurdle. 14 | 15 | We can use built-in artisan command `php artisan queue:work` 😎. 16 | 17 | ## Supports 18 | - Laravel 11 (supports by default) 19 | - Laravel 10 (supports until Laravel drops the bug fixes at [August 6th, 2024](https://laravel.com/docs/11.x/releases)) 20 | - PHP 8.2 & 8.3 21 | - Any database that Laravel supported. 22 | 23 | ## Architecture Diagram 24 | 25 | ![Seth Phat - Laravel Priority Queue](https://i.imgur.com/H8OEMhQ.png) 26 | 27 | 28 | ### Why Priority Queue Driver use Database? 29 | 30 | - Everybody knows Database (MySQL, PgSQL, etc) 👀. 31 | - Easy and simple to implement ❤️. 32 | - Utilize the `ORDER BY` and `INDEX` for fast queue msgs pop process. Faster than any other stuff 🔥. 33 | - Highest visibility (you can view the jobs and their data in DB) ⭐️. 34 | - Highest flexibility (you can change the weight directly in DB to unblock important msgs) 💰. 35 | - No extra tool involved. Just Laravel 🥰. 36 | 37 | ## Install Laravel Priority Queue 38 | 39 | ```bash 40 | composer require shipsaas/laravel-priority-queue 41 | ``` 42 | 43 | ### One-Time Setup 44 | 45 | Export and run the migration (one-time). We don't load migration by default just in case you want to customize the migration schema 😎. 46 | 47 | ```bash 48 | php artisan vendor:publish --tag=priority-queue-migrations 49 | php artisan migrate 50 | ``` 51 | 52 | Open `config/queue.php` and add this to the `connections` array: 53 | 54 | ```php 55 | 'connections' => [ 56 | // ... a lot of connections above 57 | // then our lovely guy here 58 | 'database-priority' => [ 59 | 'driver' => 'database-priority', 60 | 'connection' => 'mysql', 61 | 'table' => 'priority_jobs', 62 | 'queue' => 'default', 63 | 'retry_after' => 90, 64 | 'after_commit' => false, // or true, depends on your need 65 | ], 66 | ], 67 | ``` 68 | 69 | ## Scale/Reliability Consideration 70 | 71 | It is recommended to use a different database connection (eg `mysql_secondary`) to avoid the worker processes ramming your 72 | primary database. 73 | 74 | ## Usage 75 | 76 | ### The Job Weight 77 | 78 | The default job weight is **500**. 79 | 80 | You can define a hardcoded weight for your job by using the `$jobWeight` property. 81 | 82 | ```php 83 | class SendEmail implements ShouldQueue 84 | { 85 | public int $jobWeight = 500; 86 | } 87 | ``` 88 | 89 | Or if you want to calculate the job weight on runtime, you can use the `UseJobPrioritization` trait: 90 | 91 | ```php 92 | use ShipSaasPriorityQueue\Traits\UseJobPrioritization; 93 | 94 | class SendEmail implements ShouldQueue 95 | { 96 | use UseJobPrioritization; 97 | 98 | public function getJobWeight() : int 99 | { 100 | return $this->user->isUsingProPlan() 101 | ? 1000 102 | : 500; 103 | } 104 | } 105 | ``` 106 | 107 | ### Dispatch the Queue 108 | 109 | You can use the normal Dispatcher or Queue Facade,... to dispatch the Queue Msgs 110 | 111 | ### As primary queue 112 | 113 | ```env 114 | QUEUE_CONNECTION=database-priority 115 | ``` 116 | 117 | And you're ready to roll. 118 | 119 | ### As secondary queue 120 | Specify the `database-priority` connection when dispatching a queue msg. 121 | 122 | ```php 123 | // use Dispatcher 124 | SendEmail::dispatch($user, $emailContent) 125 | ->onConnection('database-priority'); 126 | 127 | // use Queue Facade 128 | use Illuminate\Support\Facades\Queue; 129 | 130 | Queue::connection('database-priority') 131 | ->push(new SendEmail($user, $emailContent)); 132 | ``` 133 | 134 | I get that you guys might don't want to explicitly put the connection name. Alternatively, you can do this: 135 | 136 | ```php 137 | class SendEmail implements ShouldQueue 138 | { 139 | // first option 140 | public $connection = 'database-priority'; 141 | 142 | public function __construct() 143 | { 144 | // second option 145 | $this->onConnection('database-priority'); 146 | } 147 | } 148 | ``` 149 | 150 | ## Run The Queue Worker 151 | 152 | Nothing different from the Laravel Documentation 😎. Just need to include the `database-priority` driver. 153 | 154 | ```bash 155 | php artisan queue:work database-priority 156 | 157 | # Extra win, priority on topic 158 | php artisan queue:work database-priority --queue=custom 159 | ``` 160 | 161 | ## Testing 162 | 163 | Run `composer test` 😆 164 | 165 | Available Tests: 166 | 167 | - Unit Testing 168 | - Integration Testing against MySQL and `queue:work` command 169 | 170 | ## Contributors 171 | - Seth Phat 172 | 173 | ## Contributions & Support the Project 174 | 175 | Feel free to submit any PR, please follow PSR-1/PSR-12 coding conventions and unit test is a must. 176 | 177 | If this package is helpful, please give it a ⭐️⭐️⭐️. Thank you! 178 | 179 | ## License 180 | MIT License 181 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shipsaas/laravel-priority-queue", 3 | "type": "library", 4 | "version": "1.1.0", 5 | "description": "Priority Queue implementation for your Laravel Applications", 6 | "keywords": [ 7 | "laravel library", 8 | "laravel priority queue", 9 | "laravel queue", 10 | "laravel" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Phat Tran (Seth Phat)", 15 | "email": "me@sethphat.com", 16 | "homepage": "https://github.com/sethsandaru", 17 | "role": "Sr.SWE" 18 | } 19 | ], 20 | "license": "MIT", 21 | "require": { 22 | "php": "^8.2|^8.3", 23 | "laravel/framework": "^10|^11" 24 | }, 25 | "require-dev": { 26 | "fakerphp/faker": "^v1.20.0", 27 | "mockery/mockery": "^1.5.1", 28 | "phpunit/phpunit": "^10", 29 | "orchestra/testbench": "^8|^9", 30 | "phpunit/php-code-coverage": "^9|^10", 31 | "friendsofphp/php-cs-fixer": "^3.10" 32 | }, 33 | "extra": { 34 | "laravel": { 35 | "providers": [ 36 | "ShipSaasPriorityQueue\\PriorityQueueServiceProvider" 37 | ] 38 | } 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "ShipSaasPriorityQueue\\": "src/" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "ShipSaasPriorityQueue\\Tests\\": "tests/" 48 | } 49 | }, 50 | "scripts": { 51 | "test-coverage": [ 52 | "@php vendor/bin/phpunit --coverage-clover coverage.xml" 53 | ], 54 | "test": [ 55 | "@php vendor/bin/phpunit" 56 | ] 57 | }, 58 | "minimum-stability": "dev", 59 | "prefer-stable": true 60 | } 61 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./src 6 | 7 | 8 | ./src/Database 9 | ./src/PriorityQueueServiceProvider.php 10 | 11 | 12 | 13 | 14 | ./tests/ 15 | ./tests/TestCase.php 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Database/Migrations/2022_03_31_17_00_00_create_priority_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 12 | $table->string('queue')->index(); 13 | $table->longText('payload'); 14 | $table->integer('priority')->index(); 15 | $table->unsignedTinyInteger('attempts'); 16 | $table->unsignedInteger('reserved_at')->nullable(); 17 | $table->unsignedInteger('available_at')->index(); 18 | $table->unsignedInteger('created_at')->index(); 19 | 20 | $table->rawIndex( 21 | 'priority DESC, created_at ASC', 22 | 'idx_priority_sort' 23 | ); 24 | }); 25 | } 26 | 27 | public function down(): void 28 | { 29 | Schema::dropIfExists('priority_jobs'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/PriorityQueueServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 14 | $this->publishes([ 15 | __DIR__ . '/Database/Migrations/' => database_path('migrations'), 16 | ], 'priority-queue-migrations'); 17 | } 18 | } 19 | 20 | public function register(): void 21 | { 22 | $this->app->afterResolving('queue', function (QueueManager $manager): void { 23 | $manager->addConnector( 24 | 'database-priority', 25 | fn () => new DatabasePriorityConnector($this->app['db']) 26 | ); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Queue/DatabasePriorityConnector.php: -------------------------------------------------------------------------------- 1 | connections->connection($config['connection'] ?? null), 13 | $config['table'], 14 | $config['queue'], 15 | $config['retry_after'] ?? 60, 16 | $config['after_commit'] ?? null 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Queue/DatabasePriorityQueue.php: -------------------------------------------------------------------------------- 1 | database->table($this->table) 15 | ->lock($this->getLockForPopping()) 16 | ->where('queue', $this->getQueue($queue)) 17 | ->where(function ($query) { 18 | $this->isAvailable($query); 19 | $this->isReservedButExpired($query); 20 | }) 21 | ->orderBy('priority', 'desc') 22 | ->orderBy('created_at') 23 | ->first(); 24 | 25 | return $job 26 | ? new DatabaseJobRecord((object) $job) 27 | : null; 28 | } 29 | 30 | public function push($job, $data = '', $queue = null): mixed 31 | { 32 | return $this->enqueueUsing( 33 | $job, 34 | $this->createPayload($job, $this->getQueue($queue), $data), 35 | $queue, 36 | null, 37 | function ($payload, $queue) use ($job) { 38 | return $this->pushToDatabase( 39 | $queue, 40 | $payload, 41 | weight: $this->getJobWeight($job) 42 | ); 43 | } 44 | ); 45 | } 46 | 47 | public function later($delay, $job, $data = '', $queue = null): mixed 48 | { 49 | return $this->enqueueUsing( 50 | $job, 51 | $this->createPayload($job, $this->getQueue($queue), $data), 52 | $queue, 53 | $delay, 54 | function ($payload, $queue, $delay) use ($job) { 55 | return $this->pushToDatabase( 56 | $queue, 57 | $payload, 58 | $delay, 59 | weight: $this->getJobWeight($job) 60 | ); 61 | } 62 | ); 63 | } 64 | 65 | protected function pushToDatabase( 66 | $queue, 67 | $payload, 68 | $delay = 0, 69 | $attempts = 0, 70 | $weight = self::DEFAULT_WEIGHT 71 | ): int { 72 | return $this->database->table($this->table)->insertGetId($this->buildDatabaseRecord( 73 | $this->getQueue($queue), 74 | $payload, 75 | $this->availableAt($delay), 76 | $attempts, 77 | $weight 78 | )); 79 | } 80 | 81 | protected function buildDatabaseRecord( 82 | $queue, 83 | $payload, 84 | $availableAt, 85 | $attempts = 0, 86 | $weight = self::DEFAULT_WEIGHT 87 | ): array { 88 | return [ 89 | ...parent::buildDatabaseRecord($queue, $payload, $availableAt, $attempts), 90 | 'priority' => $weight, 91 | ]; 92 | } 93 | 94 | protected function getJobWeight($job): int 95 | { 96 | if (is_object($job) && property_exists($job, 'jobWeight')) { 97 | return (int) $job->jobWeight; 98 | } 99 | 100 | if (is_object($job) && method_exists($job, 'getJobWeight')) { 101 | return $job->getJobWeight(); 102 | } 103 | 104 | return self::DEFAULT_WEIGHT; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Traits/UseJobPrioritization.php: -------------------------------------------------------------------------------- 1 | uuid('id')->unique(); 22 | $table->string('text'); 23 | }); 24 | } 25 | 26 | public function testQueueWorkCommandWorksNormallyForDatabasePriorityQueueDriver() 27 | { 28 | TestJob::dispatch('Hello Seth Tran', $firstId = fake()->uuid()); 29 | TestJob::dispatch('Hello Seth Phat', $secondId = fake()->uuid()); 30 | 31 | $this->artisan('queue:work database-priority --max-jobs=1'); 32 | 33 | $this->assertDatabaseHas('logs', [ 34 | 'id' => $firstId, 35 | 'text' => 'Hello Seth Tran', 36 | ]); 37 | 38 | $this->artisan('queue:work database-priority --max-jobs=1'); 39 | 40 | $this->assertDatabaseHas('logs', [ 41 | 'id' => $secondId, 42 | 'text' => 'Hello Seth Phat', 43 | ]); 44 | } 45 | 46 | public function testQueueWorkCommandHandlesTheHighestWeightJobFirst() 47 | { 48 | TestJob::dispatch('Hello Seth Tran', $firstId = fake()->uuid()) 49 | ->setWeight(400); 50 | TestJob::dispatch('Hello Seth Phat', $secondId = fake()->uuid()); 51 | 52 | $this->artisan('queue:work database-priority --max-jobs=1'); 53 | 54 | // second job will be picked 55 | $this->assertDatabaseHas('logs', [ 56 | 'id' => $secondId, 57 | 'text' => 'Hello Seth Phat', 58 | ]); 59 | 60 | $this->artisan('queue:work database-priority --max-jobs=1'); 61 | 62 | $this->assertDatabaseHas('logs', [ 63 | 'id' => $firstId, 64 | 'text' => 'Hello Seth Tran', 65 | ]); 66 | } 67 | } 68 | 69 | class TestJob implements ShouldQueue 70 | { 71 | use Dispatchable; 72 | use Queueable; 73 | use UseJobPrioritization; 74 | 75 | public int $customWeight; 76 | 77 | public function __construct(public string $hello, public string $id) 78 | { 79 | $this->onConnection('database-priority'); 80 | } 81 | 82 | public function handle(): void 83 | { 84 | DB::table('logs')->insert([ 85 | 'id' => $this->id, 86 | 'text' => $this->hello, 87 | ]); 88 | } 89 | 90 | public function setWeight(int $weight): self 91 | { 92 | $this->customWeight = $weight; 93 | 94 | return $this; 95 | } 96 | 97 | public function getJobWeight(): int 98 | { 99 | return $this->customWeight ?? 1000; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | load(); 37 | 38 | // setup configs 39 | $app['config']->set('database.default', 'mysql'); 40 | $app['config']->set('database.connections.mysql', [ 41 | 'driver' => 'mysql', 42 | 'host' => env('DB_HOST'), 43 | 'port' => env('DB_PORT'), 44 | 'database' => env('DB_DATABASE'), 45 | 'username' => env('DB_USERNAME'), 46 | 'password' => env('DB_PASSWORD'), 47 | 'charset' => 'utf8mb4', 48 | 'collation' => 'utf8mb4_unicode_ci', 49 | 'prefix' => '', 50 | 'strict' => true, 51 | 'engine' => null, 52 | ]); 53 | $app['config']->set('queue.connections.database-priority', [ 54 | 'driver' => 'database-priority', 55 | 'connection' => 'mysql', 56 | 'table' => 'priority_jobs', 57 | 'queue' => 'default', 58 | 'retry_after' => 90, 59 | ]); 60 | 61 | $app['db']->connection('mysql') 62 | ->getSchemaBuilder() 63 | ->dropAllTables(); 64 | 65 | $migrationFiles = [ 66 | __DIR__ . '/../src/Database/Migrations/2022_03_31_17_00_00_create_priority_jobs_table.php', 67 | ]; 68 | 69 | foreach ($migrationFiles as $migrationFile) { 70 | $migrateInstance = include $migrationFile; 71 | $migrateInstance->up(); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/Unit/Queue/DatabasePriorityConnectorTest.php: -------------------------------------------------------------------------------- 1 | connect([ 14 | 'connection' => 'mysql', 15 | 'table' => 'priority_jobs', 16 | 'queue' => 'default', 17 | ]); 18 | 19 | $this->assertNotNull($queue); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Unit/Queue/DatabasePriorityQueueTest.php: -------------------------------------------------------------------------------- 1 | queue = app(QueueManager::class) 23 | ->connection('database-priority'); 24 | } 25 | 26 | public function testGetNextAvailableJobReturnsEmptyOnNoJob() 27 | { 28 | $job = $this->queue->pop(); 29 | 30 | $this->assertNull($job); 31 | } 32 | 33 | public function testGetNextAvailableJobReturnsTheHighestPriorityJob() 34 | { 35 | DB::table('priority_jobs')->insert([ 36 | [ 37 | 'queue' => 'default', 38 | 'priority' => 999, 39 | 'payload' => serialize('hehe'), 40 | 'attempts' => 0, 41 | 'available_at' => time(), 42 | 'created_at' => time(), 43 | ], 44 | [ 45 | 'queue' => 'default', 46 | 'priority' => 888, 47 | 'payload' => serialize('meme'), 48 | 'attempts' => 0, 49 | 'available_at' => time(), 50 | 'created_at' => time(), 51 | ], 52 | ]); 53 | 54 | $job = $this->queue->pop(); 55 | 56 | $this->assertNotNull($job); 57 | $this->assertStringContainsString('hehe', $job->getRawBody()); 58 | } 59 | 60 | public function testGetNextAvailableJobReturnsSamePriorityWouldPickTheOneCreatedFirst() 61 | { 62 | DB::table('priority_jobs')->insert([ 63 | [ 64 | 'queue' => 'default', 65 | 'priority' => 555, 66 | 'payload' => serialize('hehe'), 67 | 'attempts' => 0, 68 | 'available_at' => time(), 69 | 'created_at' => time(), 70 | ], 71 | [ 72 | 'queue' => 'default', 73 | 'priority' => 555, 74 | 'payload' => serialize('meme'), 75 | 'attempts' => 0, 76 | 'available_at' => time() - 100, 77 | 'created_at' => time() - 100, 78 | ], 79 | ]); 80 | 81 | $job = $this->queue->pop(); 82 | 83 | $this->assertNotNull($job); 84 | $this->assertStringContainsString('meme', $job->getRawBody()); 85 | } 86 | 87 | public function testPushJobWithWeightProperty() 88 | { 89 | TestJobWeightProperty::dispatch('hello world') 90 | ->onConnection('database-priority'); 91 | 92 | $this->assertDatabaseHas('priority_jobs', [ 93 | 'queue' => 'default', 94 | 'priority' => 100, 95 | ]); 96 | } 97 | 98 | public function testPushJobWithWeightMethod() 99 | { 100 | TestJobWeightMethod::dispatch('hello world x2') 101 | ->onConnection('database-priority'); 102 | 103 | $this->assertDatabaseHas('priority_jobs', [ 104 | 'queue' => 'default', 105 | 'priority' => 1000, 106 | ]); 107 | } 108 | 109 | public function testPushJobWithoutWeightUsesDefaultWeight() 110 | { 111 | TestJobWithoutWeight::dispatch('hello world x 3') 112 | ->onConnection('database-priority'); 113 | 114 | $this->assertDatabaseHas('priority_jobs', [ 115 | 'queue' => 'default', 116 | 'priority' => DatabasePriorityQueue::DEFAULT_WEIGHT, 117 | ]); 118 | } 119 | 120 | 121 | public function testPushJobLaterWorks() 122 | { 123 | $now = now()->addSeconds(100); 124 | 125 | TestJobWithoutWeight::dispatch('hello world x 3') 126 | ->delay($now) 127 | ->onConnection('database-priority'); 128 | 129 | $this->assertDatabaseHas('priority_jobs', [ 130 | 'queue' => 'default', 131 | 'available_at' => $now->timestamp, 132 | 'priority' => DatabasePriorityQueue::DEFAULT_WEIGHT, 133 | ]); 134 | } 135 | } 136 | 137 | class TestJobWeightProperty implements ShouldQueue 138 | { 139 | use Dispatchable; 140 | use Queueable; 141 | 142 | public int $jobWeight = 100; 143 | 144 | public function __construct(public string $hello) 145 | { 146 | } 147 | } 148 | 149 | 150 | 151 | class TestJobWeightMethod implements ShouldQueue 152 | { 153 | use Dispatchable; 154 | use Queueable; 155 | use UseJobPrioritization; 156 | 157 | public function __construct(public string $hello) 158 | { 159 | } 160 | 161 | public function getJobWeight(): int 162 | { 163 | return 1000; 164 | } 165 | } 166 | 167 | class TestJobWithoutWeight implements ShouldQueue 168 | { 169 | use Dispatchable; 170 | use Queueable; 171 | 172 | public function __construct(public string $hello) 173 | { 174 | } 175 | } 176 | --------------------------------------------------------------------------------