├── .gitattributes ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── linux.yml │ ├── perltidy.yml │ └── rebuild-website.yml ├── .gitignore ├── .mergify └── config.yml ├── .perltidyrc ├── Changes ├── LICENSE ├── MANIFEST.SKIP ├── Makefile.PL ├── README.md ├── examples ├── admin.png ├── linkcheck │ ├── lib │ │ ├── LinkCheck.pm │ │ └── LinkCheck │ │ │ ├── Controller │ │ │ └── Links.pm │ │ │ └── Task │ │ │ └── CheckLinks.pm │ ├── linkcheck.conf │ ├── script │ │ └── linkcheck │ ├── t │ │ └── linkcheck.t │ └── templates │ │ ├── layouts │ │ └── linkcheck.html.ep │ │ └── links │ │ ├── index.html.ep │ │ └── result.html.ep └── minion_bench.pl ├── lib ├── Minion.pm ├── Minion │ ├── Backend.pm │ ├── Backend │ │ ├── Pg.pm │ │ └── resources │ │ │ └── migrations │ │ │ └── pg.sql │ ├── Command │ │ ├── minion.pm │ │ └── minion │ │ │ ├── job.pm │ │ │ └── worker.pm │ ├── Guide.pod │ ├── Iterator.pm │ ├── Job.pm │ └── Worker.pm └── Mojolicious │ └── Plugin │ ├── Minion.pm │ └── Minion │ ├── Admin.pm │ └── resources │ ├── public │ └── minion │ │ ├── app.css │ │ ├── app.js │ │ ├── bootstrap │ │ ├── bootstrap.css │ │ └── bootstrap.js │ │ ├── d3 │ │ └── d3.js │ │ ├── epoch │ │ ├── epoch.css │ │ └── epoch.js │ │ ├── fontawesome │ │ └── fontawesome.css │ │ ├── jquery │ │ └── jquery.js │ │ ├── logo-black-2x.png │ │ ├── logo-black.png │ │ ├── moment │ │ └── moment.js │ │ ├── pinstripe-light.png │ │ └── webfonts │ │ ├── fa-brands-400.eot │ │ ├── fa-brands-400.svg │ │ ├── fa-brands-400.ttf │ │ ├── fa-brands-400.woff │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.eot │ │ ├── fa-regular-400.svg │ │ ├── fa-regular-400.ttf │ │ ├── fa-regular-400.woff │ │ ├── fa-regular-400.woff2 │ │ ├── fa-solid-900.eot │ │ ├── fa-solid-900.svg │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff │ │ └── fa-solid-900.woff2 │ └── templates │ ├── layouts │ └── minion.html.ep │ └── minion │ ├── _limit.html.ep │ ├── _notifications.html.ep │ ├── _pagination.html.ep │ ├── dashboard.html.ep │ ├── jobs.html.ep │ ├── locks.html.ep │ └── workers.html.ep └── t ├── backend.t ├── commands.t ├── lib └── MinionTest │ ├── AddTestTask.pm │ ├── BadTestTask.pm │ ├── EmptyTestTask.pm │ ├── FailTestTask.pm │ ├── NoResultTestTask.pm │ └── SyntaxErrorTestTask.pm ├── pg.t ├── pg_admin.t ├── pg_lite_app.t ├── pg_worker.t ├── pod.t └── pod_coverage.t /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pl linguist-language=Perl 2 | *.pm linguist-language=Perl 3 | *.t linguist-language=Perl 4 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please read the guide for [contributing to Mojolicious](http://mojolicious.org/perldoc/Mojolicious/Guides/Contributing), Minion is a spin-off project and follows the same rules. 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * Minion version: VERSION HERE 2 | * Perl version: VERSION HERE 3 | * Operating system: NAME AND VERSION HERE 4 | 5 | ### Steps to reproduce the behavior 6 | EXPLAIN WHAT HAPPENED HERE, PREFERABLY WITH CODE EXAMPLES 7 | 8 | ### Expected behavior 9 | EXPLAIN WHAT SHOULD HAPPEN HERE 10 | 11 | ### Actual behavior 12 | EXPLAIN WHAT HAPPENED INSTEAD HERE 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | DESCRIBE THE BIG PICTURE OF YOUR CHANGES HERE 3 | 4 | ### Motivation 5 | EXPLAIN WHY YOU BELIEVE THESE CHANGES ARE NECESSARY HERE 6 | 7 | ### References 8 | LIST RELEVANT ISSUES, PULL REQUESTS AND FORUM DISCUSSIONS HERE 9 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: linux 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | tags-ignore: 7 | - '*' 8 | pull_request: 9 | jobs: 10 | perl: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | codename: 15 | - buster 16 | perl-version: 17 | - '5.16' 18 | - '5.18' 19 | - '5.20' 20 | - '5.22' 21 | - '5.30' 22 | container: 23 | image: perl:${{ matrix.perl-version }}-${{ matrix.codename }} 24 | services: 25 | postgres: 26 | image: postgres 27 | env: 28 | POSTGRES_PASSWORD: postgres 29 | POSTGRES_INITDB_ARGS: --auth-host=md5 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: upgrade libpq 33 | run: | 34 | curl https://salsa.debian.org/postgresql/postgresql-common/raw/master/pgdg/apt.postgresql.org.sh | bash 35 | apt-get -y update && apt-get -y upgrade 36 | - name: perl -V 37 | run: perl -V 38 | - name: Fix ExtUtils::MakeMaker (for Perl 5.16 and 5.18) 39 | run: cpanm -n App::cpanminus ExtUtils::MakeMaker 40 | - name: Install dependencies 41 | run: | 42 | cpanm -n --installdeps . 43 | cpanm -n Mojo::Pg 44 | - name: Run tests 45 | env: 46 | TEST_ONLINE: postgresql://postgres:postgres@postgres:5432/postgres 47 | run: prove -l t 48 | -------------------------------------------------------------------------------- /.github/workflows/perltidy.yml: -------------------------------------------------------------------------------- 1 | name: perltidy 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | tags-ignore: 7 | - '*' 8 | pull_request: 9 | jobs: 10 | perltidy: 11 | runs-on: ubuntu-latest 12 | container: 13 | image: perl:5.32 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Fix git permissions 17 | # work around https://github.com/actions/checkout/issues/766 18 | run: git config --global --add safe.directory "$GITHUB_WORKSPACE" 19 | - name: perl -V 20 | run: perl -V 21 | - name: Install dependencies 22 | run: cpanm -n Perl::Tidy 23 | - name: perltidy --version 24 | run: perltidy --version 25 | - name: Run perltidy 26 | shell: bash 27 | run: | 28 | export GLOBIGNORE=t/lib/MinionTest/SyntaxErrorTestTask.pm 29 | shopt -s extglob globstar nullglob 30 | perltidy --pro=.../.perltidyrc -b -bext='/' **/*.p[lm] **/*.t && git diff --exit-code 31 | -------------------------------------------------------------------------------- /.github/workflows/rebuild-website.yml: -------------------------------------------------------------------------------- 1 | name: Rebuild Website 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | rebuild_website: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Trigger website workflow 11 | run: | 12 | curl \ 13 | -X POST \ 14 | -u "${{ secrets.WORKFLOW_DISPATCH_USERINFO }}" \ 15 | -H "Accept: application/vnd.github.everest-preview+json" \ 16 | -H "Content-Type: application/json" \ 17 | --data '{"ref": "main"}' \ 18 | https://api.github.com/repos/mojolicious/mojolicious.org/actions/workflows/publish-website.yml/dispatches 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | *~ 3 | !.gitignore 4 | !.perltidyrc 5 | !.travis.yml 6 | /blib 7 | /pm_to_blib 8 | /Makefile 9 | /Makefile.old 10 | /MANIFEST* 11 | !MANIFEST.SKIP 12 | /META.* 13 | /MYMETA.* 14 | node_modules 15 | package-lock.json 16 | -------------------------------------------------------------------------------- /.mergify/config.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: automatic merge 3 | conditions: 4 | - "#approved-reviews-by>=2" 5 | - "#changes-requested-reviews-by=0" 6 | - base=main 7 | actions: 8 | merge: 9 | method: merge 10 | - name: remove outdated reviews 11 | conditions: 12 | - base=main 13 | actions: 14 | dismiss_reviews: {} 15 | - name: ask to resolve conflict 16 | conditions: 17 | - conflict 18 | actions: 19 | comment: 20 | message: This pull request is now in conflicts. Could you fix it @{{author}}? 🙏 21 | -------------------------------------------------------------------------------- /.perltidyrc: -------------------------------------------------------------------------------- 1 | -pbp # Start with Perl Best Practices 2 | -w # Show all warnings 3 | -iob # Ignore old breakpoints 4 | -l=120 # 120 characters per line 5 | -mbl=2 # No more than 2 blank lines 6 | -i=2 # Indentation is 2 columns 7 | -ci=2 # Continuation indentation is 2 columns 8 | -vt=0 # Less vertical tightness 9 | -pt=2 # High parenthesis tightness 10 | -bt=2 # High brace tightness 11 | -sbt=2 # High square bracket tightness 12 | -wn # Weld nested containers 13 | -isbc # Don't indent comments without leading space 14 | -nst # Don't output to STDOUT 15 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | 2 | 11.00 2025-05-05 3 | - Minion::Backend::Pg now requires PostgreSQL 13. 4 | 5 | 10.31 2024-09-21 6 | - Restore old repair behavior for job dependencies without performance loss. (HEM42) 7 | 8 | 10.30 2024-06-05 9 | - Improved stats performance in Minion::Backend::Pg by an order of magnitude. (rshingleton) 10 | 11 | 10.29 2024-03-19 12 | - Added id field to list_locks method in Minion::Backend::Pg. (stuartskelton) 13 | - Improved admin UI with lock id information. (stuartskelton) 14 | - Fixed jQuery dependency. 15 | 16 | 10.28 2023-11-27 17 | - Improved repair and history performance in most cases. 18 | 19 | 10.27 2023-11-20 20 | - Improved repair performance in cases where there are a lot of finished jobs with dependencies. 21 | 22 | 10.26 2023-11-10 23 | - Added type information to worker status. 24 | - Improved workers by calling srand() after starting a new job process. 25 | 26 | 10.25 2022-06-24 27 | - Added workers field to stats methods in Minion and Minion::Backend::Pg. 28 | 29 | 10.24 2022-04-28 30 | - Improved Mojolicious::Plugin::Minion::Admin to log whenever a job gets removed. 31 | 32 | 10.23 2022-01-20 33 | - Improved repair performance in Minion::Backend::Pg. (andrii-suse, ilmari, kraih) 34 | 35 | 10.22 2021-06-10 36 | - Added perform_jobs_in_foreground method to Minion. 37 | 38 | 10.21 2021-03-20 39 | - Fixed YAML::XS dependency. 40 | 41 | 10.20 2021-03-13 42 | - Removed experimental status from expiring jobs feature. 43 | - Added Minion::Guide. 44 | - Improved admin ui and job command to use YAML::XS to make job information easier to read. 45 | 46 | 10.19 2021-03-10 47 | - Improved admin ui to include search feature on all pages. 48 | 49 | 10.18 2021-03-09 50 | - Improved admin ui with search feature. 51 | 52 | 10.17 2021-03-05 53 | - Added support for high priority fast lane. 54 | - Added each method to Minion::Iterator. 55 | - Added min_priority option to dequeue methods in Minion::Worker and Minion::Backend::Pg. 56 | - Added spare and spare_min_priority options to run method in Minion::Worker. 57 | - Added -s and -S options to worker command. 58 | - Added --retry-failed and --remove-failed options to job command. 59 | 60 | 10.16 2021-02-16 61 | - Fixed Mojolicious 9.0 compatibility. 62 | 63 | 10.15 2020-12-10 64 | - Updated examples to use signatures. 65 | 66 | 10.14 2020-10-24 67 | - Changed SQL style to use uppercase keywords. 68 | - Fixed a bug where Minion::Worker could inherit timers from Mojolicious applications. 69 | 70 | 10.13 2020-07-30 71 | - Added EXPERIMENTAL support for lax dependencies. 72 | - Added EXPERIMENTAL lax option to enqueue method in Minion, Minion::Backend and Minion::Backend::Pg. 73 | - Removed experimental status from notes option in Minion::Backend and Minion::Backend::Pg. 74 | - Added lax field to list_jobs method in Minion::Backend::Pg. 75 | - Added is_locked method to Minion. 76 | - Added auto_retry_job method to Minion::Backend. 77 | - Added parents method to Minion::Job. 78 | - Added -x option to job command. 79 | - Fixed a bug in Minion::Backend::Pg where manual retries would count towards the attempts limit for automatic 80 | retries. 81 | 82 | 10.12 2020-07-25 83 | - Added EXPERIMENTAL support for expiring jobs. 84 | - Removed experimental support for job sequences again since use cases turned out to be weaker than expected. 85 | - Added EXPERIMENTAL expire option to enqueue method in Minion, Minion::Backend and Minion::Backend::Pg. 86 | - Added expires field to list_jobs method in Minion::Backend::Pg. 87 | - Added -E option to job command. 88 | 89 | 10.11 2020-07-21 90 | - Improved adming ui with more icons. 91 | 92 | 10.10 2020-07-11 93 | - Added stuck_after attribute to Minion. 94 | - Improved repair method in Minion::Backend::Pg to detect stuck jobs and transtion them to the failed state after 2 95 | days of inactivity. 96 | 97 | 10.09 2020-07-09 98 | - Added EXPERIMENTAL support for job sequences. 99 | - Added EXPERIMENTAL sequence option to enqueue method in Minion, Minion::Backend and Minion::Backend::Pg. 100 | - Added EXPERIMENTAL sequences option to list_jobs method in Minion::Backend and Minion::Backend::Pg. 101 | - Added next and previous fields to list_jobs method in Minion::Backend::Pg. 102 | - Added -Q option to job command. 103 | - Improved admin ui to allow navigating job sequences. 104 | 105 | 10.08 2020-06-17 106 | - Improved Minion::Job to record exit code and the signal a job died from, if it was terminated unexpectedly. 107 | 108 | 10.07 2020-06-16 109 | - Removed experimental status from Minion::Iterator module. 110 | - Removed experimental status from jobs and workers methods in Minion. 111 | - Removed experimental status from before options of list_jobs and list_workers methods in Minion::Backend and 112 | Minion::Backend::Pg. 113 | - Updated project metadata. 114 | - Improved admin ui to allow sending TERM signals. 115 | - Fixed a bug in Minion::Job where CHLD, INT, TERM and QUIT signals would be ignored by job processes. 116 | 117 | 10.06 2020-06-01 118 | - Added EXPERIMENTAL support for custom task classes. 119 | - Added EXPERIMENTAL class_for_task method to Minion. 120 | - Added EXPERIMENTAL run method to Minion::Job. 121 | - Added -T option to job command. 122 | 123 | 10.05 2020-05-30 124 | - Improved .perltidyrc with more modern settings. 125 | - Fixed result_p promise leak in Minion. 126 | 127 | 10.04 2020-01-30 128 | - Added EXPERIMENTAL total method to Minion::Iterator. 129 | 130 | 10.03 2020-01-29 131 | - Added EXPERIMENTAL Minion::Iterator module. 132 | - Added EXPERIMENTAL jobs and workers methods to Minion. 133 | - Added EXPERIMENTAL before options to list_jobs and list_workers methods in Minion::Backend and Minion::Backend::Pg. 134 | 135 | 10.02 2019-12-20 136 | - Fixed QUIT signal in Minion::Worker. 137 | - Fixed stop remote control command. 138 | - Fixed a problem where Minion::Worker was unable to deal with immediately restarted jobs. 139 | 140 | 10.01 2019-12-16 141 | - Fixed an unlock concurrency issue in Minion::Backend::Pg. (andrii-suse) 142 | 143 | 10.0 2019-11-15 144 | - Removed PostgreSQL migrations older than 2 years, that means you need to have Minion version 7.01 or newer installed 145 | before you can upgrade to version 10.0. 146 | - Removed experimental status from result_p method in Minion. 147 | - Removed experimental status from history method in Minion and Minion::Backend::Pg. 148 | - Removed experimental status from delayed_jobs and enqueued_jobs fields from stats methods in Minion and 149 | Minion::Backend::Pg. 150 | - Removed experimental status from cleanup and finish events in Minion::Job. 151 | - Changed reset method in Minion and Minion::Backend::Pg to require options for what to reset. 152 | - Improved reset method in Minion and Minion::Backend::Pg to allow for locks to be reset without resetting the whole 153 | queue. 154 | - Improved performance by making the minion_workers table unlogged. 155 | 156 | 9.13 2019-08-29 157 | - Added EXPERIMENTAL cleanup event to Minion::Job. 158 | 159 | 9.12 2019-08-04 160 | - Added EXPERIMENTAL notes option to list_jobs method in Minion::Backend and Minion::Backend::Pg. 161 | - Added -n option to job command. 162 | - Improved note methods in Minion::Job, Minion::Backend and Minion::Backend::Pg to allow for fields to be removed. 163 | 164 | 9.11 2019-07-08 165 | - Added time field to list_jobs method in Minion::Backend::Pg. 166 | - Added time field to info method in Minion::Job. 167 | - Improved admin ui to show the current runtime for every job, as well as the current delay for delayed jobs. 168 | - Improved Minion::Backend::Pg to ignore missing workers for jobs in the minion_foreground named queue and make 169 | debugging very slow jobs easier. 170 | 171 | 9.10 2019-05-09 172 | - Fixed a few query parameter bugs in the admin ui. (fskale, sri) 173 | 174 | 9.09 2019-02-03 175 | - Changed result_p method in Minion to pass along the whole job info hash instead of just the job result. 176 | 177 | 9.08 2019-02-01 178 | - Added EXPERIMENTAL result_p method to Minion. 179 | 180 | 9.07 2018-10-18 181 | - Improved various attributes to use new Mojolicious 8.03 features to avoid memory leaks. 182 | 183 | 9.06 2018-09-22 184 | - Added support for controlling job processes by installing signal handlers for INT, USR1 and USR2 in tasks. 185 | - Added kill method to Minion::Job. 186 | - Improved worker command with support for kill remote control command. 187 | - Improved admin ui with a new menu to send signals to job processes. 188 | 189 | 9.05 2018-09-18 190 | - Fixed screenshot in documentation. 191 | 192 | 9.04 2018-09-15 193 | - Updated project metadata. 194 | 195 | 9.03 2018-04-19 196 | - Removed hour value from daily history data. 197 | - Improved 24 hour history graph on Mojolicious::Plugin::Minion::Admin dashboard to use the local time, consistent 198 | with the real-time graph. 199 | 200 | 9.02 2018-04-18 201 | - Changed 24 hour history graph on Mojolicious::Plugin::Minion::Admin dashboard to refresh in 10 minute intervals. 202 | - Changed daily history data to include an epoch value instead of a day. 203 | - Fixed a few ordering bugs in history methods. 204 | 205 | 9.01 2018-04-17 206 | - Improved 24 hour history graph to refresh automatically every 5 minutes in Mojolicious::Plugin::Minion::Admin. 207 | (s1037989) 208 | 209 | 9.0 2018-04-15 210 | - Replaced queue, state and task options of list_jobs method in Minion::Backend::Pg with queues, states and tasks 211 | options. 212 | - Replaced name option of list_locks method in Minion::Backend::Pg with names option. 213 | - Replaced key/value argument of note method in Minion::Backend::Pg with a hash reference. 214 | - Added EXPERIMENTAL support for displaying a 24 hour history graph on the Mojolicious::Plugin::Minion::Admin 215 | dashboard. 216 | - Added EXPERIMENTAL finish event to Minion::Job. 217 | - Added EXPERIMENTAL history methods to Minion and Minion::Backend::Pg. 218 | - Added execute method to Minion::Job. 219 | - Added -H option to job command. 220 | - Improved note method in Minion::Job to allow for multiple metadata fields to be changed at once. 221 | - Fixed a bug where the job command could remove all parents from retried jobs. 222 | - Fixed filtering of jobs by queue and state in Mojolicious::Plugin::Minion::Admin. 223 | 224 | 8.12 2018-03-07 225 | - Added parents option to retry and retry_job methods in Minion::Job and Minion::Backend::Pg. (CandyAngel) 226 | 227 | 8.11 2018-02-28 228 | - Fixed worker page links in Mojolicious::Plugin::Minion::Admin. 229 | 230 | 8.10 2018-02-18 231 | - Improved Mojolicious::Plugin::Minion::Admin to use less distracting colors. 232 | - Fixed a bug in Mojolicious::Plugin::Minion::Admin where job results containing very long strings could not be 233 | displayed properly. 234 | 235 | 8.09 2018-01-25 236 | - Converted Mojolicious::Plugin::Minion::Admin to Bootstrap 4. 237 | 238 | 8.08 2017-12-15 239 | - Added busy and wait events to Minion::Worker. 240 | - Added dequeue_timeout option to run method in Minion::Worker. 241 | - Added -D option to worker command. 242 | 243 | 8.07 2017-12-11 244 | - Fixed guard method in Minion not to release already expired locks. 245 | 246 | 8.06 2017-12-11 247 | - Added active_locks field to stats methods in Minion, Minion::Backend and Minion::Backend::Pg again. 248 | - Improved Mojolicious::Plugin::Minion::Admin with support for locks. 249 | 250 | 8.05 2017-12-10 251 | - Removed active_locks field from stats methods again since it did not work correctly and there is no efficient way to 252 | fix it. 253 | - Fixed list_locks method in Minion::Backend::Pg to exclude already expired locks. 254 | 255 | 8.04 2017-12-08 256 | - Added list_locks method to Minion::Backend and Minion::Backend::Pg. 257 | - Added -L and -U options to job command. 258 | - Added active_locks field to stats methods in Minion, Minion::Backend and Minion::Backend::Pg. 259 | - Updated Font Awesome to version 5. 260 | - Improved Minion::Job to clear signal handlers in job processes. 261 | - Fixed a bug in Mojolicious::Plugin::Minion::Admin where filtering jobs by task or queue would not reset the offset. 262 | 263 | 8.03 2017-11-21 264 | - Removed -f option from worker command. 265 | - Added broadcast method to Minion. 266 | - Added run method to Minion::Worker. 267 | 268 | 8.02 2017-11-19 269 | - Improved admin ui with alerts for job management. 270 | 271 | 8.01 2017-11-18 272 | - Fixed installation problems with some versions of Perl on Windows. 273 | 274 | 8.0 2017-11-16 275 | - Removed job_info and worker_info methods from Minion::Backend and Minion::Backend::Pg. 276 | - Changed return value of list_jobs and list_workers methods in Minion::Backend and Minion::Backend::Pg. 277 | - Added new module Mojolicious::Plugin::Minion::Admin. 278 | - Added ids option to list_jobs and list_workers methods in Minion::Backend and Minion::Backend::Pg. 279 | - Added uptime field to stats methods in Minion, Minion::Backend and Minion::Backend::Pg. 280 | 281 | 7.09 2017-10-20 282 | - Fixed a deadlock problem in Minion::Backend::Pg where jobs could fail if two processes tried to acquire the same 283 | lock at the same time. 284 | 285 | 7.08 2017-10-18 286 | - Fixed a bug in the worker command where processing jobs could be delayed significantly after a system restart. 287 | (rgci) 288 | 289 | 7.07 2017-10-11 290 | - Added reap event to Minion::Job. 291 | 292 | 7.06 2017-09-21 293 | - Added guard method to Minion. 294 | 295 | 7.05 2017-08-07 296 | - Improved foreground method in Minion to rethrow job exceptions. 297 | 298 | 7.04 2017-08-01 299 | - Added foreground method to Minion. 300 | - Added id option to dequeue methods in Minion::Worker and Minion::Backend::Pg. 301 | - Added attempts option to retry and retry_job methods in Minion::Job and Minion::Backend::Pg. 302 | - Added -f option to job command. 303 | 304 | 7.03 2017-07-06 305 | - Updated Mojo::Pg requirement to 4.0. 306 | - Improved Minion::Backend::Pg to support sharing the database connection cache with existing Mojo::Pg objects. 307 | 308 | 7.02 2017-07-05 309 | - Improved performance of worker command when processing jobs that are finished very quickly. 310 | 311 | 7.01 2017-06-25 312 | - Added note methods to Minion::Job and Minion::Backend::Pg. 313 | - Added notes option to enqueue methods in Minion and Minion::Backend::Pg. 314 | - Added notes field to info method in Minion::Job and job_info method in Minion::Backend::Pg. 315 | - Improved performance of stats and lock methods in Minion::Backend::Pg with a new index and other optimizations. 316 | (depesz) 317 | - Improved benchmark script to be more consistent. (depesz) 318 | 319 | 7.0 2017-06-18 320 | - Added support for rate limiting and unique jobs. 321 | - Added lock and unlock methods to Minion and Minion::Backend::Pg. 322 | - Improved performance of Minion::Backend::Pg significantly with a new index and other optimizations. 323 | 324 | 6.06 2017-06-03 325 | - Added an example application to demonstrate how to integrate background jobs into well-structured Mojolicious 326 | applications. 327 | 328 | 6.05 2017-04-03 329 | - Added support for sharing worker status information. 330 | - Improved retry methods to allow for active jobs to be retried as well. 331 | - Improved job command to show timestamps in RFC 3339 (1994-11-06T08:49:37Z) format. 332 | 333 | 6.04 2017-03-18 334 | - Added -f option to worker command. 335 | - Removed -r option from job command, so you have to write --remove from now on, which should prevent accidental 336 | mixups with the -R option in the future. 337 | 338 | 6.03 2017-03-14 339 | - Fixed serious performance problems in Minion::Backend::Pg. 340 | 341 | 6.02 2017-01-02 342 | - Updated Mojo::Pg requirement to 2.18. 343 | 344 | 6.01 2017-01-01 345 | - Updated Mojo::Pg requirement to 2.33. 346 | - Improved performance of Minion::Backend::Pg with a new index. 347 | 348 | 6.0 2016-09-17 349 | - Removed TTIN, TTOU and USR1 signals from worker command. 350 | - Changed return value of start method in Minion::Job. 351 | - Added support for worker remote control commands. 352 | - Added commands attribute to Minion::Worker. 353 | - Added add_command and process_commands methods to Minion::Worker. 354 | - Added pid and stop methods to Minion::Job. 355 | - Added broadcast and receive methods to Minion::Backend::Pg. 356 | - Added -b option to job command. 357 | - Improved worker command with support for jobs and stop remote control commands. 358 | 359 | 5.09 2016-08-31 360 | - Added EXPERIMENTAL enqueued_jobs field to stats methods in Minion and Minion::Backend::Pg. 361 | - Improved Minion::Backend::Pg performance slightly with a new index. 362 | 363 | 5.08 2016-05-20 364 | - Improved repair methods not to remove finished jobs with unresolved dependencies. 365 | 366 | 5.07 2016-05-17 367 | - Added support for job dependencies. (jberger, sri) 368 | - Added parents option to enqueue methods in Minion and Minion::Backend::Pg. (jberger, sri) 369 | - Added children and parents fields to info method in Minion::Job and job_info method in Minion::Backend::Pg. 370 | - Added -P option to job command. 371 | - Improved stats methods to include jobs with unresolved dependencies in delayed_jobs count. 372 | 373 | 5.06 2016-05-05 374 | - Improved worker command to support the TTIN, TTOU and USR1 signals. 375 | - Improved Minion::Backend::Pg to handle delayed and retried jobs more efficiently. 376 | 377 | 5.05 2016-04-20 378 | - Added queue option to list_jobs method in Minion::Backend::Pg. 379 | - Improved performance of stats method in Minion::Backend::Pg slightly. 380 | 381 | 5.04 2016-04-19 382 | - Added EXPERIMENTAL delayed_jobs field to stats methods in Minion and Minion::Backend::Pg. 383 | - Updated Mojo::Pg requirement to 2.18. 384 | - Improved job command to show more detailed information for jobs and workers. 385 | 386 | 5.03 2016-04-10 387 | - Added enqueue event to Minion. (jberger) 388 | 389 | 5.02 2016-03-23 390 | - Fixed copyright notice. 391 | 392 | 5.01 2016-02-24 393 | - Fixed worker command to repair in regular intervals. 394 | 395 | 5.0 2016-02-17 396 | - Minion::Backend::Pg now requires PostgreSQL 9.5. 397 | - Added start event to Minion::Job. 398 | - Added -R option to worker command. 399 | - Reduced default missing_after value to 30 minutes. 400 | - Reduced default remove_after value to 2 days. 401 | - Improved Minion::Backend::Pg performance significantly with a new index and PostgreSQL 9.5 features. 402 | - Improved Minion::Job to capture more exceptions. 403 | - Improved worker command to support the QUIT signal. 404 | - Improved worker command to repair in less regular intervals. 405 | 406 | 4.06 2016-02-06 407 | - Improved performance of Minion::Backend::Pg slightly. 408 | 409 | 4.05 2016-02-05 410 | - Improved Minion::Backend::Pg to check the PostgreSQL version. 411 | 412 | 4.04 2016-01-23 413 | - Updated Minion::Backend::Pg to use new Mojo::Pg features. 414 | 415 | 4.03 2016-01-17 416 | - Removed an unused index from Minion::Backend::Pg. 417 | - Fixed a bug where the worker command would always watch the default queue. (avkhozov) 418 | 419 | 4.02 2016-01-03 420 | - Updated links to Mojolicious website. 421 | 422 | 4.01 2015-11-12 423 | - Improved retry methods to allow options to be changed for already inactive jobs. 424 | 425 | 4.0 2015-11-09 426 | - Removed attempts attribute from Minion::Job. 427 | - Improved Minion::Backend::Pg to preserve more information across retries for debugging. 428 | - Fixed bug where jobs could not be retried automatically if a worker went away. 429 | 430 | 3.03 2015-11-08 431 | - Added queues option to perform_jobs method in Minion. 432 | 433 | 3.02 2015-10-31 434 | - Fixed portability issues in tests. 435 | 436 | 3.01 2015-10-30 437 | - Added support for retrying failed jobs automatically. 438 | - Added backoff attribute to Minion. 439 | - Added attempts attribute to Minion::Job. 440 | - Added attempts option to enqueue methods in Minion and Minion::Backend::Pg. 441 | - Added -A option to job command. 442 | 443 | 3.0 2015-10-30 444 | - Removed Minion::Backend::File, because DBM::Deep quickly becomes unusably slow, you can use the CPAN module 445 | Minion::Backend::SQLite instead. 446 | 447 | 2.05 2015-10-15 448 | - Fixed bug where jobs could sometimes not be finished correctly by the worker command. 449 | 450 | 2.04 2015-10-14 451 | - Fixed portability issue in worker command. 452 | 453 | 2.03 2015-10-09 454 | - Improved commands to show all options that can affect their behavior. 455 | 456 | 2.02 2015-10-08 457 | - Improved job command to show the queue in job lists. 458 | 459 | 2.01 2015-10-02 460 | - Fixed Windows portability issues in tests. 461 | 462 | 2.0 2015-10-01 463 | - Removed -t option from worker command. 464 | - Added support for multiple named queues. 465 | - Added retries attribute to Minion::Job. 466 | - Added retries argument to fail_job, finish_job and retry_job methods in Minion::Backend::File and 467 | Minion::Backend::Pg. 468 | - Added queue option to enqueue method in Minion. 469 | - Added queue option to enqueue and retry_job methods in Minion::Backend::File and Minion::Backend::Pg. 470 | - Added queues option to dequeue methods in Minion::Worker, Minion::Backend::File and Minion::Backend::Pg. 471 | - Added -q option to job and worker commands. 472 | - Improved worker command to be more resilient to time jumps. 473 | - Fixed a race condition in Minion::Backend::File and Minion::Backend::Pg where a retried job did not have to be 474 | dequeued again before it could be finished. 475 | 476 | 1.19 2015-09-28 477 | - Added support for retrying jobs with a new priority. 478 | - Added priority option to retry method in Minion::Job. 479 | - Added priority option to retry_job methods in Minion::Backend::File and Minion::Backend::Pg. 480 | 481 | 1.18 2015-08-30 482 | - Fixed Makefile.PL to be compliant with version 2 of the CPAN distribution metadata specification. 483 | 484 | 1.17 2015-08-29 485 | - Fixed bug in worker command where new jobs would still be dequeued after receiving an INT/TERM signal. 486 | 487 | 1.16 2015-08-28 488 | - Improved worker command to detect workers without heartbeat a little faster. 489 | 490 | 1.15 2015-05-15 491 | - Added support for retrying jobs with a delay. (kwa) 492 | - Added delay option to retry method in Minion::Job. (kwa) 493 | - Added delay option to retry_job methods in Minion::Backend::File and Minion::Backend::Pg. (kwa) 494 | 495 | 1.14 2015-04-21 496 | - Improved performance of Minion::Backend::Pg with a new index. (avkhozov) 497 | 498 | 1.13 2015-03-25 499 | - Improved Minion::Backend::Pg to reset the job queue a little faster. 500 | 501 | 1.12 2015-03-17 502 | - Improved portability of some tests. 503 | 504 | 1.11 2015-03-10 505 | - Fixed tests to work without Mojo::Pg. 506 | 507 | 1.10 2015-03-09 508 | - Added support for performing multiple jobs concurrently with a single worker. (bpmedley, sri) 509 | - Added is_finished and start methods to Minion::Job. (bpmedley, sri) 510 | - Added -j option to worker command. (bpmedley, sri) 511 | - Fixed concurrency bugs in Minion::Backend::File. 512 | - Fixed bug in job command where timing information was not displayed correctly. 513 | 514 | 1.09 2015-03-02 515 | - Added support for monitoring workers with heartbeats instead of signals. 516 | - Added missing_after attribute to Minion. 517 | - Added -I option to worker command. 518 | - Fixed bug where workers were considered active even if they had no active jobs assigned. 519 | 520 | 1.08 2015-02-20 521 | - Updated for Mojolicious 5.81. 522 | 523 | 1.07 2015-02-12 524 | - Updated Minion::Backend::Pg for Mojo::Pg 1.08. 525 | 526 | 1.06 2015-01-26 527 | - Improved commands to use less punctuation. 528 | 529 | 1.05 2015-01-05 530 | - Improved repair methods in Minion::Backend::File and Minion::Backend::Pg to mention the current process in results 531 | of failed jobs. 532 | 533 | 1.04 2015-01-03 534 | - Improved Minion::Backend::Pg to use new JSON features of Mojo::Pg. 535 | 536 | 1.03 2014-12-19 537 | - Added -t option to worker command. 538 | 539 | 1.02 2014-11-22 540 | - Renamed -L and -T options to -l and -S. 541 | - Improved job command table formatting. 542 | 543 | 1.01 2014-11-20 544 | - Improved documentation. 545 | 546 | 1.0 2014-11-19 547 | - Removed experimental status from distribution. 548 | 549 | 0.45 2014-11-18 550 | - Improved dequeue performance in Minion::Backend::Pg significantly. (bpmedley) 551 | 552 | 0.44 2014-11-17 553 | - Fixed bug where jobs could not be dequeued inside a running event loop. 554 | 555 | 0.43 2014-11-17 556 | - Fixed bug where advisory locks would run out of shared memory. 557 | 558 | 0.42 2014-11-16 559 | - Improved Minion::Backend::Pg performance with advisory locks and notifications. (bpmedley, sri) 560 | 561 | 0.41 2014-11-15 562 | - Improved Minion::Backend::Pg performance. 563 | 564 | 0.40 2014-11-11 565 | - Added PostgreSQL support with Mojo::Pg. (bpmedley, sri) 566 | - Added support for job results. 567 | - Added Minion::Backend::Pg. (bpmedley, sri) 568 | 569 | 0.39 2014-10-05 570 | - Added DBM::Deep support to Minion::Backend::File. 571 | - Renamed -S option to -o. 572 | 573 | 0.38 2014-10-04 574 | - Removed support for non-blocking enqueue. 575 | - Removed Minion::Backend::Mango. 576 | 577 | 0.37 2014-09-22 578 | - Fixed packaging bug. 579 | 580 | 0.36 2014-09-21 581 | - Updated Makefile.PL for version 2 of the CPAN distribution metadata specification. 582 | 583 | 0.35 2014-09-06 584 | - Improved Minion::Backend::File to write less often. 585 | 586 | 0.34 2014-08-26 587 | - Improved job command to show higher precision times. 588 | 589 | 0.33 2014-07-10 590 | - Replaced state argument of list_jobs methods in Minion::Backend::File and Minion::Backend::Mango with more versatile 591 | options argument. 592 | - Added -t option to job command. 593 | 594 | 0.32 2014-07-10 595 | - Added state argument to list_jobs methods in Minion::Backend::File and Minion::Backend::Mango. 596 | - Added -T option to job command. 597 | 598 | 0.31 2014-07-09 599 | - Reduced CPU usage of Minion::Backend::Mango when waiting for new jobs. 600 | 601 | 0.30 2014-07-08 602 | - Added timeout argument to dequeue methods in Minion::Backend::File, Minion::Backend::Mango and Minion::Worker. 603 | 604 | 0.29 2014-07-07 605 | - Renamed restart_job method to retry_job in Minion::Backend, Minion::Backend::File and Minion::Backend::Mango. 606 | - Renamed restart method to retry in Minion::Job. 607 | - Improved worker command to repair in regular intervals. 608 | 609 | 0.28 2014-06-28 610 | - Added spawn event to Minion::Job. 611 | 612 | 0.27 2014-06-21 613 | - Replaced delayed option with delay. 614 | 615 | 0.26 2014-06-18 616 | - Renamed clean_up_after attribute in Minion to remove_after. 617 | 618 | 0.25 2014-06-17 619 | - Removed auto_perform attribute from Minion. 620 | - Added perform_jobs method to Minion. 621 | - Fixed multiple Windows bugs. 622 | 623 | 0.24 2014-06-16 624 | - Improved Minion::Job to reset Mojo::IOLoop. 625 | - Fixed Windows bugs in tests. 626 | 627 | 0.23 2014-06-15 628 | - Fixed Minion::Backend::File Windows support. 629 | 630 | 0.22 2014-06-15 631 | - Reduced default clean_up_after value to 10 days. 632 | 633 | 0.21 2014-06-14 634 | - Added clean_up_after attribute to Minion. 635 | - Improved performance of repair methods. 636 | 637 | 0.20 2014-06-13 638 | - Added module Minion::Backend::File. 639 | - Improved Minion::Backend to provide a generic repair method. 640 | 641 | 0.15 2014-06-04 642 | - Fixed a few small bugs in Minion::Backend::Mango. 643 | 644 | 0.14 2014-06-04 645 | - Fixed Minion::Backend::Mango to work with strings in addition to object ids. 646 | 647 | 0.13 2014-06-03 648 | - Added list_workers methods to Minion::Backend and Minion::Backend::Mango. 649 | 650 | 0.12 2014-06-03 651 | - Fixed enqueue to use the correct time format. 652 | 653 | 0.11 2014-06-03 654 | - Changed a few return values in Minion::Backend::Mango. 655 | 656 | 0.10 2014-06-02 657 | - Removed created, delayed, error, finished, priority, restarted, restarts, started and state methods from 658 | Minion::Job. 659 | - Removed started method from Minion::Worker. 660 | - Added support for pluggable backends. 661 | - Added modules Minion::Backend and Minion::Backend::Mango. 662 | - Added backend attribute to Minion. 663 | - Added reset method to Minion. 664 | - Added info methods to Minion::Job and Minion::Worker. 665 | - Added -L and -S options to job command. 666 | 667 | 0.09 2014-04-05 668 | - Added worker event to Minion. 669 | - Added dequeue event to Minion::Worker. 670 | - Added failed and finished events to Minion::Job. 671 | - Added restarted method to Minion::Job. 672 | - Changed return values of fail, finish and restart methods in Minion::Job. 673 | 674 | 0.08 2014-04-04 675 | - Added support for non-blocking enqueue. 676 | 677 | 0.07 2014-04-03 678 | - Improved performance by reusing Mango connections. 679 | 680 | 0.06 2014-04-03 681 | - Added delayed and priority methods to Minion::Job. 682 | - Renamed after option to delayed. 683 | - Improved job command to use a human readable time format and allow new jobs to be enqueued. 684 | 685 | 0.05 2014-04-03 686 | - Improved job command to stream job lists and show more information. 687 | 688 | 0.04 2014-04-02 689 | - Removed all_jobs and one_job methods from Minion::Job. 690 | - Removed repair method from Minion::Worker; 691 | - Added module Minion::Command::minion::job. 692 | - Added auto_perform attribute to Minion. 693 | - Added repair method to Minion. 694 | - Added created, error, finished, remove, restart, restarts and started methods to Minion::Job. 695 | - Added started method to Minion::Worker. 696 | 697 | 0.03 2014-03-30 698 | - Removed doc and worker attributes from Minion::Job. 699 | - Added args, id and minion attributes to Minion::Job. 700 | - Added id attribute to Minion::Worker. 701 | - Added job method to Minion. 702 | - Added state method to Minion::Job. 703 | 704 | 0.02 2014-03-28 705 | - Added support for delayed jobs. 706 | - Added stats method to Minion. 707 | - Added app method to Minion::Job. 708 | - Reduced Perl requirement to 5.10.1. 709 | 710 | 0.01 2014-03-27 711 | - First release. 712 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The Artistic License 2.0 2 | 3 | Copyright (c) 2000-2006, The Perl Foundation. 4 | 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | This license establishes the terms under which a given free software 11 | Package may be copied, modified, distributed, and/or redistributed. 12 | The intent is that the Copyright Holder maintains some artistic 13 | control over the development of that Package while still keeping the 14 | Package available as open source and free software. 15 | 16 | You are always permitted to make arrangements wholly outside of this 17 | license directly with the Copyright Holder of a given Package. If the 18 | terms of this license do not permit the full use that you propose to 19 | make of the Package, you should contact the Copyright Holder and seek 20 | a different licensing arrangement. 21 | 22 | Definitions 23 | 24 | "Copyright Holder" means the individual(s) or organization(s) 25 | named in the copyright notice for the entire Package. 26 | 27 | "Contributor" means any party that has contributed code or other 28 | material to the Package, in accordance with the Copyright Holder's 29 | procedures. 30 | 31 | "You" and "your" means any person who would like to copy, 32 | distribute, or modify the Package. 33 | 34 | "Package" means the collection of files distributed by the 35 | Copyright Holder, and derivatives of that collection and/or of 36 | those files. A given Package may consist of either the Standard 37 | Version, or a Modified Version. 38 | 39 | "Distribute" means providing a copy of the Package or making it 40 | accessible to anyone else, or in the case of a company or 41 | organization, to others outside of your company or organization. 42 | 43 | "Distributor Fee" means any fee that you charge for Distributing 44 | this Package or providing support for this Package to another 45 | party. It does not mean licensing fees. 46 | 47 | "Standard Version" refers to the Package if it has not been 48 | modified, or has been modified only in ways explicitly requested 49 | by the Copyright Holder. 50 | 51 | "Modified Version" means the Package, if it has been changed, and 52 | such changes were not explicitly requested by the Copyright 53 | Holder. 54 | 55 | "Original License" means this Artistic License as Distributed with 56 | the Standard Version of the Package, in its current version or as 57 | it may be modified by The Perl Foundation in the future. 58 | 59 | "Source" form means the source code, documentation source, and 60 | configuration files for the Package. 61 | 62 | "Compiled" form means the compiled bytecode, object code, binary, 63 | or any other form resulting from mechanical transformation or 64 | translation of the Source form. 65 | 66 | 67 | Permission for Use and Modification Without Distribution 68 | 69 | (1) You are permitted to use the Standard Version and create and use 70 | Modified Versions for any purpose without restriction, provided that 71 | you do not Distribute the Modified Version. 72 | 73 | 74 | Permissions for Redistribution of the Standard Version 75 | 76 | (2) You may Distribute verbatim copies of the Source form of the 77 | Standard Version of this Package in any medium without restriction, 78 | either gratis or for a Distributor Fee, provided that you duplicate 79 | all of the original copyright notices and associated disclaimers. At 80 | your discretion, such verbatim copies may or may not include a 81 | Compiled form of the Package. 82 | 83 | (3) You may apply any bug fixes, portability changes, and other 84 | modifications made available from the Copyright Holder. The resulting 85 | Package will still be considered the Standard Version, and as such 86 | will be subject to the Original License. 87 | 88 | 89 | Distribution of Modified Versions of the Package as Source 90 | 91 | (4) You may Distribute your Modified Version as Source (either gratis 92 | or for a Distributor Fee, and with or without a Compiled form of the 93 | Modified Version) provided that you clearly document how it differs 94 | from the Standard Version, including, but not limited to, documenting 95 | any non-standard features, executables, or modules, and provided that 96 | you do at least ONE of the following: 97 | 98 | (a) make the Modified Version available to the Copyright Holder 99 | of the Standard Version, under the Original License, so that the 100 | Copyright Holder may include your modifications in the Standard 101 | Version. 102 | 103 | (b) ensure that installation of your Modified Version does not 104 | prevent the user installing or running the Standard Version. In 105 | addition, the Modified Version must bear a name that is different 106 | from the name of the Standard Version. 107 | 108 | (c) allow anyone who receives a copy of the Modified Version to 109 | make the Source form of the Modified Version available to others 110 | under 111 | 112 | (i) the Original License or 113 | 114 | (ii) a license that permits the licensee to freely copy, 115 | modify and redistribute the Modified Version using the same 116 | licensing terms that apply to the copy that the licensee 117 | received, and requires that the Source form of the Modified 118 | Version, and of any works derived from it, be made freely 119 | available in that license fees are prohibited but Distributor 120 | Fees are allowed. 121 | 122 | 123 | Distribution of Compiled Forms of the Standard Version 124 | or Modified Versions without the Source 125 | 126 | (5) You may Distribute Compiled forms of the Standard Version without 127 | the Source, provided that you include complete instructions on how to 128 | get the Source of the Standard Version. Such instructions must be 129 | valid at the time of your distribution. If these instructions, at any 130 | time while you are carrying out such distribution, become invalid, you 131 | must provide new instructions on demand or cease further distribution. 132 | If you provide valid instructions or cease distribution within thirty 133 | days after you become aware that the instructions are invalid, then 134 | you do not forfeit any of your rights under this license. 135 | 136 | (6) You may Distribute a Modified Version in Compiled form without 137 | the Source, provided that you comply with Section 4 with respect to 138 | the Source of the Modified Version. 139 | 140 | 141 | Aggregating or Linking the Package 142 | 143 | (7) You may aggregate the Package (either the Standard Version or 144 | Modified Version) with other packages and Distribute the resulting 145 | aggregation provided that you do not charge a licensing fee for the 146 | Package. Distributor Fees are permitted, and licensing fees for other 147 | components in the aggregation are permitted. The terms of this license 148 | apply to the use and Distribution of the Standard or Modified Versions 149 | as included in the aggregation. 150 | 151 | (8) You are permitted to link Modified and Standard Versions with 152 | other works, to embed the Package in a larger work of your own, or to 153 | build stand-alone binary or bytecode versions of applications that 154 | include the Package, and Distribute the result without restriction, 155 | provided the result does not expose a direct interface to the Package. 156 | 157 | 158 | Items That are Not Considered Part of a Modified Version 159 | 160 | (9) Works (including, but not limited to, modules and scripts) that 161 | merely extend or make use of the Package, do not, by themselves, cause 162 | the Package to be a Modified Version. In addition, such works are not 163 | considered parts of the Package itself, and are not subject to the 164 | terms of this license. 165 | 166 | 167 | General Provisions 168 | 169 | (10) Any use, modification, and distribution of the Standard or 170 | Modified Versions is governed by this Artistic License. By using, 171 | modifying or distributing the Package, you accept this license. Do not 172 | use, modify, or distribute the Package, if you do not accept this 173 | license. 174 | 175 | (11) If your Modified Version has been derived from a Modified 176 | Version made by someone other than you, you are nevertheless required 177 | to ensure that your Modified Version complies with the requirements of 178 | this license. 179 | 180 | (12) This license does not grant you the right to use any trademark, 181 | service mark, tradename, or logo of the Copyright Holder. 182 | 183 | (13) This license includes the non-exclusive, worldwide, 184 | free-of-charge patent license to make, have made, use, offer to sell, 185 | sell, import and otherwise transfer the Package with respect to any 186 | patent claims licensable by the Copyright Holder that are necessarily 187 | infringed by the Package. If you institute patent litigation 188 | (including a cross-claim or counterclaim) against any party alleging 189 | that the Package constitutes direct or contributory patent 190 | infringement, then this Artistic License to you shall terminate on the 191 | date that such litigation is filed. 192 | 193 | (14) Disclaimer of Warranty: 194 | THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS 195 | IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED 196 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR 197 | NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL 198 | LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL 199 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 200 | DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF 201 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 202 | -------------------------------------------------------------------------------- /MANIFEST.SKIP: -------------------------------------------------------------------------------- 1 | ^\.(?!perltidyrc) 2 | .*\.old$ 3 | \.tar\.gz$ 4 | ^Makefile$ 5 | ^MYMETA\. 6 | ^blib 7 | ^pm_to_blib 8 | \B\.DS_Store 9 | ^node_modules 10 | ^package-lock\. 11 | -------------------------------------------------------------------------------- /Makefile.PL: -------------------------------------------------------------------------------- 1 | use 5.016; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use ExtUtils::MakeMaker; 7 | 8 | WriteMakefile( 9 | NAME => 'Minion', 10 | VERSION_FROM => 'lib/Minion.pm', 11 | ABSTRACT => 'Job queue', 12 | AUTHOR => 'Sebastian Riedel ', 13 | LICENSE => 'artistic_2', 14 | META_MERGE => { 15 | dynamic_config => 0, 16 | 'meta-spec' => {version => 2}, 17 | no_index => {directory => ['t']}, 18 | prereqs => {runtime => {requires => {perl => '5.016'}}}, 19 | resources => { 20 | bugtracker => {web => 'https://github.com/mojolicious/minion/issues'}, 21 | homepage => 'https://mojolicious.org', 22 | license => ['http://www.opensource.org/licenses/artistic-license-2.0'], 23 | repository => { 24 | type => 'git', 25 | url => 'https://github.com/mojolicious/minion.git', 26 | web => 'https://github.com/mojolicious/minion', 27 | }, 28 | x_IRC => {url => 'irc://irc.libera.chat/#mojo', web => 'https://web.libera.chat/#mojo'} 29 | }, 30 | }, 31 | PREREQ_PM => {Mojolicious => '9.0', 'YAML::XS' => '0.67'}, 32 | test => {TESTS => 't/*.t t/*/*.t'} 33 | ); 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![Screenshot](https://raw.github.com/mojolicious/minion/main/examples/admin.png?raw=true) 3 | 4 | [![](https://github.com/mojolicious/minion/workflows/linux/badge.svg)](https://github.com/mojolicious/minion/actions) 5 | 6 | A high performance job queue for the Perl programming language. Also available for [Node.js](https://github.com/mojolicious/minion.js). 7 | 8 | Minion comes with support for multiple named queues, priorities, high priority fast lane, delayed jobs, job 9 | dependencies, job progress, job results, retries with backoff, rate limiting, unique jobs, expiring jobs, statistics, 10 | distributed workers, parallel processing, autoscaling, remote control, [Mojolicious](https://mojolicious.org) admin ui, 11 | resource leak protection and multiple backends (such as [PostgreSQL](https://www.postgresql.org)). 12 | 13 | Job queues allow you to process time and/or computationally intensive tasks in background processes, outside of the 14 | request/response lifecycle of web applications. Among those tasks you'll commonly find image resizing, spam filtering, 15 | HTTP downloads, building tarballs, warming caches and basically everything else you can imagine that's not super fast. 16 | 17 | ```perl 18 | use Mojolicious::Lite -signatures; 19 | 20 | plugin Minion => {Pg => 'postgresql://postgres@/test'}; 21 | 22 | # Slow task 23 | app->minion->add_task(slow_log => sub ($job, $msg) { 24 | sleep 5; 25 | $job->app->log->debug(qq{Received message "$msg"}); 26 | }); 27 | 28 | # Perform job in a background worker process 29 | get '/log' => sub ($c) { 30 | $c->minion->enqueue(slow_log => [$c->param('msg') // 'no message']); 31 | $c->render(text => 'Your message will be logged soon.'); 32 | }; 33 | 34 | app->start; 35 | ``` 36 | 37 | Just start one or more background worker processes in addition to your web server. 38 | 39 | $ ./myapp.pl minion worker 40 | 41 | ## Installation 42 | 43 | All you need is a one-liner, it takes less than a minute. 44 | 45 | $ curl -L https://cpanmin.us | perl - -M https://cpan.metacpan.org -n Minion 46 | 47 | We recommend the use of a [Perlbrew](http://perlbrew.pl) environment. 48 | 49 | ## Want to know more? 50 | 51 | Take a look at our excellent [documentation](https://mojolicious.org/perldoc/Minion/Guide)! 52 | -------------------------------------------------------------------------------- /examples/admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojolicious/minion/b82fd176d7809106a71e72f8e82b23863e048a04/examples/admin.png -------------------------------------------------------------------------------- /examples/linkcheck/lib/LinkCheck.pm: -------------------------------------------------------------------------------- 1 | package LinkCheck; 2 | use Mojo::Base 'Mojolicious', -signatures; 3 | 4 | sub startup ($self) { 5 | 6 | # Configuration 7 | my $config = $self->plugin(Config => {file => 'linkcheck.conf'}); 8 | $self->secrets($config->{secrets}); 9 | 10 | # Job queue (requires a background worker process) 11 | # 12 | # $ script/linkcheck minion worker 13 | # 14 | $self->plugin(Minion => {Pg => $config->{pg}}); 15 | $self->plugin('Minion::Admin'); 16 | $self->plugin('LinkCheck::Task::CheckLinks'); 17 | 18 | # Controller 19 | my $r = $self->routes; 20 | $r->get('/' => sub { shift->redirect_to('index') }); 21 | $r->get('/links')->to('links#index')->name('index'); 22 | $r->post('/links')->to('links#check')->name('check'); 23 | $r->get('/links/:id')->to('links#result')->name('result'); 24 | } 25 | 26 | 1; 27 | -------------------------------------------------------------------------------- /examples/linkcheck/lib/LinkCheck/Controller/Links.pm: -------------------------------------------------------------------------------- 1 | package LinkCheck::Controller::Links; 2 | use Mojo::Base 'Mojolicious::Controller', -signatures; 3 | 4 | sub check ($self) { 5 | my $v = $self->validation; 6 | $v->required('url'); 7 | return $self->render(action => 'index') if $v->has_error; 8 | 9 | my $id = $self->minion->enqueue(check_links => [$v->param('url')]); 10 | $self->redirect_to('result', id => $id); 11 | } 12 | 13 | sub index { } 14 | 15 | sub result ($self) { 16 | return $self->reply->not_found unless my $job = $self->minion->job($self->param('id')); 17 | $self->render(result => $job->info->{result}); 18 | } 19 | 20 | 1; 21 | -------------------------------------------------------------------------------- /examples/linkcheck/lib/LinkCheck/Task/CheckLinks.pm: -------------------------------------------------------------------------------- 1 | package LinkCheck::Task::CheckLinks; 2 | use Mojo::Base 'Mojolicious::Plugin', -signatures; 3 | 4 | use Mojo::URL; 5 | 6 | sub register ($self, $app, $config) { 7 | $app->minion->add_task(check_links => \&_check_links); 8 | } 9 | 10 | sub _check_links ($job, $url) { 11 | my @results; 12 | my $ua = $job->app->ua; 13 | my $res = $ua->get($url)->result; 14 | push @results, [$url, $res->code]; 15 | 16 | for my $link ($res->dom->find('a[href]')->map(attr => 'href')->each) { 17 | my $abs = Mojo::URL->new($link)->to_abs(Mojo::URL->new($url)); 18 | $res = $ua->head($abs)->result; 19 | push @results, [$link, $res->code]; 20 | } 21 | 22 | $job->finish(\@results); 23 | } 24 | 25 | 1; 26 | -------------------------------------------------------------------------------- /examples/linkcheck/linkcheck.conf: -------------------------------------------------------------------------------- 1 | { 2 | pg => 'postgresql://postgres@127.0.0.1:5432/postgres', 3 | secrets => ['s3cret'] 4 | } 5 | -------------------------------------------------------------------------------- /examples/linkcheck/script/linkcheck: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use FindBin; 7 | BEGIN { unshift @INC, "$FindBin::Bin/../lib" } 8 | 9 | # Start command line interface for application 10 | require Mojolicious::Commands; 11 | Mojolicious::Commands->start_app('LinkCheck'); 12 | -------------------------------------------------------------------------------- /examples/linkcheck/t/linkcheck.t: -------------------------------------------------------------------------------- 1 | use Mojo::Base -strict; 2 | 3 | use Test::More; 4 | 5 | # This test requires a PostgreSQL connection string for an existing database 6 | # 7 | # TEST_ONLINE=postgres://tester:testing@/test prove -l t/*.t 8 | # 9 | plan skip_all => 'set TEST_ONLINE to enable this test' unless $ENV{TEST_ONLINE}; 10 | 11 | use Mojo::Pg; 12 | use Mojo::URL; 13 | use Test::Mojo; 14 | 15 | # Isolate tests 16 | my $url = Mojo::URL->new($ENV{TEST_ONLINE})->query([search_path => 'linkcheck_test']); 17 | my $pg = Mojo::Pg->new($url); 18 | $pg->db->query('DROP SCHEMA IF EXISTS linkcheck_test CASCADE'); 19 | $pg->db->query('CREATE SCHEMA linkcheck_test'); 20 | 21 | # Override configuration for testing 22 | my $t = Test::Mojo->new(LinkCheck => {pg => $url, secrets => ['test_s3cret']}); 23 | $t->ua->max_redirects(10); 24 | 25 | # Enqueue a background job 26 | $t->get_ok('/')->status_is(200)->text_is('title' => 'Check links')->element_exists('form input[type=url]'); 27 | $t->post_ok('/links' => form => {url => 'https://mojolicious.org'}) 28 | ->status_is(200) 29 | ->text_is('title' => 'Result') 30 | ->text_is('p' => 'Waiting for result...') 31 | ->element_exists_not('table'); 32 | 33 | # Perform the background job 34 | $t->get_ok('/links/1') 35 | ->status_is(200) 36 | ->text_is('title' => 'Result') 37 | ->text_is('p' => 'Waiting for result...') 38 | ->element_exists_not('table'); 39 | $t->app->minion->perform_jobs; 40 | $t->get_ok('/links/1')->status_is(200)->text_is('title' => 'Result')->element_exists_not('p')->element_exists('table'); 41 | 42 | # Clean up once we are done 43 | $pg->db->query('DROP SCHEMA linkcheck_test CASCADE'); 44 | 45 | done_testing(); 46 | -------------------------------------------------------------------------------- /examples/linkcheck/templates/layouts/linkcheck.html.ep: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= title %> 5 | 10 | %= content_for 'header' 11 | 12 | 13 |

<%= link_to 'Link Check' => 'index' %>

14 | %= content 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/linkcheck/templates/links/index.html.ep: -------------------------------------------------------------------------------- 1 | % layout 'linkcheck', title => 'Check links'; 2 | %= form_for 'check' => begin 3 | %= url_field url => 'https://mojolicious.org/perldoc' 4 | %= submit_button 'Check' 5 | % end 6 | -------------------------------------------------------------------------------- /examples/linkcheck/templates/links/result.html.ep: -------------------------------------------------------------------------------- 1 | % layout 'linkcheck', title => 'Result'; 2 | % if (ref $result eq 'ARRAY') { 3 | 4 | 5 | 6 | 7 | 8 | % for my $status (@$result) { 9 | 10 | 11 | 12 | 13 | % } 14 |
URLStatus
<%= $status->[0] %><%= $status->[1] %>
15 | % } 16 | % elsif (!$result) { 17 | % content_for header => begin 18 | 19 | % end 20 |

Waiting for result...

21 | % } 22 | % else { 23 | %= $result 24 | % } 25 | -------------------------------------------------------------------------------- /examples/minion_bench.pl: -------------------------------------------------------------------------------- 1 | use Mojo::Base -strict; 2 | 3 | use Minion; 4 | use Time::HiRes 'time'; 5 | 6 | my $ENQUEUE = 20000; 7 | my $BATCHES = 20; 8 | my $DEQUEUE = 50; 9 | my $PARENTS = 50; 10 | my $CHILDREN = 500; 11 | my $REPETITIONS = 2; 12 | my $WORKERS = 4; 13 | my $INFO = 100; 14 | my $STATS = 100; 15 | my $REPAIR = 10; 16 | my $LOCK = 1000; 17 | my $UNLOCK = 1000; 18 | 19 | # A benchmark script for comparing backends and evaluating the performance impact of proposed changes 20 | my $minion = Minion->new(Pg => 'postgresql://postgres@127.0.0.1:5432/postgres'); 21 | $minion->add_task(foo => sub { }); 22 | $minion->add_task(bar => sub { }); 23 | $minion->reset({all => 1}); 24 | 25 | # Enqueue 26 | say "Clean start with $ENQUEUE jobs"; 27 | my @parents = map { $minion->enqueue('foo') } 1 .. $PARENTS; 28 | my $worker = $minion->worker->register; 29 | $worker->dequeue(0.5, {id => $_})->finish for @parents; 30 | $worker->unregister; 31 | my $before = time; 32 | my @jobs = map { $minion->enqueue($_ % 2 ? 'foo' : 'bar' => [] => {parents => \@parents}) } 1 .. $ENQUEUE; 33 | my $elapsed = time - $before; 34 | my $avg = sprintf '%.3f', $ENQUEUE / $elapsed; 35 | say "Enqueued $ENQUEUE jobs in $elapsed seconds ($avg/s)"; 36 | $minion->enqueue('foo' => [] => {parents => \@jobs}) for 1 .. $CHILDREN; 37 | $minion->backend->pg->db->query('ANALYZE minion_jobs'); 38 | 39 | my $FINISH = $DEQUEUE * $BATCHES; 40 | 41 | # Dequeue 42 | sub dequeue { 43 | my @pids; 44 | for (1 .. $WORKERS) { 45 | die "Couldn't fork: $!" unless defined(my $pid = fork); 46 | unless ($pid) { 47 | my $worker = $minion->repair->worker->register; 48 | my $before = time; 49 | for (1 .. $BATCHES) { 50 | my @jobs; 51 | while (@jobs < $DEQUEUE) { 52 | next unless my $job = $worker->dequeue(0.5); 53 | push @jobs, $job; 54 | } 55 | $_->finish({minion_bench => $_->id}) for @jobs; 56 | } 57 | my $elapsed = time - $before; 58 | my $avg = sprintf '%.3f', $FINISH / $elapsed; 59 | say "$$ finished $FINISH jobs in $elapsed seconds ($avg/s)"; 60 | $worker->unregister; 61 | exit; 62 | } 63 | push @pids, $pid; 64 | } 65 | 66 | say "$$ has started $WORKERS workers"; 67 | my $before = time; 68 | waitpid $_, 0 for @pids; 69 | my $elapsed = time - $before; 70 | my $avg = sprintf '%.3f', ($FINISH * $WORKERS) / $elapsed; 71 | say "$WORKERS workers finished $FINISH jobs each in $elapsed seconds ($avg/s)"; 72 | } 73 | dequeue() for 1 .. $REPETITIONS; 74 | 75 | # Job info 76 | say "Requesting job info $INFO times"; 77 | $before = time; 78 | my $backend = $minion->backend; 79 | $backend->list_jobs(0, 1, {ids => [$_]}) for 1 .. $INFO; 80 | $elapsed = time - $before; 81 | $avg = sprintf '%.3f', $INFO / $elapsed; 82 | say "Received job info $INFO times in $elapsed seconds ($avg/s)"; 83 | 84 | # Stats 85 | say "Requesting stats $STATS times"; 86 | $before = time; 87 | $minion->stats for 1 .. $STATS; 88 | $elapsed = time - $before; 89 | $avg = sprintf '%.3f', $STATS / $elapsed; 90 | say "Received stats $STATS times in $elapsed seconds ($avg/s)"; 91 | 92 | # Repair 93 | say "Repairing $REPAIR times"; 94 | $before = time; 95 | $minion->repair for 1 .. $REPAIR; 96 | $elapsed = time - $before; 97 | $avg = sprintf '%.3f', $REPAIR / $elapsed; 98 | say "Repaired $REPAIR times in $elapsed seconds ($avg/s)"; 99 | 100 | # Lock 101 | say "Acquiring locks $LOCK times"; 102 | $before = time; 103 | $minion->lock($_ % 2 ? 'foo' : 'bar', 3600, {limit => int($LOCK / 2)}) for 1 .. $LOCK; 104 | $elapsed = time - $before; 105 | $avg = sprintf '%.3f', $LOCK / $elapsed; 106 | say "Acquired locks $LOCK times in $elapsed seconds ($avg/s)"; 107 | 108 | # Unlock 109 | say "Releasing locks $UNLOCK times"; 110 | $before = time; 111 | $minion->unlock($_ % 2 ? 'foo' : 'bar') for 1 .. $UNLOCK; 112 | $elapsed = time - $before; 113 | $avg = sprintf '%.3f', $UNLOCK / $elapsed; 114 | say "Releasing locks $UNLOCK times in $elapsed seconds ($avg/s)"; 115 | -------------------------------------------------------------------------------- /lib/Minion/Backend.pm: -------------------------------------------------------------------------------- 1 | package Minion::Backend; 2 | use Mojo::Base -base; 3 | 4 | use Carp qw(croak); 5 | 6 | has minion => undef, weak => 1; 7 | 8 | sub auto_retry_job { 9 | my ($self, $id, $retries, $attempts) = @_; 10 | return 1 if $attempts <= 1; 11 | my $delay = $self->minion->backoff->($retries); 12 | return $self->retry_job($id, $retries, {attempts => $attempts > 1 ? $attempts - 1 : 1, delay => $delay}); 13 | } 14 | 15 | sub broadcast { croak 'Method "broadcast" not implemented by subclass' } 16 | sub dequeue { croak 'Method "dequeue" not implemented by subclass' } 17 | sub enqueue { croak 'Method "enqueue" not implemented by subclass' } 18 | sub fail_job { croak 'Method "fail_job" not implemented by subclass' } 19 | sub finish_job { croak 'Method "finish_job" not implemented by subclass' } 20 | sub history { croak 'Method "history" not implemented by subclass' } 21 | sub list_jobs { croak 'Method "list_jobs" not implemented by subclass' } 22 | sub list_locks { croak 'Method "list_locks" not implemented by subclass' } 23 | sub list_workers { croak 'Method "list_workers" not implemented by subclass' } 24 | sub lock { croak 'Method "lock" not implemented by subclass' } 25 | sub note { croak 'Method "note" not implemented by subclass' } 26 | sub receive { croak 'Method "receive" not implemented by subclass' } 27 | sub register_worker { croak 'Method "register_worker" not implemented by subclass' } 28 | sub remove_job { croak 'Method "remove_job" not implemented by subclass' } 29 | sub repair { croak 'Method "repair" not implemented by subclass' } 30 | sub reset { croak 'Method "reset" not implemented by subclass' } 31 | sub retry_job { croak 'Method "retry_job" not implemented by subclass' } 32 | sub stats { croak 'Method "stats" not implemented by subclass' } 33 | sub unlock { croak 'Method "unlock" not implemented by subclass' } 34 | sub unregister_worker { croak 'Method "unregister_worker" not implemented by subclass' } 35 | 36 | 1; 37 | 38 | =encoding utf8 39 | 40 | =head1 NAME 41 | 42 | Minion::Backend - Backend base class 43 | 44 | =head1 SYNOPSIS 45 | 46 | package Minion::Backend::MyBackend; 47 | use Mojo::Base 'Minion::Backend'; 48 | 49 | sub broadcast {...} 50 | sub dequeue {...} 51 | sub enqueue {...} 52 | sub fail_job {...} 53 | sub finish_job {...} 54 | sub history {...} 55 | sub list_jobs {...} 56 | sub list_locks {...} 57 | sub list_workers {...} 58 | sub lock {...} 59 | sub note {...} 60 | sub receive {...} 61 | sub register_worker {...} 62 | sub remove_job {...} 63 | sub repair {...} 64 | sub reset {...} 65 | sub retry_job {...} 66 | sub stats {...} 67 | sub unlock {...} 68 | sub unregister_worker {...} 69 | 70 | =head1 DESCRIPTION 71 | 72 | L is an abstract base class for L backends, like L. 73 | 74 | =head1 ATTRIBUTES 75 | 76 | L implements the following attributes. 77 | 78 | =head2 minion 79 | 80 | my $minion = $backend->minion; 81 | $backend = $backend->minion(Minion->new); 82 | 83 | L object this backend belongs to. Note that this attribute is weakened. 84 | 85 | =head1 METHODS 86 | 87 | L inherits all methods from L and implements the following new ones. 88 | 89 | =head2 auto_retry_job 90 | 91 | my $bool = $backend->auto_retry_job($job_id, $retries, $attempts); 92 | 93 | Automatically L job with L if there are attempts left, used to implement backends like 94 | L. 95 | 96 | =head2 broadcast 97 | 98 | my $bool = $backend->broadcast('some_command'); 99 | my $bool = $backend->broadcast('some_command', [@args]); 100 | my $bool = $backend->broadcast('some_command', [@args], [$id1, $id2, $id3]); 101 | 102 | Broadcast remote control command to one or more workers. Meant to be overloaded in a subclass. 103 | 104 | =head2 dequeue 105 | 106 | my $job_info = $backend->dequeue($worker_id, 0.5); 107 | my $job_info = $backend->dequeue($worker_id, 0.5, {queues => ['important']}); 108 | 109 | Wait a given amount of time in seconds for a job, dequeue it and transition from C to C state, or 110 | return C if queues were empty. Meant to be overloaded in a subclass. 111 | 112 | These options are currently available: 113 | 114 | =over 2 115 | 116 | =item id 117 | 118 | id => '10023' 119 | 120 | Dequeue a specific job. 121 | 122 | =item min_priority 123 | 124 | min_priority => 3 125 | 126 | Do not dequeue jobs with a lower priority. 127 | 128 | =item queues 129 | 130 | queues => ['important'] 131 | 132 | One or more queues to dequeue jobs from, defaults to C. 133 | 134 | =back 135 | 136 | These fields are currently available: 137 | 138 | =over 2 139 | 140 | =item args 141 | 142 | args => ['foo', 'bar'] 143 | 144 | Job arguments. 145 | 146 | =item id 147 | 148 | id => '10023' 149 | 150 | Job ID. 151 | 152 | =item retries 153 | 154 | retries => 3 155 | 156 | Number of times job has been retried. 157 | 158 | =item task 159 | 160 | task => 'foo' 161 | 162 | Task name. 163 | 164 | =back 165 | 166 | =head2 enqueue 167 | 168 | my $job_id = $backend->enqueue('foo'); 169 | my $job_id = $backend->enqueue(foo => [@args]); 170 | my $job_id = $backend->enqueue(foo => [@args] => {priority => 1}); 171 | 172 | Enqueue a new job with C state. Meant to be overloaded in a subclass. 173 | 174 | These options are currently available: 175 | 176 | =over 2 177 | 178 | =item attempts 179 | 180 | attempts => 25 181 | 182 | Number of times performing this job will be attempted, with a delay based on L after the first 183 | attempt, defaults to C<1>. 184 | 185 | =item delay 186 | 187 | delay => 10 188 | 189 | Delay job for this many seconds (from now), defaults to C<0>. 190 | 191 | =item expire 192 | 193 | expire => 300 194 | 195 | Job is valid for this many seconds (from now) before it expires. 196 | 197 | =item lax 198 | 199 | lax => 1 200 | 201 | Existing jobs this job depends on may also have transitioned to the C state to allow for it to be processed, 202 | defaults to C. Note that this option is B and might change without warning! 203 | 204 | =item notes 205 | 206 | notes => {foo => 'bar', baz => [1, 2, 3]} 207 | 208 | Hash reference with arbitrary metadata for this job. 209 | 210 | =item parents 211 | 212 | parents => [$id1, $id2, $id3] 213 | 214 | One or more existing jobs this job depends on, and that need to have transitioned to the state C before it 215 | can be processed. 216 | 217 | =item priority 218 | 219 | priority => 5 220 | 221 | Job priority, defaults to C<0>. Jobs with a higher priority get performed first. Priorities can be positive or negative, 222 | but should be in the range between C<100> and C<-100>. 223 | 224 | =item queue 225 | 226 | queue => 'important' 227 | 228 | Queue to put job in, defaults to C. 229 | 230 | =back 231 | 232 | =head2 fail_job 233 | 234 | my $bool = $backend->fail_job($job_id, $retries); 235 | my $bool = $backend->fail_job($job_id, $retries, 'Something went wrong!'); 236 | my $bool = $backend->fail_job( 237 | $job_id, $retries, {whatever => 'Something went wrong!'}); 238 | 239 | Transition from C to C state with or without a result, and if there are attempts remaining, transition 240 | back to C with a delay based on L. Meant to be overloaded in a subclass. 241 | 242 | =head2 finish_job 243 | 244 | my $bool = $backend->finish_job($job_id, $retries); 245 | my $bool = $backend->finish_job($job_id, $retries, 'All went well!'); 246 | my $bool = $backend->finish_job( 247 | $job_id, $retries, {whatever => 'All went well!'}); 248 | 249 | Transition from C to C state with or without a result. Meant to be overloaded in a subclass. 250 | 251 | =head2 history 252 | 253 | my $history = $backend->history; 254 | 255 | Get history information for job queue. Meant to be overloaded in a subclass. 256 | 257 | These fields are currently available: 258 | 259 | =over 2 260 | 261 | =item daily 262 | 263 | daily => [{epoch => 12345, finished_jobs => 95, failed_jobs => 2}, ...] 264 | 265 | Hourly counts for processed jobs from the past day. 266 | 267 | =back 268 | 269 | =head2 list_jobs 270 | 271 | my $results = $backend->list_jobs($offset, $limit); 272 | my $results = $backend->list_jobs($offset, $limit, {states => ['inactive']}); 273 | 274 | Returns the information about jobs in batches. Meant to be overloaded in a subclass. 275 | 276 | # Get the total number of results (without limit) 277 | my $num = $backend->list_jobs(0, 100, {queues => ['important']})->{total}; 278 | 279 | # Check job state 280 | my $results = $backend->list_jobs(0, 1, {ids => [$job_id]}); 281 | my $state = $results->{jobs}[0]{state}; 282 | 283 | # Get job result 284 | my $results = $backend->list_jobs(0, 1, {ids => [$job_id]}); 285 | my $result = $results->{jobs}[0]{result}; 286 | 287 | These options are currently available: 288 | 289 | =over 2 290 | 291 | =item before 292 | 293 | before => 23 294 | 295 | List only jobs before this id. 296 | 297 | =item ids 298 | 299 | ids => ['23', '24'] 300 | 301 | List only jobs with these ids. 302 | 303 | =item notes 304 | 305 | notes => ['foo', 'bar'] 306 | 307 | List only jobs with one of these notes. 308 | 309 | =item queues 310 | 311 | queues => ['important', 'unimportant'] 312 | 313 | List only jobs in these queues. 314 | 315 | =item states 316 | 317 | states => ['inactive', 'active'] 318 | 319 | List only jobs in these states. 320 | 321 | =item tasks 322 | 323 | tasks => ['foo', 'bar'] 324 | 325 | List only jobs for these tasks. 326 | 327 | =back 328 | 329 | These fields are currently available: 330 | 331 | =over 2 332 | 333 | =item args 334 | 335 | args => ['foo', 'bar'] 336 | 337 | Job arguments. 338 | 339 | =item attempts 340 | 341 | attempts => 25 342 | 343 | Number of times performing this job will be attempted. 344 | 345 | =item children 346 | 347 | children => ['10026', '10027', '10028'] 348 | 349 | Jobs depending on this job. 350 | 351 | =item created 352 | 353 | created => 784111777 354 | 355 | Epoch time job was created. 356 | 357 | =item delayed 358 | 359 | delayed => 784111777 360 | 361 | Epoch time job was delayed to. 362 | 363 | =item expires 364 | 365 | expires => 784111777 366 | 367 | Epoch time job is valid until before it expires. 368 | 369 | =item finished 370 | 371 | finished => 784111777 372 | 373 | Epoch time job was finished. 374 | 375 | =item id 376 | 377 | id => 10025 378 | 379 | Job id. 380 | 381 | =item lax 382 | 383 | lax => 0 384 | 385 | Existing jobs this job depends on may also have failed to allow for it to be processed. 386 | 387 | =item notes 388 | 389 | notes => {foo => 'bar', baz => [1, 2, 3]} 390 | 391 | Hash reference with arbitrary metadata for this job. 392 | 393 | =item parents 394 | 395 | parents => ['10023', '10024', '10025'] 396 | 397 | Jobs this job depends on. 398 | 399 | =item priority 400 | 401 | priority => 3 402 | 403 | Job priority. 404 | 405 | =item queue 406 | 407 | queue => 'important' 408 | 409 | Queue name. 410 | 411 | =item result 412 | 413 | result => 'All went well!' 414 | 415 | Job result. 416 | 417 | =item retried 418 | 419 | retried => 784111777 420 | 421 | Epoch time job has been retried. 422 | 423 | =item retries 424 | 425 | retries => 3 426 | 427 | Number of times job has been retried. 428 | 429 | =item started 430 | 431 | started => 784111777 432 | 433 | Epoch time job was started. 434 | 435 | =item state 436 | 437 | state => 'inactive' 438 | 439 | Current job state, usually C, C, C or C. 440 | 441 | =item task 442 | 443 | task => 'foo' 444 | 445 | Task name. 446 | 447 | =item time 448 | 449 | time => 78411177 450 | 451 | Server time. 452 | 453 | =item worker 454 | 455 | worker => '154' 456 | 457 | Id of worker that is processing the job. 458 | 459 | =back 460 | 461 | =head2 list_locks 462 | 463 | my $results = $backend->list_locks($offset, $limit); 464 | my $results = $backend->list_locks($offset, $limit, {names => ['foo']}); 465 | 466 | Returns information about locks in batches. Meant to be overloaded in a subclass. 467 | 468 | # Get the total number of results (without limit) 469 | my $num = $backend->list_locks(0, 100, {names => ['bar']})->{total}; 470 | 471 | # Check expiration time 472 | my $results = $backend->list_locks(0, 1, {names => ['foo']}); 473 | my $expires = $results->{locks}[0]{expires}; 474 | 475 | These options are currently available: 476 | 477 | =over 2 478 | 479 | =item names 480 | 481 | names => ['foo', 'bar'] 482 | 483 | List only locks with these names. 484 | 485 | =back 486 | 487 | These fields are currently available: 488 | 489 | =over 2 490 | 491 | =item expires 492 | 493 | expires => 784111777 494 | 495 | Epoch time this lock will expire. 496 | 497 | =item id 498 | 499 | id => 1 500 | 501 | Lock id. 502 | 503 | =item name 504 | 505 | name => 'foo' 506 | 507 | Lock name. 508 | 509 | =back 510 | 511 | =head2 list_workers 512 | 513 | my $results = $backend->list_workers($offset, $limit); 514 | my $results = $backend->list_workers($offset, $limit, {ids => [23]}); 515 | 516 | Returns information about workers in batches. Meant to be overloaded in a subclass. 517 | 518 | # Get the total number of results (without limit) 519 | my $num = $backend->list_workers(0, 100)->{total}; 520 | 521 | # Check worker host 522 | my $results = $backend->list_workers(0, 1, {ids => [$worker_id]}); 523 | my $host = $results->{workers}[0]{host}; 524 | 525 | These options are currently available: 526 | 527 | =over 2 528 | 529 | =item before 530 | 531 | before => 23 532 | 533 | List only workers before this id. 534 | 535 | =item ids 536 | 537 | ids => ['23', '24'] 538 | 539 | List only workers with these ids. 540 | 541 | =back 542 | 543 | These fields are currently available: 544 | 545 | =over 2 546 | 547 | =item id 548 | 549 | id => 22 550 | 551 | Worker id. 552 | 553 | =item host 554 | 555 | host => 'localhost' 556 | 557 | Worker host. 558 | 559 | =item jobs 560 | 561 | jobs => ['10023', '10024', '10025', '10029'] 562 | 563 | Ids of jobs the worker is currently processing. 564 | 565 | =item notified 566 | 567 | notified => 784111777 568 | 569 | Epoch time worker sent the last heartbeat. 570 | 571 | =item pid 572 | 573 | pid => 12345 574 | 575 | Process id of worker. 576 | 577 | =item started 578 | 579 | started => 784111777 580 | 581 | Epoch time worker was started. 582 | 583 | =item status 584 | 585 | status => {queues => ['default', 'important']} 586 | 587 | Hash reference with whatever status information the worker would like to share. 588 | 589 | =back 590 | 591 | =head2 lock 592 | 593 | my $bool = $backend->lock('foo', 3600); 594 | my $bool = $backend->lock('foo', 3600, {limit => 20}); 595 | 596 | Try to acquire a named lock that will expire automatically after the given amount of time in seconds. An expiration 597 | time of C<0> can be used to check if a named lock already exists without creating one. Meant to be overloaded in a 598 | subclass. 599 | 600 | These options are currently available: 601 | 602 | =over 2 603 | 604 | =item limit 605 | 606 | limit => 20 607 | 608 | Number of shared locks with the same name that can be active at the same time, defaults to C<1>. 609 | 610 | =back 611 | 612 | =head2 note 613 | 614 | my $bool = $backend->note($job_id, {mojo => 'rocks', minion => 'too'}); 615 | 616 | Change one or more metadata fields for a job. Setting a value to C will remove the field. Meant to be overloaded 617 | in a subclass. 618 | 619 | =head2 receive 620 | 621 | my $commands = $backend->receive($worker_id); 622 | 623 | Receive remote control commands for worker. Meant to be overloaded in a subclass. 624 | 625 | =head2 register_worker 626 | 627 | my $worker_id = $backend->register_worker; 628 | my $worker_id = $backend->register_worker($worker_id); 629 | my $worker_id = $backend->register_worker( 630 | $worker_id, {status => {queues => ['default', 'important']}}); 631 | 632 | Register worker or send heartbeat to show that this worker is still alive. Meant to be overloaded in a subclass. 633 | 634 | These options are currently available: 635 | 636 | =over 2 637 | 638 | =item status 639 | 640 | status => {queues => ['default', 'important']} 641 | 642 | Hash reference with whatever status information the worker would like to share. 643 | 644 | =back 645 | 646 | =head2 remove_job 647 | 648 | my $bool = $backend->remove_job($job_id); 649 | 650 | Remove C, C or C job from queue. Meant to be overloaded in a subclass. 651 | 652 | =head2 repair 653 | 654 | $backend->repair; 655 | 656 | Repair worker registry and job queue if necessary. Meant to be overloaded in a subclass. 657 | 658 | =head2 reset 659 | 660 | $backend->reset({all => 1}); 661 | 662 | Reset job queue. Meant to be overloaded in a subclass. 663 | 664 | These options are currently available: 665 | 666 | =over 2 667 | 668 | =item all 669 | 670 | all => 1 671 | 672 | Reset everything. 673 | 674 | =item locks 675 | 676 | locks => 1 677 | 678 | Reset only locks. 679 | 680 | =back 681 | 682 | =head2 retry_job 683 | 684 | my $bool = $backend->retry_job($job_id, $retries); 685 | my $bool = $backend->retry_job($job_id, $retries, {delay => 10}); 686 | 687 | Transition job back to C state, already C jobs may also be retried to change options. Meant to be 688 | overloaded in a subclass. 689 | 690 | These options are currently available: 691 | 692 | =over 2 693 | 694 | =item attempts 695 | 696 | attempts => 25 697 | 698 | Number of times performing this job will be attempted. 699 | 700 | =item delay 701 | 702 | delay => 10 703 | 704 | Delay job for this many seconds (from now), defaults to C<0>. 705 | 706 | =item expire 707 | 708 | expire => 300 709 | 710 | Job is valid for this many seconds (from now) before it expires. 711 | 712 | =item lax 713 | 714 | lax => 1 715 | 716 | Existing jobs this job depends on may also have transitioned to the C state to allow for it to be processed, 717 | defaults to C. Note that this option is B and might change without warning! 718 | 719 | =item parents 720 | 721 | parents => [$id1, $id2, $id3] 722 | 723 | Jobs this job depends on. 724 | 725 | =item priority 726 | 727 | priority => 5 728 | 729 | Job priority. 730 | 731 | =item queue 732 | 733 | queue => 'important' 734 | 735 | Queue to put job in. 736 | 737 | =back 738 | 739 | =head2 stats 740 | 741 | my $stats = $backend->stats; 742 | 743 | Get statistics for the job queue. Meant to be overloaded in a subclass. 744 | 745 | These fields are currently available: 746 | 747 | =over 2 748 | 749 | =item active_jobs 750 | 751 | active_jobs => 100 752 | 753 | Number of jobs in C state. 754 | 755 | =item active_locks 756 | 757 | active_locks => 100 758 | 759 | Number of active named locks. 760 | 761 | =item active_workers 762 | 763 | active_workers => 100 764 | 765 | Number of workers that are currently processing a job. 766 | 767 | =item delayed_jobs 768 | 769 | delayed_jobs => 100 770 | 771 | Number of jobs in C state that are scheduled to run at specific time in the future or have unresolved 772 | dependencies. 773 | 774 | =item enqueued_jobs 775 | 776 | enqueued_jobs => 100000 777 | 778 | Rough estimate of how many jobs have ever been enqueued. 779 | 780 | =item failed_jobs 781 | 782 | failed_jobs => 100 783 | 784 | Number of jobs in C state. 785 | 786 | =item finished_jobs 787 | 788 | finished_jobs => 100 789 | 790 | Number of jobs in C state. 791 | 792 | =item inactive_jobs 793 | 794 | inactive_jobs => 100 795 | 796 | Number of jobs in C state. 797 | 798 | =item inactive_workers 799 | 800 | inactive_workers => 100 801 | 802 | Number of workers that are currently not processing a job. 803 | 804 | =item uptime 805 | 806 | uptime => 1000 807 | 808 | Uptime in seconds. 809 | 810 | =item workers 811 | 812 | workers => 200; 813 | 814 | Number of registered workers. 815 | 816 | =back 817 | 818 | =head2 unlock 819 | 820 | my $bool = $backend->unlock('foo'); 821 | 822 | Release a named lock. Meant to be overloaded in a subclass. 823 | 824 | =head2 unregister_worker 825 | 826 | $backend->unregister_worker($worker_id); 827 | 828 | Unregister worker. Meant to be overloaded in a subclass. 829 | 830 | =head1 SEE ALSO 831 | 832 | L, L, L, L, L. 833 | 834 | =cut 835 | -------------------------------------------------------------------------------- /lib/Minion/Backend/resources/migrations/pg.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- These are the migrations for the PostgreSQL Minion backend. They are only used for upgrades to the latest version. 3 | -- Downgrades may be used to clean up the database, but they do not have to work with old versions of Minion. 4 | -- 5 | -- 18 up 6 | CREATE TYPE minion_state AS ENUM ('inactive', 'active', 'failed', 'finished'); 7 | CREATE TABLE IF NOT EXISTS minion_jobs ( 8 | id BIGSERIAL NOT NULL PRIMARY KEY, 9 | args JSONB NOT NULL CHECK(JSONB_TYPEOF(args) = 'array'), 10 | attempts INT NOT NULL DEFAULT 1, 11 | created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), 12 | delayed TIMESTAMP WITH TIME ZONE NOT NULL, 13 | finished TIMESTAMP WITH TIME ZONE, 14 | notes JSONB CHECK(JSONB_TYPEOF(notes) = 'object') NOT NULL DEFAULT '{}', 15 | parents BIGINT[] NOT NULL DEFAULT '{}', 16 | priority INT NOT NULL, 17 | queue TEXT NOT NULL DEFAULT 'default', 18 | result JSONB, 19 | retried TIMESTAMP WITH TIME ZONE, 20 | retries INT NOT NULL DEFAULT 0, 21 | started TIMESTAMP WITH TIME ZONE, 22 | state minion_state NOT NULL DEFAULT 'inactive'::MINION_STATE, 23 | task TEXT NOT NULL, 24 | worker BIGINT 25 | ); 26 | CREATE INDEX ON minion_jobs (state, priority DESC, id); 27 | CREATE INDEX ON minion_jobs USING GIN (parents); 28 | CREATE TABLE IF NOT EXISTS minion_workers ( 29 | id BIGSERIAL NOT NULL PRIMARY KEY, 30 | host TEXT NOT NULL, 31 | inbox JSONB CHECK(JSONB_TYPEOF(inbox) = 'array') NOT NULL DEFAULT '[]', 32 | notified TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), 33 | pid INT NOT NULL, 34 | started TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), 35 | status JSONB CHECK(JSONB_TYPEOF(status) = 'object') NOT NULL DEFAULT '{}' 36 | ); 37 | CREATE UNLOGGED TABLE IF NOT EXISTS minion_locks ( 38 | id BIGSERIAL NOT NULL PRIMARY KEY, 39 | name TEXT NOT NULL, 40 | expires TIMESTAMP WITH TIME ZONE NOT NULL 41 | ); 42 | CREATE INDEX ON minion_locks (name, expires); 43 | 44 | CREATE OR REPLACE FUNCTION minion_jobs_notify_workers() RETURNS trigger AS $$ 45 | BEGIN 46 | IF new.delayed <= NOW() THEN 47 | NOTIFY "minion.job"; 48 | END IF; 49 | RETURN NULL; 50 | END; 51 | $$ LANGUAGE plpgsql; 52 | CREATE TRIGGER minion_jobs_notify_workers_trigger 53 | AFTER INSERT OR UPDATE OF retries ON minion_jobs 54 | FOR EACH ROW EXECUTE PROCEDURE minion_jobs_notify_workers(); 55 | 56 | CREATE OR REPLACE FUNCTION minion_lock(TEXT, INT, INT) RETURNS BOOL AS $$ 57 | DECLARE 58 | new_expires TIMESTAMP WITH TIME ZONE = NOW() + (INTERVAL '1 second' * $2); 59 | BEGIN 60 | lock TABLE minion_locks IN exclusive mode; 61 | DELETE FROM minion_locks WHERE expires < NOW(); 62 | IF (SELECT COUNT(*) >= $3 FROM minion_locks WHERE NAME = $1) THEN 63 | RETURN false; 64 | END IF; 65 | IF new_expires > NOW() THEN 66 | INSERT INTO minion_locks (name, expires) VALUES ($1, new_expires); 67 | END IF; 68 | RETURN TRUE; 69 | END; 70 | $$ LANGUAGE plpgsql; 71 | 72 | -- 18 down 73 | DROP TABLE IF EXISTS minion_jobs; 74 | DROP TABLE if EXISTS minion_workers; 75 | DROP TABLE IF EXISTS minion_locks; 76 | DROP TYPE IF EXISTS minion_state; 77 | DROP TRIGGER IF EXISTS minion_jobs_notify_workers_trigger ON minion_jobs; 78 | DROP FUNCTION IF EXISTS minion_jobs_notify_workers(); 79 | DROP FUNCTION IF EXISTS minion_lock(TEXT, INT, INT); 80 | 81 | -- 19 up 82 | CREATE INDEX ON minion_jobs USING GIN (notes); 83 | 84 | -- 20 up 85 | ALTER TABLE minion_workers SET UNLOGGED; 86 | 87 | -- 22 up 88 | ALTER TABLE minion_jobs DROP COLUMN IF EXISTS SEQUENCE; 89 | ALTER TABLE minion_jobs DROP COLUMN IF EXISTS NEXT; 90 | ALTER TABLE minion_jobs ADD COLUMN EXPIRES TIMESTAMP WITH TIME ZONE; 91 | CREATE INDEX ON minion_jobs (expires); 92 | 93 | -- 23 up 94 | ALTER TABLE minion_jobs ADD COLUMN lax BOOL NOT NULL DEFAULT FALSE; 95 | 96 | -- 24 up 97 | CREATE INDEX ON minion_jobs (finished, state); 98 | -------------------------------------------------------------------------------- /lib/Minion/Command/minion.pm: -------------------------------------------------------------------------------- 1 | package Minion::Command::minion; 2 | use Mojo::Base 'Mojolicious::Commands'; 3 | 4 | has description => 'Minion job queue'; 5 | has hint => < sub { shift->extract_usage . "\nCommands:\n" }; 11 | has namespaces => sub { ['Minion::Command::minion'] }; 12 | 13 | sub help { shift->run(@_) } 14 | 15 | 1; 16 | 17 | =encoding utf8 18 | 19 | =head1 NAME 20 | 21 | Minion::Command::minion - Minion command 22 | 23 | =head1 SYNOPSIS 24 | 25 | Usage: APPLICATION minion COMMAND [OPTIONS] 26 | 27 | =head1 DESCRIPTION 28 | 29 | L lists available L commands. 30 | 31 | =head1 ATTRIBUTES 32 | 33 | L inherits all attributes from L and implements the following new ones. 34 | 35 | =head2 description 36 | 37 | my $description = $minion->description; 38 | $minion = $minion->description('Foo'); 39 | 40 | Short description of this command, used for the command list. 41 | 42 | =head2 hint 43 | 44 | my $hint = $minion->hint; 45 | $minion = $minion->hint('Foo'); 46 | 47 | Short hint shown after listing available L commands. 48 | 49 | =head2 message 50 | 51 | my $msg = $minion->message; 52 | $minion = $minion->message('Bar'); 53 | 54 | Short usage message shown before listing available L commands. 55 | 56 | =head2 namespaces 57 | 58 | my $namespaces = $minion->namespaces; 59 | $minion = $minion->namespaces(['MyApp::Command::minion']); 60 | 61 | Namespaces to search for available L commands, defaults to L. 62 | 63 | =head1 METHODS 64 | 65 | L inherits all methods from L and implements the following new ones. 66 | 67 | =head2 help 68 | 69 | $minion->help('app'); 70 | 71 | Print usage information for L command. 72 | 73 | =head1 SEE ALSO 74 | 75 | L, L, L, L, L. 76 | 77 | =cut 78 | -------------------------------------------------------------------------------- /lib/Minion/Command/minion/job.pm: -------------------------------------------------------------------------------- 1 | package Minion::Command::minion::job; 2 | use Mojo::Base 'Mojolicious::Command'; 3 | 4 | use Mojo::JSON qw(decode_json); 5 | use Mojo::Util qw(getopt tablify); 6 | 7 | has description => 'Manage Minion jobs'; 8 | has usage => sub { shift->extract_usage }; 9 | 10 | sub run { 11 | my ($self, @args) = @_; 12 | 13 | my ($args, $options) = ([], {}); 14 | getopt \@args, 15 | 'A|attempts=i' => \$options->{attempts}, 16 | 'a|args=s' => sub { $args = decode_json($_[1]) }, 17 | 'b|broadcast=s' => (\my $command), 18 | 'd|delay=i' => \$options->{delay}, 19 | 'E|expire=i' => \$options->{expire}, 20 | 'e|enqueue=s' => \my $enqueue, 21 | 'f|foreground' => \my $foreground, 22 | 'H|history' => \my $history, 23 | 'L|locks' => \my $locks, 24 | 'l|limit=i' => \(my $limit = 100), 25 | 'n|notes=s' => sub { $options->{notes} = decode_json($_[1]) }, 26 | 'o|offset=i' => \(my $offset = 0), 27 | 'P|parent=s' => sub { push @{$options->{parents}}, $_[1] }, 28 | 'p|priority=i' => \$options->{priority}, 29 | 'q|queue=s' => sub { push @{$options->{queues}}, $options->{queue} = $_[1] }, 30 | 'R|retry' => \my $retry, 31 | 'retry-failed' => \my $retry_failed, 32 | 'remove' => \my $remove, 33 | 'remove-failed' => \my $remove_failed, 34 | 'S|state=s' => sub { push @{$options->{states}}, $_[1] }, 35 | 's|stats' => \my $stats, 36 | 'T|tasks' => \my $tasks, 37 | 't|task=s' => sub { push @{$options->{tasks}}, $_[1] }, 38 | 'U|unlock=s' => \my $unlock, 39 | 'w|workers' => \my $workers, 40 | 'x|lax=s' => \$options->{lax}; 41 | 42 | # Worker remote control command 43 | my $minion = $self->app->minion; 44 | return $minion->backend->broadcast($command, $args, \@args) if $command; 45 | 46 | # Enqueue 47 | return say $minion->enqueue($enqueue, $args, $options) if $enqueue; 48 | 49 | # Show stats 50 | return $self->_stats if $stats; 51 | 52 | # Show history 53 | return print Minion::_dump($minion->history) if $history; 54 | 55 | # List tasks 56 | return print tablify [map { [$_, $minion->class_for_task($_)] } keys %{$minion->tasks}] if $tasks; 57 | 58 | # Iterate through failed jobs 59 | return $minion->jobs({states => ['failed']})->each(sub { $minion->job($_->{id})->remove }) if $remove_failed; 60 | return $minion->jobs({states => ['failed']})->each(sub { $minion->job($_->{id})->retry }) if $retry_failed; 61 | 62 | # Locks 63 | return $minion->unlock($unlock) if $unlock; 64 | return $self->_list_locks($offset, $limit, @args ? {names => \@args} : ()) if $locks; 65 | 66 | # Workers 67 | my $id = @args ? shift @args : undef; 68 | return $id ? $self->_worker($id) : $self->_list_workers($offset, $limit) if $workers; 69 | 70 | # List jobs 71 | return $self->_list_jobs($offset, $limit, $options) unless defined $id; 72 | die "Job does not exist.\n" unless my $job = $minion->job($id); 73 | 74 | # Remove job 75 | return $job->remove || die "Job is active.\n" if $remove; 76 | 77 | # Retry job 78 | return $job->retry($options) || die "Job is active.\n" if $retry; 79 | 80 | # Perform job in foreground 81 | return $minion->foreground($id) || die "Job is not ready.\n" if $foreground; 82 | 83 | # Job info 84 | print Minion::_dump(Minion::_datetime($job->info)); 85 | } 86 | 87 | sub _list_jobs { 88 | my $jobs = shift->app->minion->backend->list_jobs(@_)->{jobs}; 89 | print tablify [map { [@$_{qw(id state queue task)}] } @$jobs]; 90 | } 91 | 92 | sub _list_locks { 93 | my $locks = shift->app->minion->backend->list_locks(@_)->{locks}; 94 | @$locks = map { Minion::_datetime($_) } @$locks; 95 | print tablify [map { [@$_{qw(id name expires)}] } @$locks]; 96 | } 97 | 98 | sub _list_workers { 99 | my $workers = shift->app->minion->backend->list_workers(@_)->{workers}; 100 | my @workers = map { [$_->{id}, $_->{host} . ':' . $_->{pid}] } @$workers; 101 | print tablify \@workers; 102 | } 103 | 104 | sub _stats { print Minion::_dump(shift->app->minion->stats) } 105 | 106 | sub _worker { 107 | my $worker = shift->app->minion->backend->list_workers(0, 1, {ids => [shift]})->{workers}[0]; 108 | die "Worker does not exist.\n" unless $worker; 109 | print Minion::_dump(Minion::_datetime($worker)); 110 | } 111 | 112 | 1; 113 | 114 | =encoding utf8 115 | 116 | =head1 NAME 117 | 118 | Minion::Command::minion::job - Minion job command 119 | 120 | =head1 SYNOPSIS 121 | 122 | Usage: APPLICATION minion job [OPTIONS] [IDS] 123 | 124 | ./myapp.pl minion job 125 | ./myapp.pl minion job 10023 126 | ./myapp.pl minion job -w 127 | ./myapp.pl minion job -w 23 128 | ./myapp.pl minion job -s 129 | ./myapp.pl minion job -f 10023 130 | ./myapp.pl minion job -q important -t foo -t bar -S inactive 131 | ./myapp.pl minion job -q 'host:localhost' -S inactive 132 | ./myapp.pl minion job -e foo -a '[23, "bar"]' 133 | ./myapp.pl minion job -e foo -x 1 -P 10023 -P 10024 -p 5 -q important 134 | ./myapp.pl minion job -e 'foo' -n '{"test":123}' 135 | ./myapp.pl minion job -R -d 10 -E 300 10023 136 | ./myapp.pl minion job --remove 10023 137 | ./myapp.pl minion job --retry-failed 138 | ./myapp.pl minion job -n '["test"]' 139 | ./myapp.pl minion job -L 140 | ./myapp.pl minion job -L some_lock some_other_lock 141 | ./myapp.pl minion job -b jobs -a '[12]' 142 | ./myapp.pl minion job -b jobs -a '[12]' 23 24 25 143 | 144 | Options: 145 | -A, --attempts Number of times performing this new job will be 146 | attempted, defaults to 1 147 | -a, --args Arguments for new job or worker remote control 148 | command in JSON format 149 | -b, --broadcast Broadcast remote control command to one or more 150 | workers 151 | -d, --delay Delay new job for this many seconds 152 | -E, --expire New job is valid for this many seconds before 153 | it expires 154 | -e, --enqueue New job to be enqueued 155 | -f, --foreground Retry job in "minion_foreground" queue and 156 | perform it right away in the foreground (very 157 | useful for debugging) 158 | -H, --history Show queue history 159 | -h, --help Show this summary of available options 160 | --home Path to home directory of your application, 161 | defaults to the value of MOJO_HOME or 162 | auto-detection 163 | -L, --locks List active named locks 164 | -l, --limit Number of jobs/workers to show when listing 165 | them, defaults to 100 166 | -m, --mode Operating mode for your application, defaults to 167 | the value of MOJO_MODE/PLACK_ENV or 168 | "development" 169 | -n, --notes Notes in JSON format for new job or list only 170 | jobs with one of these notes 171 | -o, --offset Number of jobs/workers to skip when listing 172 | them, defaults to 0 173 | -P, --parent One or more jobs the new job depends on 174 | -p, --priority Priority of new job, defaults to 0 175 | -q, --queue Queue to put new job in, defaults to "default", 176 | or list only jobs in these queues 177 | -R, --retry Retry job 178 | --retry-failed Retry all failed jobs at once 179 | --remove Remove job 180 | --remove-failed Remove all failed jobs at once 181 | -S, --state List only jobs in these states 182 | -s, --stats Show queue statistics 183 | -T, --tasks List available tasks 184 | -t, --task List only jobs for these tasks 185 | -U, --unlock Release named lock 186 | -w, --workers List workers instead of jobs, or show 187 | information for a specific worker 188 | -x, --lax Jobs this job depends on may also have failed 189 | to allow for it to be processed 190 | 191 | =head1 DESCRIPTION 192 | 193 | L manages the L job queue. 194 | 195 | =head1 ATTRIBUTES 196 | 197 | L inherits all attributes from L and implements the following new 198 | ones. 199 | 200 | =head2 description 201 | 202 | my $description = $job->description; 203 | $job = $job->description('Foo'); 204 | 205 | Short description of this command, used for the command list. 206 | 207 | =head2 usage 208 | 209 | my $usage = $job->usage; 210 | $job = $job->usage('Foo'); 211 | 212 | Usage information for this command, used for the help screen. 213 | 214 | =head1 METHODS 215 | 216 | L inherits all methods from L and implements the following new 217 | ones. 218 | 219 | =head2 run 220 | 221 | $job->run(@ARGV); 222 | 223 | Run this command. 224 | 225 | =head1 SEE ALSO 226 | 227 | L, L, L, L, L. 228 | 229 | =cut 230 | -------------------------------------------------------------------------------- /lib/Minion/Command/minion/worker.pm: -------------------------------------------------------------------------------- 1 | package Minion::Command::minion::worker; 2 | use Mojo::Base 'Mojolicious::Command'; 3 | 4 | use Mojo::Util qw(getopt); 5 | 6 | has description => 'Start Minion worker'; 7 | has usage => sub { shift->extract_usage }; 8 | 9 | sub run { 10 | my ($self, @args) = @_; 11 | 12 | my $worker = $self->app->minion->worker; 13 | my $status = $worker->status; 14 | getopt \@args, 15 | 'C|command-interval=i' => \$status->{command_interval}, 16 | 'D|dequeue-timeout=i' => \$status->{dequeue_timeout}, 17 | 'I|heartbeat-interval=i' => \$status->{heartbeat_interval}, 18 | 'j|jobs=i' => \$status->{jobs}, 19 | 'q|queue=s' => \my @queues, 20 | 'R|repair-interval=i' => \$status->{repair_interval}, 21 | 's|spare=i' => \$status->{spare}, 22 | 'S|spare-min-priority=i' => \$status->{spare_min_priority}; 23 | $status->{queues} = \@queues if @queues; 24 | 25 | my $log = $self->app->log; 26 | $log->info("Worker $$ started"); 27 | $worker->on(dequeue => sub { pop->once(spawn => \&_spawn) }); 28 | $worker->run; 29 | $log->info("Worker $$ stopped"); 30 | } 31 | 32 | sub _spawn { 33 | my ($job, $pid) = @_; 34 | my ($id, $task) = ($job->id, $job->task); 35 | $job->app->log->debug(qq{Process $pid is performing job "$id" with task "$task"}); 36 | } 37 | 38 | 1; 39 | 40 | =encoding utf8 41 | 42 | =head1 NAME 43 | 44 | Minion::Command::minion::worker - Minion worker command 45 | 46 | =head1 SYNOPSIS 47 | 48 | Usage: APPLICATION minion worker [OPTIONS] 49 | 50 | ./myapp.pl minion worker 51 | ./myapp.pl minion worker -m production -I 15 -C 5 -R 3600 -j 10 52 | ./myapp.pl minion worker -q important -q default 53 | 54 | Options: 55 | -C, --command-interval Worker remote control command interval, 56 | defaults to 10 57 | -D, --dequeue-timeout Maximum amount of time to wait for 58 | jobs, defaults to 5 59 | -h, --help Show this summary of available options 60 | --home Path to home directory of your 61 | application, defaults to the value of 62 | MOJO_HOME or auto-detection 63 | -I, --heartbeat-interval Heartbeat interval, defaults to 300 64 | -j, --jobs Maximum number of jobs to perform 65 | parallel in forked worker processes 66 | (not including spare processes), 67 | defaults to 4 68 | -m, --mode Operating mode for your application, 69 | defaults to the value of 70 | MOJO_MODE/PLACK_ENV or "development" 71 | -q, --queue One or more queues to get jobs from, 72 | defaults to "default" 73 | -R, --repair-interval Repair interval, up to half of this 74 | value can be subtracted randomly to 75 | make sure not all workers repair at the 76 | same time, defaults to 21600 (6 hours) 77 | -s, --spare Number of spare worker processes to 78 | reserve for high priority jobs, 79 | defaults to 1 80 | -S, --spare-min-priority Minimum priority of jobs to use spare 81 | worker processes for, defaults to 1 82 | 83 | =head1 DESCRIPTION 84 | 85 | L starts a L worker. You can have as many workers as you like. 86 | 87 | =head1 WORKER SIGNALS 88 | 89 | The L process can be controlled at runtime with the following signals. 90 | 91 | =head2 INT, TERM 92 | 93 | Stop gracefully after finishing the current jobs. 94 | 95 | =head2 QUIT 96 | 97 | Stop immediately without finishing the current jobs. 98 | 99 | =head1 JOB SIGNALS 100 | 101 | The job processes spawned by the L process can be controlled at runtime with the 102 | following signals. 103 | 104 | =head2 INT, TERM 105 | 106 | This signal starts out with the operating system default and allows for jobs to install a custom signal handler to stop 107 | gracefully. 108 | 109 | =head2 USR1, USR2 110 | 111 | These signals start out being ignored and allow for jobs to install custom signal handlers. 112 | 113 | =head1 REMOTE CONTROL COMMANDS 114 | 115 | The L process can be controlled at runtime through L, 116 | from anywhere in the network, by broadcasting the following remote control commands. 117 | 118 | =head2 jobs 119 | 120 | $ ./myapp.pl minion job -b jobs -a '[10]' 121 | $ ./myapp.pl minion job -b jobs -a '[10]' 23 122 | 123 | Instruct one or more workers to change the number of jobs to perform concurrently. Setting this value to C<0> will 124 | effectively pause the worker. That means all current jobs will be finished, but no new ones accepted, until the number 125 | is increased again. 126 | 127 | =head2 kill 128 | 129 | $ ./myapp.pl minion job -b kill -a '["INT", 10025]' 130 | $ ./myapp.pl minion job -b kill -a '["INT", 10025]' 23 131 | 132 | Instruct one or more workers to send a signal to a job that is currently being performed. This command will be ignored 133 | by workers that do not have a job matching the id. That means it is safe to broadcast this command to all workers. 134 | 135 | =head2 stop 136 | 137 | $ ./myapp.pl minion job -b stop -a '[10025]' 138 | $ ./myapp.pl minion job -b stop -a '[10025]' 23 139 | 140 | Instruct one or more workers to stop a job that is currently being performed immediately. This command will be ignored 141 | by workers that do not have a job matching the id. That means it is safe to broadcast this command to all workers. 142 | 143 | =head1 ATTRIBUTES 144 | 145 | L inherits all attributes from L and implements the following 146 | new ones. 147 | 148 | =head2 description 149 | 150 | my $description = $worker->description; 151 | $worker = $worker->description('Foo'); 152 | 153 | Short description of this command, used for the command list. 154 | 155 | =head2 usage 156 | 157 | my $usage = $worker->usage; 158 | $worker = $worker->usage('Foo'); 159 | 160 | Usage information for this command, used for the help screen. 161 | 162 | =head1 METHODS 163 | 164 | L inherits all methods from L and implements the following new 165 | ones. 166 | 167 | =head2 run 168 | 169 | $worker->run(@ARGV); 170 | 171 | Run this command. 172 | 173 | =head1 SEE ALSO 174 | 175 | L, L, L, L, L. 176 | 177 | =cut 178 | -------------------------------------------------------------------------------- /lib/Minion/Guide.pod: -------------------------------------------------------------------------------- 1 | 2 | =encoding utf8 3 | 4 | =head1 NAME 5 | 6 | Minion::Guide - An introduction to Minion 7 | 8 | =head1 OVERVIEW 9 | 10 | This document contains an introduction to L and explains the most important features it has to offer. 11 | 12 | =head1 INTRODUCTION 13 | 14 | Essentials every L developer should know. 15 | 16 | =head2 Job queue 17 | 18 | Job queues allow you to process time and/or computationally intensive tasks in background processes, outside of the 19 | request/response lifecycle of web applications. Among those tasks you'll commonly find image resizing, spam filtering, 20 | HTTP downloads, building tarballs, warming caches and basically everything else you can imagine that's not super fast. 21 | 22 | Mojo::Server::Prefork +--------------+ Minion::Worker 23 | |- Mojo::Server::Daemon [1] enqueue job -> | | -> dequeue job |- Minion::Job [1] 24 | |- Mojo::Server::Daemon [2] | PostgreSQL | |- Minion::Job [2] 25 | |- Mojo::Server::Daemon [3] retrieve result <- | | <- store result |- Minion::Job [3] 26 | +- Mojo::Server::Daemon [4] +--------------+ |- Minion::Job [4] 27 | +- Minion::Job [5] 28 | 29 | They are not to be confused with time based job schedulers, such as cron or systemd timers. Both serve very different 30 | purposes, and cron jobs are in fact commonly used to enqueue L jobs that need to follow a schedule. For example 31 | to perform regular maintenance tasks. 32 | 33 | =head2 Mojolicious 34 | 35 | You can use L as a standalone job queue or integrate it into L applications with the plugin 36 | L. 37 | 38 | use Mojolicious::Lite -signatures; 39 | 40 | plugin Minion => {Pg => 'postgresql://sri:s3cret@localhost/test'}; 41 | 42 | # Slow task 43 | app->minion->add_task(poke_mojo => sub ($job, @args) { 44 | $job->app->ua->get('mojolicious.org'); 45 | $job->app->log->debug('We have poked mojolicious.org for a visitor'); 46 | }); 47 | 48 | # Perform job in a background worker process 49 | get '/' => sub ($c) { 50 | $c->minion->enqueue('poke_mojo'); 51 | $c->render(text => 'We will poke mojolicious.org for you soon.'); 52 | }; 53 | 54 | app->start; 55 | 56 | Background worker processes are usually started with the command L, which becomes 57 | automatically available when an application loads L. 58 | 59 | $ ./myapp.pl minion worker 60 | 61 | The worker process will fork a new process for every job that is being processed. This allows for resources such as 62 | memory to be returned to the operating system once a job is finished. Perl fork is very fast, so don't worry about the 63 | overhead. 64 | 65 | Minion::Worker 66 | |- Minion::Job [1] 67 | |- Minion::Job [2] 68 | +- ... 69 | 70 | By default up to four jobs will be processed in parallel, but that can be changed with configuration options or on 71 | demand with signals. 72 | 73 | $ ./myapp.pl minion worker -j 12 74 | 75 | Jobs can be managed right from the command line with L. 76 | 77 | $ ./myapp.pl minion job 78 | 79 | You can also add an admin ui to your application by loading the plugin L. Just make 80 | sure to secure access before making your application publicly accessible. 81 | 82 | # Make admin ui available under "/minion" 83 | plugin 'Minion::Admin'; 84 | 85 | =head2 Deployment 86 | 87 | To manage background worker processes with systemd, you can use a unit configuration file like this. 88 | 89 | [Unit] 90 | Description=My Mojolicious application workers 91 | After=postgresql.service 92 | 93 | [Service] 94 | Type=simple 95 | ExecStart=/home/sri/myapp/myapp.pl minion worker -m production 96 | KillMode=process 97 | 98 | [Install] 99 | WantedBy=multi-user.target 100 | 101 | =head2 Consistency 102 | 103 | Every new job starts out as C, then progresses to C when it is dequeued by a worker, and finally ends 104 | up as C or C, depending on its result. Every C job can then be retried to progress back to the 105 | C state and start all over again. 106 | 107 | +----------+ 108 | | | 109 | +-----> | finished | 110 | +----------+ +--------+ | | | 111 | | | | | | +----------+ 112 | | inactive | -------> | active | ------+ 113 | | | | | | +----------+ 114 | +----------+ +--------+ | | | 115 | +-----> | failed | -----+ 116 | ^ | | | 117 | | +----------+ | 118 | | | 119 | +----------------------------------------------------------------+ 120 | 121 | The system is eventually consistent and will preserve job results for as long as you like, depending on 122 | L. But be aware that C results are preserved indefinitely, and need to be manually 123 | removed by an administrator if they are out of automatic retries. 124 | 125 | While individual workers can fail in the middle of processing a job, the system will detect this and ensure that no job 126 | is left in an uncertain state, depending on L. Jobs that do not get processed after a certain 127 | amount of time, depending on L, will be considered stuck and fail automatically. So an admin can 128 | take a look and resolve the issue. 129 | 130 | =head1 FEATURES 131 | 132 | L has many great features. This section is still very incomplete, but will be expanded over time. 133 | 134 | =head2 Priorities 135 | 136 | Every job enqueued with L<"enqueue" in Minion|Minion/"enqueue1"> has a priority. Jobs with a higher priority get 137 | performed first, the default priority is C<0>. Priorities can be positive or negative, but should be in the range 138 | between C<100> and C<-100>. 139 | 140 | # Default priority 141 | $minion->enqueue('check_links', ['https://mojolicious.org']); 142 | 143 | # High priority 144 | $minion->enqueue('check_links', ['https://mojolicious.org'], {priority => 30}); 145 | 146 | # Low priority 147 | $minion->enqueue('check_links', ['https://mojolicious.org'], {priority => -30}); 148 | 149 | You can use L to raise or lower the priority of a job. 150 | 151 | $job->retry({priority => 50}); 152 | 153 | =head2 Job results 154 | 155 | The result of a job has two parts. First there is its state, which can be C for a successfully processed job, 156 | and C for the opposite. And second there's a C data structure, that may be C, a scalar, a hash 157 | reference, or an array reference. You can check both at any time in the life cycle of a job with L, all 158 | you need is the job id. 159 | 160 | # Check job state 161 | my $state = $minion->job($job_id)->info->{state}; 162 | 163 | # Get job result 164 | my $result = $minion->job($job_id)->info->{result}; 165 | 166 | While the C will be assigned automatically by L, the C for C jobs is usually assigned 167 | manually with L<"finish" in Minion::Job|Minion::Job/"finish1">. 168 | 169 | $minion->add_task(job_with_result => sub ($job) { 170 | sleep 5; 171 | $job->finish({message => 'This job should have taken about 5 seconds'}); 172 | }); 173 | 174 | For jobs that C due to an exception, that exception will be assigned as C. 175 | 176 | $minion->add_task(job_that_fails => sub ($job) { 177 | sleep 5; 178 | die 'This job should always fail after 5 seconds'; 179 | }); 180 | 181 | But jobs can also fail manually with L. 182 | 183 | $minion->add_task(job_that_fails_with_result => sub ($job) { 184 | sleep 5; 185 | $job->fail({errors => ['This job should fail after 5 seconds']}); 186 | }); 187 | 188 | Retrieving job results is of course completely optional, and it is very common to have jobs where the result is 189 | unimportant. 190 | 191 | =head2 Named queues 192 | 193 | Each job can be enqueued with L<"enqueue" in Minion|Minion/"enqueue1"> into arbitrarily named queues, independent of all 194 | their other properties. This is commonly used to have separate classes of workers, for example to ensure that free 195 | customers of your web service do not negatively affect your service level agreements with paying customers. The default 196 | named queue is C, but aside from that it has no special properties. 197 | 198 | # Use "default" queue 199 | $minion->enqueue('check_links', ['https://mojolicious.org']); 200 | 201 | # Use custom "important" queue 202 | $minion->enqueue('check_links', ['https://mojolicious.org'], {queue => 'important'}); 203 | 204 | For every named queue you can start as many workers as you like with the command L. And 205 | each worker can process jobs from multiple named queues. So your workers can have overlapping responsibilities. 206 | 207 | $ ./myapp.pl minion worker -q default -q important 208 | 209 | There is one special named queue called C that you should avoid using directly. It is reserved for 210 | debugging jobs with L. 211 | 212 | =head2 Job progress 213 | 214 | Progress information and other job metadata can be stored in notes at any time during the life cycle of a job with 215 | L. The metadata can be arbitrary data structures constructed with scalars, hash references and array 216 | references. 217 | 218 | $minion->add_task(job_with_progress => sub ($job) { 219 | sleep 1; 220 | $job->note(progress => '25%'); 221 | sleep 1; 222 | $job->note(progress => '50%'); 223 | sleep 1; 224 | $job->note(progress => '75%'); 225 | sleep 1; 226 | $job->note(progress => '100%'); 227 | }); 228 | 229 | Notes, similar to job results, can be retrieved with L, all you need is the job id. 230 | 231 | # Get job metadata 232 | my $progress = $minion->job($job_id)->info->{notes}{progress}; 233 | 234 | You can also use notes to store arbitrary metadata with new jobs when you create them with 235 | L<"enqueue" in Minion|Minion/"enqueue1">. 236 | 237 | # Create job with metadata 238 | $minion->enqueue('job_with_progress', [], {notes => {progress => 0, something_else => [1, 2, 3]}}); 239 | 240 | The admin ui provided by L allows searching for jobs containing a certain note, so 241 | you can also use them to tag jobs. 242 | 243 | =head2 Delayed jobs 244 | 245 | The C option of L<"enqueue" in Minion|Minion/"enqueue1"> can be used to delay the processing of a job by a 246 | certain amount of seconds (from now). 247 | 248 | # Job will not be processed for 60 seconds 249 | $minion->enqueue('check_links', ['https://mojolicious.org'], {delay => 20}); 250 | 251 | You can use L to change the delay. 252 | 253 | $job->retry({delay => 10}); 254 | 255 | =head2 Expiring jobs 256 | 257 | The C option of L<"enqueue" in Minion|Minion/"enqueue1"> can be used to limit for how many seconds (from now) a 258 | job should be valid before it expires and gets deleted from the queue. 259 | 260 | # Job will vanish if it is not dequeued within 60 seconds 261 | $minion->enqueue('check_links', ['https://mojolicious.org'], {expire => 60}); 262 | 263 | You can use L to reset the expiration time. 264 | 265 | $job->retry({expire => 30}); 266 | 267 | =head2 Custom workers 268 | 269 | In cases where you don't want to use L together with L, you can just skip the plugins and write 270 | your own worker scripts. 271 | 272 | #!/usr/bin/perl 273 | use strict; 274 | use warnings; 275 | 276 | use Minion; 277 | 278 | # Connect to backend 279 | my $minion = Minion->new(Pg => 'postgresql://postgres@/test'); 280 | 281 | # Add tasks 282 | $minion->add_task(something_slow => sub ($job, @args) { 283 | sleep 5; 284 | say 'This is a background worker process.'; 285 | }); 286 | 287 | # Start a worker to perform up to 12 jobs concurrently 288 | my $worker = $minion->worker; 289 | $worker->status->{jobs} = 12; 290 | $worker->run; 291 | 292 | The method L contains all features you would expect from a L worker and can be easily 293 | configured with L. For even more customization options L also has a very rich 294 | low level API you could for example use to build workers that do not fork at all. 295 | 296 | =head2 Task plugins 297 | 298 | As your L application grows, you can move tasks into application specific plugins. 299 | 300 | package MyApp::Task::PokeMojo; 301 | use Mojo::Base 'Mojolicious::Plugin', -signatures; 302 | 303 | sub register ($self, $app, $config) { 304 | $app->minion->add_task(poke_mojo => sub ($job, @args) { 305 | $job->app->ua->get('mojolicious.org'); 306 | $job->app->log->debug('We have poked mojolicious.org for a visitor'); 307 | }); 308 | } 309 | 310 | 1; 311 | 312 | Which are loaded like any other plugin from your application. 313 | 314 | # Mojolicious 315 | $app->plugin('MyApp::Task::PokeMojo'); 316 | 317 | # Mojolicious::Lite 318 | plugin 'MyApp::Task::PokeMojo'; 319 | 320 | =head2 Task classes 321 | 322 | For more flexibility, or if you are using L as a standalone job queue, you can also move tasks into dedicated 323 | classes. Allowing the use of Perl features such as inheritance and roles. But be aware that support for task classes is 324 | still B and might change without warning! 325 | 326 | package MyApp::Task::PokeMojo; 327 | use Mojo::Base 'Minion::Job', -signatures; 328 | 329 | sub run ($self, @args) { 330 | $self->app->ua->get('mojolicious.org'); 331 | $self->app->log->debug('We have poked mojolicious.org for a visitor'); 332 | } 333 | 334 | 1; 335 | 336 | Task classes are registered just like any other task with L and you can even register the same class 337 | with multiple names. 338 | 339 | $minion->add_task(poke_mojo => 'MyApp::Task::PokeMojo'); 340 | 341 | =head1 MORE 342 | 343 | You can continue with L now or take a look at the L, which contains a lot more documentation and examples by many different 345 | authors. 346 | 347 | =head1 SUPPORT 348 | 349 | If you have any questions the documentation might not yet answer, don't hesitate to ask in the 350 | L or the official IRC channel C<#mojo> on C 351 | (L). 352 | 353 | =cut -------------------------------------------------------------------------------- /lib/Minion/Iterator.pm: -------------------------------------------------------------------------------- 1 | package Minion::Iterator; 2 | use Mojo::Base -base; 3 | 4 | has fetch => 10; 5 | has [qw(minion options)]; 6 | 7 | sub each { 8 | my ($self, $cb) = @_; 9 | while ($_ = $self->next) { $cb->($_) } 10 | } 11 | 12 | sub next { shift @{shift->_fetch(0)->{results}} } 13 | 14 | sub total { shift->_fetch(1)->{total} } 15 | 16 | sub _fetch { 17 | my ($self, $lazy) = @_; 18 | 19 | return $self if ($lazy && exists $self->{total}) || @{$self->{results} // []}; 20 | 21 | my $what = $self->{jobs} ? 'jobs' : 'workers'; 22 | my $method = "list_$what"; 23 | my $options = $self->options; 24 | my $results = $self->minion->backend->$method(0, $self->fetch, $options); 25 | 26 | $self->{total} = $results->{total} + ($self->{count} // 0); 27 | $self->{count} += my @results = @{$results->{$what}}; 28 | push @{$self->{results}}, @results; 29 | $options->{before} = $results[-1]{id} if @results; 30 | 31 | return $self; 32 | } 33 | 34 | 1; 35 | 36 | =encoding utf8 37 | 38 | =head1 NAME 39 | 40 | Minion::Iterator - Minion iterator 41 | 42 | =head1 SYNOPSIS 43 | 44 | use Minion::Iterator; 45 | 46 | my $iter = Minion::Iterator->new(minion => $minion, options => {states => ['inactive']}); 47 | 48 | =head1 DESCRIPTION 49 | 50 | L is an iterator for L listing methods. 51 | 52 | =head1 ATTRIBUTES 53 | 54 | L implements the following attributes. 55 | 56 | =head2 fetch 57 | 58 | my $fetch = $iter->fetch; 59 | $iter = $iter->fetch(2); 60 | 61 | Number of results to cache, defaults to C<10>. 62 | 63 | =head2 minion 64 | 65 | my $minion = $iter->minion; 66 | $iter = $iter->minion(Minion->new); 67 | 68 | L object this job belongs to. 69 | 70 | =head2 options 71 | 72 | my $options = $iter->options; 73 | $iter = $iter->options({states => ['inactive']}); 74 | 75 | Options to be passed to L or L. 76 | 77 | =head1 METHODS 78 | 79 | L inherits all methods from L and implements the following new ones. 80 | 81 | =head2 each 82 | 83 | $iter->each(sub {...}); 84 | 85 | Evaluate callback for each element in collection. The element will be the first argument passed to the callback, and is 86 | also available as C<$_>. 87 | 88 | =head2 next 89 | 90 | my $value = $iter->next; 91 | 92 | Get next value. 93 | 94 | =head2 total 95 | 96 | my $num = $iter->total; 97 | 98 | Total number of results. If results are removed in the backend while iterating, this number will become an estimate 99 | that gets updated every time new results are fetched. 100 | 101 | =head1 SEE ALSO 102 | 103 | L, L, L, L, L. 104 | 105 | =cut 106 | -------------------------------------------------------------------------------- /lib/Minion/Job.pm: -------------------------------------------------------------------------------- 1 | package Minion::Job; 2 | use Mojo::Base 'Mojo::EventEmitter'; 3 | 4 | use Carp qw(croak); 5 | use Mojo::Collection; 6 | use Mojo::IOLoop; 7 | use POSIX qw(WNOHANG); 8 | 9 | has [qw(args id minion retries task)]; 10 | 11 | sub app { shift->minion->app } 12 | 13 | sub execute { 14 | my $self = shift; 15 | return eval { 16 | my $task = $self->minion->tasks->{$self->emit('start')->task}; 17 | ref $task ? $self->$task(@{$self->args}) : $self->run(@{$self->args}); 18 | !!$self->emit('finish'); 19 | } ? undef : $@; 20 | } 21 | 22 | sub fail { 23 | my ($self, $err) = (shift, shift // 'Unknown error'); 24 | my $ok = $self->minion->backend->fail_job($self->id, $self->retries, $err); 25 | return $ok ? !!$self->emit(failed => $err) : undef; 26 | } 27 | 28 | sub finish { 29 | my ($self, $result) = @_; 30 | my $ok = $self->minion->backend->finish_job($self->id, $self->retries, $result); 31 | return $ok ? !!$self->emit(finished => $result) : undef; 32 | } 33 | 34 | sub info { $_[0]->minion->backend->list_jobs(0, 1, {ids => [$_[0]->id]})->{jobs}[0] } 35 | 36 | sub is_finished { 37 | my $self = shift; 38 | return undef unless waitpid($self->{pid}, WNOHANG) == $self->{pid}; 39 | $self->_reap($? ? (1, $? >> 8, $? & 127) : ()); 40 | return 1; 41 | } 42 | 43 | sub kill { CORE::kill($_[1], $_[0]->{pid}) } 44 | 45 | sub note { 46 | my $self = shift; 47 | return $self->minion->backend->note($self->id, {@_}); 48 | } 49 | 50 | sub parents { 51 | my $self = shift; 52 | my $minion = $self->minion; 53 | return Mojo::Collection->new(map { $minion->job($_) // () } @{($self->info || {})->{parents} || []}); 54 | } 55 | 56 | sub perform { 57 | my $self = shift; 58 | waitpid $self->start->pid, 0; 59 | $self->_reap($? ? (1, $? >> 8, $? & 127) : ()); 60 | } 61 | 62 | sub pid { shift->{pid} } 63 | 64 | sub remove { $_[0]->minion->backend->remove_job($_[0]->id) } 65 | 66 | sub retry { 67 | my $self = shift; 68 | return $self->minion->backend->retry_job($self->id, $self->retries, @_); 69 | } 70 | 71 | sub run { croak 'Method "run" not implemented by subclass' } 72 | 73 | sub start { 74 | my $self = shift; 75 | 76 | # Parent 77 | die "Can't fork: $!" unless defined(my $pid = fork); 78 | return $self->emit(spawn => $pid) if $self->{pid} = $pid; 79 | 80 | # Reset event loop 81 | Mojo::IOLoop->reset; 82 | local $SIG{CHLD} = local $SIG{INT} = local $SIG{TERM} = local $SIG{QUIT} = 'DEFAULT'; 83 | local $SIG{USR1} = local $SIG{USR2} = 'IGNORE'; 84 | srand; 85 | 86 | # Child 87 | if (defined(my $err = $self->execute)) { $self->fail($err) } 88 | $self->emit('cleanup'); 89 | POSIX::_exit(0); 90 | } 91 | 92 | sub stop { shift->kill('KILL') } 93 | 94 | sub _reap { 95 | my ($self, $term, $exit, $sig) = @_; 96 | $self->emit(reap => $self->{pid}); 97 | $term ? $self->fail("Job terminated unexpectedly (exit code: $exit, signal: $sig)") : $self->finish; 98 | } 99 | 100 | 1; 101 | 102 | =encoding utf8 103 | 104 | =head1 NAME 105 | 106 | Minion::Job - Minion job 107 | 108 | =head1 SYNOPSIS 109 | 110 | package MyApp::Task::Foo; 111 | use Mojo::Base 'Minion::Job', -signatures; 112 | 113 | sub run ($self, @args) { 114 | 115 | # Magic here! :) 116 | } 117 | 118 | =head1 DESCRIPTION 119 | 120 | L is a container for L jobs. 121 | 122 | =head1 EVENTS 123 | 124 | L inherits all events from L and can emit the following new ones. 125 | 126 | =head2 cleanup 127 | 128 | $job->on(cleanup => sub ($job) { 129 | ... 130 | }); 131 | 132 | Emitted in the process performing this job right before the process will exit. 133 | 134 | $job->on(cleanup => sub ($job) { 135 | $job->app->log->debug("Process $$ is about to exit"); 136 | }); 137 | 138 | =head2 failed 139 | 140 | $job->on(failed => sub ($job, $err) { 141 | ... 142 | }); 143 | 144 | Emitted in the worker process managing this job or the process performing it, after it has transitioned to the 145 | C state. 146 | 147 | $job->on(failed => sub ($job, $err) { 148 | say "Something went wrong: $err"; 149 | }); 150 | 151 | =head2 finish 152 | 153 | $job->on(finish => sub ($job) { 154 | ... 155 | }); 156 | 157 | Emitted in the process performing this job if the task was successful. 158 | 159 | $job->on(finish => sub ($job) { 160 | my $id = $job->id; 161 | my $task = $job->task; 162 | $job->app->log->debug(qq{Job "$id" was performed with task "$task"}); 163 | }); 164 | 165 | =head2 finished 166 | 167 | $job->on(finished => sub ($job, $result) { 168 | ... 169 | }); 170 | 171 | Emitted in the worker process managing this job or the process performing it, after it has transitioned to the 172 | C state. 173 | 174 | $job->on(finished => sub ($job, $result) { 175 | my $id = $job->id; 176 | say "Job $id is finished."; 177 | }); 178 | 179 | =head2 reap 180 | 181 | $job->on(reap => sub ($job, $pid) { 182 | ... 183 | }); 184 | 185 | Emitted in the worker process managing this job, after the process performing it has exited. 186 | 187 | $job->on(reap => sub ($job, $pid) { 188 | my $id = $job->id; 189 | say "Job $id ran in process $pid"; 190 | }); 191 | 192 | =head2 spawn 193 | 194 | $job->on(spawn => sub ($job, $pid) { 195 | ... 196 | }); 197 | 198 | Emitted in the worker process managing this job, after a new process has been spawned for processing. 199 | 200 | $job->on(spawn => sub ($job, $pid) { 201 | my $id = $job->id; 202 | say "Job $id running in process $pid"; 203 | }); 204 | 205 | =head2 start 206 | 207 | $job->on(start => sub ($job) { 208 | ... 209 | }); 210 | 211 | Emitted in the process performing this job, after it has been spawned. 212 | 213 | $job->on(start => sub ($job) { 214 | $0 = $job->id; 215 | }); 216 | 217 | =head1 ATTRIBUTES 218 | 219 | L implements the following attributes. 220 | 221 | =head2 args 222 | 223 | my $args = $job->args; 224 | $job = $job->args([]); 225 | 226 | Arguments passed to task. 227 | 228 | =head2 id 229 | 230 | my $id = $job->id; 231 | $job = $job->id($id); 232 | 233 | Job id. 234 | 235 | =head2 minion 236 | 237 | my $minion = $job->minion; 238 | $job = $job->minion(Minion->new); 239 | 240 | L object this job belongs to. 241 | 242 | =head2 retries 243 | 244 | my $retries = $job->retries; 245 | $job = $job->retries(5); 246 | 247 | Number of times job has been retried. 248 | 249 | =head2 task 250 | 251 | my $task = $job->task; 252 | $job = $job->task('foo'); 253 | 254 | Task name. 255 | 256 | =head1 METHODS 257 | 258 | L inherits all methods from L and implements the following new ones. 259 | 260 | =head2 app 261 | 262 | my $app = $job->app; 263 | 264 | Get application from L. 265 | 266 | # Longer version 267 | my $app = $job->minion->app; 268 | 269 | =head2 execute 270 | 271 | my $err = $job->execute; 272 | 273 | Perform job in this process and return C if the task was successful or an exception otherwise. Note that this 274 | method should only be used to implement custom workers. 275 | 276 | # Perform job in foreground 277 | if (my $err = $job->execute) { $job->fail($err) } 278 | else { $job->finish } 279 | 280 | =head2 fail 281 | 282 | my $bool = $job->fail; 283 | my $bool = $job->fail('Something went wrong!'); 284 | my $bool = $job->fail({whatever => 'Something went wrong!'}); 285 | 286 | Transition from C to C state with or without a result, and if there are attempts remaining, transition 287 | back to C with a delay based on L. 288 | 289 | =head2 finish 290 | 291 | my $bool = $job->finish; 292 | my $bool = $job->finish('All went well!'); 293 | my $bool = $job->finish({whatever => 'All went well!'}); 294 | 295 | Transition from C to C state with or without a result. 296 | 297 | =head2 info 298 | 299 | my $info = $job->info; 300 | 301 | Get job information. 302 | 303 | # Check job state 304 | my $state = $job->info->{state}; 305 | 306 | # Get job metadata 307 | my $progress = $job->info->{notes}{progress}; 308 | 309 | # Get job result 310 | my $result = $job->info->{result}; 311 | 312 | These fields are currently available: 313 | 314 | =over 2 315 | 316 | =item args 317 | 318 | args => ['foo', 'bar'] 319 | 320 | Job arguments. 321 | 322 | =item attempts 323 | 324 | attempts => 25 325 | 326 | Number of times performing this job will be attempted. 327 | 328 | =item children 329 | 330 | children => ['10026', '10027', '10028'] 331 | 332 | Jobs depending on this job. 333 | 334 | =item created 335 | 336 | created => 784111777 337 | 338 | Epoch time job was created. 339 | 340 | =item delayed 341 | 342 | delayed => 784111777 343 | 344 | Epoch time job was delayed to. 345 | 346 | =item expires 347 | 348 | expires => 784111777 349 | 350 | Epoch time job is valid until before it expires. 351 | 352 | =item finished 353 | 354 | finished => 784111777 355 | 356 | Epoch time job was finished. 357 | 358 | =item lax 359 | 360 | lax => 0 361 | 362 | Existing jobs this job depends on may also have failed to allow for it to be processed. 363 | 364 | =item notes 365 | 366 | notes => {foo => 'bar', baz => [1, 2, 3]} 367 | 368 | Hash reference with arbitrary metadata for this job. 369 | 370 | =item parents 371 | 372 | parents => ['10023', '10024', '10025'] 373 | 374 | Jobs this job depends on. 375 | 376 | =item priority 377 | 378 | priority => 3 379 | 380 | Job priority. 381 | 382 | =item queue 383 | 384 | queue => 'important' 385 | 386 | Queue name. 387 | 388 | =item result 389 | 390 | result => 'All went well!' 391 | 392 | Job result. 393 | 394 | =item retried 395 | 396 | retried => 784111777 397 | 398 | Epoch time job has been retried. 399 | 400 | =item retries 401 | 402 | retries => 3 403 | 404 | Number of times job has been retried. 405 | 406 | =item started 407 | 408 | started => 784111777 409 | 410 | Epoch time job was started. 411 | 412 | =item state 413 | 414 | state => 'inactive' 415 | 416 | Current job state, usually C, C, C or C. 417 | 418 | =item task 419 | 420 | task => 'foo' 421 | 422 | Task name. 423 | 424 | =item time 425 | 426 | time => 784111777 427 | 428 | Server time. 429 | 430 | =item worker 431 | 432 | worker => '154' 433 | 434 | Id of worker that is processing the job. 435 | 436 | =back 437 | 438 | =head2 is_finished 439 | 440 | my $bool = $job->is_finished; 441 | 442 | Check if job performed with L is finished. Note that this method should only be used to implement custom 443 | workers. 444 | 445 | =head2 kill 446 | 447 | $job->kill('INT'); 448 | 449 | Send a signal to job performed with L. Note that this method should only be used to implement custom workers. 450 | 451 | =head2 note 452 | 453 | my $bool = $job->note(mojo => 'rocks', minion => 'too'); 454 | 455 | Change one or more metadata fields for this job. Setting a value to C will remove the field. The new values will 456 | get serialized by L (often with L), so you shouldn't send objects and be careful with 457 | binary data, nested data structures with hash and array references are fine though. 458 | 459 | # Share progress information 460 | $job->note(progress => 95); 461 | 462 | # Share stats 463 | $job->note(stats => {utime => '0.012628', stime => '0.002429'}); 464 | 465 | =head2 parents 466 | 467 | my $parents = $job->parents; 468 | 469 | Return a L object containing all jobs this job depends on as L objects. 470 | 471 | # Check parent state 472 | for my $parent ($job->parents->each) { 473 | my $info = $parent->info; 474 | say "$info->{id}: $info->{state}"; 475 | } 476 | 477 | =head2 perform 478 | 479 | $job->perform; 480 | 481 | Perform job in new process and wait for it to finish. Note that this method should only be used to implement custom 482 | workers. 483 | 484 | =head2 pid 485 | 486 | my $pid = $job->pid; 487 | 488 | Process id of the process spawned by L if available. Note that this method should only be used to implement 489 | custom workers. 490 | 491 | =head2 remove 492 | 493 | my $bool = $job->remove; 494 | 495 | Remove C, C or C job from queue. 496 | 497 | =head2 retry 498 | 499 | my $bool = $job->retry; 500 | my $bool = $job->retry({delay => 10}); 501 | 502 | Transition job back to C state, already C jobs may also be retried to change options. 503 | 504 | These options are currently available: 505 | 506 | =over 2 507 | 508 | =item attempts 509 | 510 | attempts => 25 511 | 512 | Number of times performing this job will be attempted. 513 | 514 | =item delay 515 | 516 | delay => 10 517 | 518 | Delay job for this many seconds (from now), defaults to C<0>. 519 | 520 | =item expire 521 | 522 | expire => 300 523 | 524 | Job is valid for this many seconds (from now) before it expires. 525 | 526 | =item lax 527 | 528 | lax => 1 529 | 530 | Existing jobs this job depends on may also have transitioned to the C state to allow for it to be processed, 531 | defaults to C. Note that this option is B and might change without warning! 532 | 533 | =item parents 534 | 535 | parents => [$id1, $id2, $id3] 536 | 537 | Jobs this job depends on. 538 | 539 | =item priority 540 | 541 | priority => 5 542 | 543 | Job priority. 544 | 545 | =item queue 546 | 547 | queue => 'important' 548 | 549 | Queue to put job in. 550 | 551 | =back 552 | 553 | =head2 run 554 | 555 | $job->run(@args); 556 | 557 | Task to perform by this job. Meant to be overloaded in a subclass to create a custom task class. Note that this method 558 | is B and might change without warning! 559 | 560 | =head2 start 561 | 562 | $job = $job->start; 563 | 564 | Perform job in new process, but do not wait for it to finish. Note that this method should only be used to implement 565 | custom workers. 566 | 567 | # Perform two jobs concurrently 568 | $job1->start; 569 | $job2->start; 570 | my ($first, $second); 571 | sleep 1 572 | until $first ||= $job1->is_finished and $second ||= $job2->is_finished; 573 | 574 | =head2 stop 575 | 576 | $job->stop; 577 | 578 | Stop job performed with L immediately. Note that this method should only be used to implement custom workers. 579 | 580 | =head1 SEE ALSO 581 | 582 | L, L, L, L, L. 583 | 584 | =cut 585 | -------------------------------------------------------------------------------- /lib/Minion/Worker.pm: -------------------------------------------------------------------------------- 1 | package Minion::Worker; 2 | use Mojo::Base 'Mojo::EventEmitter'; 3 | 4 | use Carp qw(croak); 5 | use Mojo::Util qw(steady_time); 6 | 7 | has [qw(commands status)] => sub { {} }; 8 | has [qw(id minion)]; 9 | 10 | sub add_command { $_[0]->commands->{$_[1]} = $_[2] and return $_[0] } 11 | 12 | sub dequeue { 13 | my ($self, $wait, $options) = @_; 14 | 15 | # Worker not registered 16 | return undef unless my $id = $self->id; 17 | 18 | my $minion = $self->minion; 19 | return undef unless my $job = $minion->backend->dequeue($id, $wait, $options); 20 | $job = $minion->class_for_task($job->{task}) 21 | ->new(args => $job->{args}, id => $job->{id}, minion => $minion, retries => $job->{retries}, task => $job->{task}); 22 | $self->emit(dequeue => $job); 23 | return $job; 24 | } 25 | 26 | sub info { $_[0]->minion->backend->list_workers(0, 1, {ids => [$_[0]->id]})->{workers}[0] } 27 | 28 | sub new { 29 | my $self = shift->SUPER::new(@_); 30 | $self->on(busy => sub { sleep 1 }); 31 | return $self; 32 | } 33 | 34 | sub process_commands { 35 | my $self = shift; 36 | 37 | for my $command (@{$self->minion->backend->receive($self->id)}) { 38 | next unless my $cb = $self->commands->{shift @$command}; 39 | $self->$cb(@$command); 40 | } 41 | 42 | return $self; 43 | } 44 | 45 | sub register { 46 | my $self = shift; 47 | my $status = {status => $self->status}; 48 | return $self->id($self->minion->backend->register_worker($self->id, $status)); 49 | } 50 | 51 | sub run { 52 | my $self = shift; 53 | 54 | my $status = $self->status; 55 | $status->{command_interval} //= 10; 56 | $status->{dequeue_timeout} //= 5; 57 | $status->{heartbeat_interval} //= 300; 58 | $status->{jobs} //= 4; 59 | $status->{queues} ||= ['default']; 60 | $status->{performed} //= 0; 61 | $status->{repair_interval} //= 21600; 62 | $status->{repair_interval} -= int rand $status->{repair_interval} / 2; 63 | $status->{spare} //= 1; 64 | $status->{spare_min_priority} //= 1; 65 | $status->{type} //= 'Perl'; 66 | 67 | # Reset event loop 68 | Mojo::IOLoop->reset; 69 | local $SIG{CHLD} = sub { }; 70 | local $SIG{INT} = local $SIG{TERM} = sub { $self->{finished}++ }; 71 | local $SIG{QUIT} = sub { 72 | ++$self->{finished} and kill 'KILL', map { $_->pid } @{$self->{jobs}}; 73 | }; 74 | 75 | # Remote control commands need to validate arguments carefully 76 | my $commands = $self->commands; 77 | local $commands->{jobs} = sub { $status->{jobs} = $_[1] if ($_[1] // '') =~ /^\d+$/ }; 78 | local $commands->{kill} = \&_kill; 79 | local $commands->{stop} = sub { $self->_kill('KILL', $_[1]) }; 80 | 81 | eval { $self->_work until $self->{finished} && !@{$self->{jobs}} }; 82 | my $err = $@; 83 | $self->unregister; 84 | croak $err if $err; 85 | } 86 | 87 | sub unregister { 88 | my $self = shift; 89 | $self->minion->backend->unregister_worker(delete $self->{id}); 90 | return $self; 91 | } 92 | 93 | sub _kill { 94 | my ($self, $signal, $id) = (shift, shift // '', shift // ''); 95 | return unless grep { $signal eq $_ } qw(INT TERM KILL USR1 USR2); 96 | $_->kill($signal) for grep { $_->id eq $id } @{$self->{jobs}}; 97 | } 98 | 99 | sub _work { 100 | my $self = shift; 101 | 102 | # Send heartbeats in regular intervals 103 | my $status = $self->status; 104 | $self->{last_heartbeat} ||= -$status->{heartbeat_interval}; 105 | $self->register and $self->{last_heartbeat} = steady_time 106 | if ($self->{last_heartbeat} + $status->{heartbeat_interval}) < steady_time; 107 | 108 | # Process worker remote control commands in regular intervals 109 | $self->{last_command} ||= 0; 110 | $self->process_commands and $self->{last_command} = steady_time 111 | if ($self->{last_command} + $status->{command_interval}) < steady_time; 112 | 113 | # Repair in regular intervals (randomize to avoid congestion) 114 | $self->{last_repair} ||= 0; 115 | if (($self->{last_repair} + $status->{repair_interval}) < steady_time) { 116 | $self->minion->repair; 117 | $self->{last_repair} = steady_time; 118 | } 119 | 120 | # Check if jobs are finished 121 | my $jobs = $self->{jobs} ||= []; 122 | @$jobs = map { $_->is_finished && ++$status->{performed} ? () : $_ } @$jobs; 123 | 124 | # Job limit has been reached or worker is stopping 125 | my @extra; 126 | if ($self->{finished} || ($status->{jobs} + $status->{spare}) <= @$jobs) { return $self->emit('busy') } 127 | elsif ($status->{jobs} <= @$jobs) { @extra = (min_priority => $status->{spare_min_priority}) } 128 | 129 | # Try to get more jobs 130 | my ($max, $queues) = @{$status}{qw(dequeue_timeout queues)}; 131 | my $job = $self->emit('wait')->dequeue($max => {queues => $queues, @extra}); 132 | push @$jobs, $job->start if $job; 133 | } 134 | 135 | 1; 136 | 137 | =encoding utf8 138 | 139 | =head1 NAME 140 | 141 | Minion::Worker - Minion worker 142 | 143 | =head1 SYNOPSIS 144 | 145 | use Minion::Worker; 146 | 147 | my $worker = Minion::Worker->new(minion => $minion); 148 | 149 | =head1 DESCRIPTION 150 | 151 | L performs jobs for L. 152 | 153 | =head1 WORKER SIGNALS 154 | 155 | The L process can be controlled at runtime with the following signals. 156 | 157 | =head2 INT, TERM 158 | 159 | Stop gracefully after finishing the current jobs. 160 | 161 | =head2 QUIT 162 | 163 | Stop immediately without finishing the current jobs. 164 | 165 | =head1 JOB SIGNALS 166 | 167 | The job processes spawned by the L process can be controlled at runtime with the following signals. 168 | 169 | =head2 INT, TERM 170 | 171 | This signal starts out with the operating system default and allows for jobs to install a custom signal handler to stop 172 | gracefully. 173 | 174 | =head2 USR1, USR2 175 | 176 | These signals start out being ignored and allow for jobs to install custom signal handlers. 177 | 178 | =head1 EVENTS 179 | 180 | L inherits all events from L and can emit the following new ones. 181 | 182 | =head2 busy 183 | 184 | $worker->on(busy => sub ($worker) { 185 | ... 186 | }); 187 | 188 | Emitted in the worker process when it is performing the maximum number of jobs in parallel. 189 | 190 | $worker->on(busy => sub ($worker) { 191 | my $max = $worker->status->{jobs}; 192 | say "Performing $max jobs."; 193 | }); 194 | 195 | =head2 dequeue 196 | 197 | $worker->on(dequeue => sub ($worker, $job) { 198 | ... 199 | }); 200 | 201 | Emitted in the worker process after a job has been dequeued. 202 | 203 | $worker->on(dequeue => sub ($worker, $job) { 204 | my $id = $job->id; 205 | say "Job $id has been dequeued."; 206 | }); 207 | 208 | =head2 wait 209 | 210 | $worker->on(wait => sub ($worker) { 211 | ... 212 | }); 213 | 214 | Emitted in the worker process before it tries to dequeue a job. 215 | 216 | $worker->on(wait => sub ($worker) { 217 | my $max = $worker->status->{dequeue_timeout}; 218 | say "Waiting up to $max seconds for a new job."; 219 | }); 220 | 221 | =head1 ATTRIBUTES 222 | 223 | L implements the following attributes. 224 | 225 | =head2 commands 226 | 227 | my $commands = $worker->commands; 228 | $worker = $worker->commands({jobs => sub {...}}); 229 | 230 | Registered worker remote control commands. 231 | 232 | =head2 id 233 | 234 | my $id = $worker->id; 235 | $worker = $worker->id($id); 236 | 237 | Worker id. 238 | 239 | =head2 minion 240 | 241 | my $minion = $worker->minion; 242 | $worker = $worker->minion(Minion->new); 243 | 244 | L object this worker belongs to. 245 | 246 | =head2 status 247 | 248 | my $status = $worker->status; 249 | $worker = $worker->status({queues => ['default', 'important']); 250 | 251 | Status information to configure workers started with L and to share every time L is called. 252 | 253 | =head1 METHODS 254 | 255 | L inherits all methods from L and implements the following new ones. 256 | 257 | =head2 add_command 258 | 259 | $worker = $worker->add_command(jobs => sub {...}); 260 | 261 | Register a worker remote control command. 262 | 263 | $worker->add_command(foo => sub ($worker, @args) { 264 | ... 265 | }); 266 | 267 | =head2 dequeue 268 | 269 | my $job = $worker->dequeue(0.5); 270 | my $job = $worker->dequeue(0.5 => {queues => ['important']}); 271 | 272 | Wait a given amount of time in seconds for a job, dequeue L object and transition from C to 273 | C state, or return C if queues were empty. 274 | 275 | These options are currently available: 276 | 277 | =over 2 278 | 279 | =item id 280 | 281 | id => '10023' 282 | 283 | Dequeue a specific job. 284 | 285 | =item min_priority 286 | 287 | min_priority => 3 288 | 289 | Do not dequeue jobs with a lower priority. 290 | 291 | =item queues 292 | 293 | queues => ['important'] 294 | 295 | One or more queues to dequeue jobs from, defaults to C. 296 | 297 | =back 298 | 299 | =head2 info 300 | 301 | my $info = $worker->info; 302 | 303 | Get worker information. 304 | 305 | # Check worker host 306 | my $host = $worker->info->{host}; 307 | 308 | These fields are currently available: 309 | 310 | =over 2 311 | 312 | =item host 313 | 314 | host => 'localhost' 315 | 316 | Worker host. 317 | 318 | =item jobs 319 | 320 | jobs => ['10023', '10024', '10025', '10029'] 321 | 322 | Ids of jobs the worker is currently processing. 323 | 324 | =item notified 325 | 326 | notified => 784111777 327 | 328 | Epoch time worker sent the last heartbeat. 329 | 330 | =item pid 331 | 332 | pid => 12345 333 | 334 | Process id of worker. 335 | 336 | =item started 337 | 338 | started => 784111777 339 | 340 | Epoch time worker was started. 341 | 342 | =item status 343 | 344 | status => {queues => ['default', 'important']} 345 | 346 | Hash reference with whatever status information the worker would like to share. 347 | 348 | =back 349 | 350 | =head2 new 351 | 352 | my $worker = Minion::Worker->new; 353 | my $worker = Minion::Worker->new(status => {foo => 'bar'}); 354 | my $worker = Minion::Worker->new({status => {foo => 'bar'}}); 355 | 356 | Construct a new L object and subscribe to L event with default handler that sleeps for one 357 | second. 358 | 359 | =head2 process_commands 360 | 361 | $worker = $worker->process_commands; 362 | 363 | Process worker remote control commands. 364 | 365 | =head2 register 366 | 367 | $worker = $worker->register; 368 | 369 | Register worker or send heartbeat to show that this worker is still alive. 370 | 371 | =head2 run 372 | 373 | $worker->run; 374 | 375 | Run worker and wait for L. 376 | 377 | # Start a worker for a special named queue 378 | my $worker = $minion->worker; 379 | $worker->status->{queues} = ['important']; 380 | $worker->run; 381 | 382 | These L options are currently available: 383 | 384 | =over 2 385 | 386 | =item command_interval 387 | 388 | command_interval => 20 389 | 390 | Worker remote control command interval, defaults to C<10>. 391 | 392 | =item dequeue_timeout 393 | 394 | dequeue_timeout => 5 395 | 396 | Maximum amount time in seconds to wait for a job, defaults to C<5>. 397 | 398 | =item heartbeat_interval 399 | 400 | heartbeat_interval => 60 401 | 402 | Heartbeat interval, defaults to C<300>. 403 | 404 | =item jobs 405 | 406 | jobs => 12 407 | 408 | Maximum number of jobs to perform parallel in forked worker processes (not including spare processes), defaults to C<4>. 409 | 410 | =item queues 411 | 412 | queues => ['test'] 413 | 414 | One or more queues to get jobs from, defaults to C. 415 | 416 | =item repair_interval 417 | 418 | repair_interval => 3600 419 | 420 | Repair interval, up to half of this value can be subtracted randomly to make sure not all workers repair at the same 421 | time, defaults to C<21600> (6 hours). 422 | 423 | =item spare 424 | 425 | spare => 2 426 | 427 | Number of spare worker processes to reserve for high priority jobs, defaults to C<1>. 428 | 429 | =item spare_min_priority 430 | 431 | spare_min_priority => 7 432 | 433 | Minimum priority of jobs to use spare worker processes for, defaults to C<1>. 434 | 435 | =back 436 | 437 | These remote control L are currently available: 438 | 439 | =over 2 440 | 441 | =item jobs 442 | 443 | $minion->broadcast('jobs', [10]); 444 | $minion->broadcast('jobs', [10], [$worker_id]); 445 | 446 | Instruct one or more workers to change the number of jobs to perform concurrently. Setting this value to C<0> will 447 | effectively pause the worker. That means all current jobs will be finished, but no new ones accepted, until the number 448 | is increased again. 449 | 450 | =item kill 451 | 452 | $minion->broadcast('kill', ['INT', 10025]); 453 | $minion->broadcast('kill', ['INT', 10025], [$worker_id]); 454 | 455 | Instruct one or more workers to send a signal to a job that is currently being performed. This command will be ignored 456 | by workers that do not have a job matching the id. That means it is safe to broadcast this command to all workers. 457 | 458 | =item stop 459 | 460 | $minion->broadcast('stop', [10025]); 461 | $minion->broadcast('stop', [10025], [$worker_id]); 462 | 463 | Instruct one or more workers to stop a job that is currently being performed immediately. This command will be ignored 464 | by workers that do not have a job matching the id. That means it is safe to broadcast this command to all workers. 465 | 466 | =back 467 | 468 | =head2 unregister 469 | 470 | $worker = $worker->unregister; 471 | 472 | Unregister worker. 473 | 474 | =head1 SEE ALSO 475 | 476 | L, L, L, L, L. 477 | 478 | =cut 479 | -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion.pm: -------------------------------------------------------------------------------- 1 | package Mojolicious::Plugin::Minion; 2 | use Mojo::Base 'Mojolicious::Plugin'; 3 | 4 | use Minion; 5 | 6 | sub register { 7 | my ($self, $app, $conf) = @_; 8 | push @{$app->commands->namespaces}, 'Minion::Command'; 9 | my $minion = Minion->new(%$conf)->app($app); 10 | $app->helper(minion => sub {$minion}); 11 | } 12 | 13 | 1; 14 | 15 | =encoding utf8 16 | 17 | =head1 NAME 18 | 19 | Mojolicious::Plugin::Minion - Minion job queue plugin 20 | 21 | =head1 SYNOPSIS 22 | 23 | # Mojolicious (choose a backend) 24 | $self->plugin(Minion => {Pg => 'postgresql://postgres@/test'}); 25 | 26 | # Mojolicious::Lite (choose a backend) 27 | plugin Minion => {Pg => 'postgresql://postgres@/test'}; 28 | 29 | # Share the database connection cache (PostgreSQL backend) 30 | helper pg => sub { state $pg = Mojo::Pg->new('postgresql://postgres@/test') }; 31 | plugin Minion => {Pg => app->pg}; 32 | 33 | # Add tasks to your application 34 | app->minion->add_task(slow_log => sub ($job, $msg) { 35 | sleep 5; 36 | $job->app->log->debug(qq{Received message "$msg"}); 37 | }); 38 | 39 | # Start jobs from anywhere in your application 40 | $c->minion->enqueue(slow_log => ['test 123']); 41 | 42 | # Perform jobs in your tests 43 | $t->get_ok('/start_slow_log_job')->status_is(200); 44 | $t->get_ok('/start_another_job')->status_is(200); 45 | $t->app->minion->perform_jobs; 46 | 47 | =head1 DESCRIPTION 48 | 49 | L is a L plugin for the L job queue. 50 | 51 | =head1 HELPERS 52 | 53 | L implements the following helpers. 54 | 55 | =head2 minion 56 | 57 | my $minion = $app->minion; 58 | my $minion = $c->minion; 59 | 60 | Get L object for application. 61 | 62 | # Add job to the queue 63 | $c->minion->enqueue(foo => ['bar', 'baz']); 64 | 65 | # Perform jobs for testing 66 | $app->minion->perform_jobs; 67 | 68 | =head1 METHODS 69 | 70 | L inherits all methods from L and implements the following new ones. 71 | 72 | =head2 register 73 | 74 | $plugin->register(Mojolicious->new, {Pg => 'postgresql://postgres@/test'}); 75 | 76 | Register plugin in L application. 77 | 78 | =head1 SEE ALSO 79 | 80 | L, L, L, L, L. 81 | 82 | =cut 83 | -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/Admin.pm: -------------------------------------------------------------------------------- 1 | package Mojolicious::Plugin::Minion::Admin; 2 | use Mojo::Base 'Mojolicious::Plugin'; 3 | 4 | use Mojo::File qw(path); 5 | 6 | sub register { 7 | my ($self, $app, $config) = @_; 8 | 9 | # Config 10 | my $prefix = $config->{route} // $app->routes->any('/minion'); 11 | $prefix->to(return_to => $config->{return_to} // '/'); 12 | 13 | # Static files 14 | my $resources = path(__FILE__)->sibling('resources'); 15 | push @{$app->static->paths}, $resources->child('public')->to_string; 16 | 17 | # Templates 18 | push @{$app->renderer->paths}, $resources->child('templates')->to_string; 19 | 20 | # Routes 21 | $prefix->get('/' => \&_dashboard)->name('minion_dashboard'); 22 | $prefix->get('/stats' => \&_stats)->name('minion_stats'); 23 | $prefix->get('/history' => \&_history)->name('minion_history'); 24 | $prefix->get('/jobs' => \&_list_jobs)->name('minion_jobs'); 25 | $prefix->patch('/jobs' => \&_manage_jobs)->name('minion_manage_jobs'); 26 | $prefix->get('/locks' => \&_list_locks)->name('minion_locks'); 27 | $prefix->delete('/locks' => \&_unlock)->name('minion_unlock'); 28 | $prefix->get('/workers' => \&_list_workers)->name('minion_workers'); 29 | } 30 | 31 | sub _dashboard { 32 | my $c = shift; 33 | my $history = $c->minion->backend->history; 34 | $c->render('minion/dashboard', history => $history); 35 | } 36 | 37 | sub _history { 38 | my $c = shift; 39 | $c->render(json => $c->minion->history); 40 | } 41 | 42 | sub _list_jobs { 43 | my $c = shift; 44 | 45 | my $v = $c->validation; 46 | $v->optional($_, 'not_empty') for qw(id note queue task); 47 | $v->optional('limit')->num; 48 | $v->optional('offset')->num; 49 | $v->optional('state', 'not_empty')->in(qw(active failed finished inactive)); 50 | my $options = {}; 51 | $v->is_valid($_) && ($options->{"${_}s"} = $v->every_param($_)) for qw(id note queue state task); 52 | my $limit = $v->param('limit') || 10; 53 | my $offset = $v->param('offset') || 0; 54 | 55 | my $results = $c->minion->backend->list_jobs($offset, $limit, $options); 56 | $c->render('minion/jobs', jobs => $results->{jobs}, total => $results->{total}, limit => $limit, offset => $offset); 57 | } 58 | 59 | sub _list_locks { 60 | my $c = shift; 61 | 62 | my $v = $c->validation; 63 | $v->optional('limit')->num; 64 | $v->optional('offset')->num; 65 | $v->optional('name'); 66 | my $options = {}; 67 | $options->{names} = $v->every_param('name') if $v->is_valid('name'); 68 | my $limit = $v->param('limit') || 10; 69 | my $offset = $v->param('offset') || 0; 70 | 71 | my $results = $c->minion->backend->list_locks($offset, $limit, $options); 72 | $c->render( 73 | 'minion/locks', 74 | locks => $results->{locks}, 75 | total => $results->{total}, 76 | limit => $limit, 77 | offset => $offset 78 | ); 79 | } 80 | 81 | sub _list_workers { 82 | my $c = shift; 83 | 84 | my $v = $c->validation; 85 | $v->optional('id'); 86 | $v->optional('limit')->num; 87 | $v->optional('offset')->num; 88 | my $limit = $v->param('limit') || 10; 89 | my $offset = $v->param('offset') || 0; 90 | my $options = {}; 91 | $options->{ids} = $v->every_param('id') if $v->is_valid('id'); 92 | 93 | my $results = $c->minion->backend->list_workers($offset, $limit, $options); 94 | $c->render( 95 | 'minion/workers', 96 | workers => $results->{workers}, 97 | total => $results->{total}, 98 | limit => $limit, 99 | offset => $offset 100 | ); 101 | } 102 | 103 | sub _manage_jobs { 104 | my $c = shift; 105 | 106 | my $v = $c->validation; 107 | $v->required('id'); 108 | $v->required('do')->in('remove', 'retry', 'sig_int', 'sig_term', 'sig_usr1', 'sig_usr2', 'stop'); 109 | 110 | $c->redirect_to('minion_jobs') if $v->has_error; 111 | 112 | my $minion = $c->minion; 113 | my $ids = $v->every_param('id'); 114 | my $do = $v->param('do'); 115 | if ($do eq 'retry') { 116 | my $fail = grep { $minion->job($_)->retry ? () : 1 } @$ids; 117 | if ($fail) { $c->flash(danger => "Couldn't retry all jobs.") } 118 | else { $c->flash(success => 'All selected jobs retried.') } 119 | } 120 | elsif ($do eq 'remove') { 121 | my $fail = grep { $minion->job($_)->remove ? () : 1 } @$ids; 122 | if ($fail) { $c->flash(danger => "Couldn't remove all jobs.") } 123 | else { 124 | $c->flash(success => 'All selected jobs removed.'); 125 | my $id_list = join ', ', @$ids; 126 | my $remote_address = $c->tx->remote_address; 127 | $c->log->debug(qq{Jobs removed by user "$remote_address": $id_list}); 128 | } 129 | } 130 | elsif ($do eq 'stop') { 131 | $minion->broadcast(stop => [$_]) for @$ids; 132 | $c->flash(info => 'Trying to stop all selected jobs.'); 133 | } 134 | elsif ($do =~ /^sig_(.+)$/) { 135 | my $signal = uc $1; 136 | $minion->broadcast(kill => [$signal, $_]) for @$ids; 137 | $c->flash(info => "Trying to send $signal signal to all selected jobs."); 138 | } 139 | 140 | $c->redirect_to($c->url_for('minion_jobs')->query(id => $ids)); 141 | } 142 | 143 | sub _stats { 144 | my $c = shift; 145 | $c->render(json => $c->minion->stats); 146 | } 147 | 148 | sub _unlock { 149 | my $c = shift; 150 | 151 | my $v = $c->validation; 152 | $v->required('name'); 153 | 154 | $c->redirect_to('minion_locks') if $v->has_error; 155 | 156 | my $names = $v->every_param('name'); 157 | my $minion = $c->minion; 158 | $minion->unlock($_) for @$names; 159 | $c->flash(success => 'All selected named locks released.'); 160 | 161 | $c->redirect_to('minion_locks'); 162 | } 163 | 164 | 1; 165 | 166 | =encoding utf8 167 | 168 | =head1 NAME 169 | 170 | Mojolicious::Plugin::Minion::Admin - Admin UI 171 | 172 | =head1 SYNOPSIS 173 | 174 | # Mojolicious 175 | $self->plugin('Minion::Admin'); 176 | 177 | # Mojolicious::Lite 178 | plugin 'Minion::Admin'; 179 | 180 | # Secure access to the admin ui with Basic authentication 181 | my $under = $self->routes->under('/minion' =>sub ($c) { 182 | return 1 if $c->req->url->to_abs->userinfo eq 'Bender:rocks'; 183 | $c->res->headers->www_authenticate('Basic'); 184 | $c->render(text => 'Authentication required!', status => 401); 185 | return undef; 186 | }); 187 | $self->plugin('Minion::Admin' => {route => $under}); 188 | 189 | =head1 DESCRIPTION 190 | 191 | L is a L plugin providing an admin ui for the L job queue. 192 | 193 | =head1 OPTIONS 194 | 195 | L supports the following options. 196 | 197 | =head2 return_to 198 | 199 | # Mojolicious::Lite 200 | plugin 'Minion::Admin' => {return_to => 'some_route'}; 201 | 202 | Name of route or path to return to when leaving the admin ui, defaults to C. 203 | 204 | =head2 route 205 | 206 | # Mojolicious::Lite 207 | plugin 'Minion::Admin' => {route => app->routes->any('/admin')}; 208 | 209 | L object to attach the admin ui to, defaults to generating a new one with the prefix 210 | C. 211 | 212 | =head1 METHODS 213 | 214 | L inherits all methods from L and implements the following new 215 | ones. 216 | 217 | =head2 register 218 | 219 | $plugin->register(Mojolicious->new); 220 | 221 | Register plugin in L application. 222 | 223 | =head1 SEE ALSO 224 | 225 | L, L, L, L, L. 226 | 227 | =cut 228 | -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/public/minion/app.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | div.stats { 5 | border: solid 1px #777; 6 | border-radius: 4px; 7 | box-shadow: 0 1px 1px rgba(0, 0, 0, .05); 8 | color: #777; 9 | margin-bottom: 20px; 10 | } 11 | div.stats .stats-description { 12 | font-size: 0.8em; 13 | padding: 0 15px 15px; 14 | text-align: center; 15 | } 16 | div.stats .stats-body { 17 | font-size: 26px; 18 | padding: 15px 15px 0; 19 | text-align: center; 20 | } 21 | pre { 22 | background: #fafafa; 23 | padding: 10px; 24 | white-space: pre-wrap; 25 | } 26 | .d-flex p { 27 | padding: 8px; 28 | } 29 | .expand.accordion-toggle { 30 | vertical-align: middle; 31 | } 32 | .expand.accordion-toggle .expand-icon:after { 33 | color: grey; 34 | content: "\f107"; 35 | float: right; 36 | } 37 | .expand.accordion-toggle.collapsed .expand-icon:after { 38 | content: "\f105"; 39 | } 40 | .expand-icon { 41 | display: inline-block; 42 | float: right; 43 | text-align: center; 44 | width: 1.25em; 45 | } 46 | .hiddenRow { 47 | padding: 0 !important; 48 | } 49 | .mojo-copy { 50 | font-size: 12px; 51 | } 52 | .mojo-copy, .mojo-copy a { 53 | color: #777; 54 | } 55 | .mojo-footer { 56 | background: url(../minion/pinstripe-light.png); 57 | background-color: #f2ece9; 58 | box-shadow: inset 0 10px 10px -5px #ddd; 59 | color: #666; 60 | } 61 | .mojo-footer a:hover { 62 | color: #000; 63 | text-decoration: none; 64 | } 65 | .mojo-social a { 66 | color: #666; 67 | font-size: 1.5em; 68 | padding: 0.2em; 69 | } 70 | .table thead tr th { 71 | border-top: 0 !important; 72 | } 73 | .table td { 74 | border: 0 !important; 75 | } 76 | .wrappable { 77 | word-break: break-all; 78 | word-wrap: break-word; 79 | } 80 | #backlog-chart { 81 | height: 200px; 82 | } 83 | #backlog-chart .inactive-jobs .area { 84 | fill: #a9e3be; 85 | stroke: #77c293; 86 | stroke-width: 8; 87 | } 88 | #history-chart { 89 | height: 200px; 90 | } 91 | #history-chart .processed { 92 | fill: #a9e3be; 93 | } 94 | -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/public/minion/app.js: -------------------------------------------------------------------------------- 1 | 2 | function checkAll () { 3 | $('.checkall').click(function () { 4 | const name = $(this).data('check'); 5 | const input = $('input[type=checkbox][name=' + name + ']'); 6 | input.prop('checked', $(this).prop('checked')); 7 | }); 8 | } 9 | 10 | function humanTime () { 11 | $('.from-now').each(function () { 12 | const date = $(this); 13 | date.text(moment(date.text() * 1000).fromNow()); 14 | }); 15 | $('.duration').each(function () { 16 | const date = $(this); 17 | console.log(date.text() * 1000); 18 | date.text(moment.duration(date.text() * 1000).humanize()); 19 | }); 20 | } 21 | 22 | function pageStats (data) {} 23 | 24 | function pollStats (url) { 25 | $.get(url).done(function (data) { 26 | $('.minion-stats-active-jobs').html(data.active_jobs); 27 | $('.minion-stats-active-locks').html(data.active_locks); 28 | $('.minion-stats-failed-jobs').html(data.failed_jobs); 29 | $('.minion-stats-finished-jobs').html(data.finished_jobs); 30 | $('.minion-stats-inactive-jobs').html(data.inactive_jobs); 31 | $('.minion-stats-workers').html(data.active_workers + data.inactive_workers); 32 | pageStats(data); 33 | setTimeout(() => { pollStats(url); }, 3000); 34 | }).fail(() => { setTimeout(() => { pollStats(url); }, 3000); }); 35 | } 36 | -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/public/minion/logo-black-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojolicious/minion/b82fd176d7809106a71e72f8e82b23863e048a04/lib/Mojolicious/Plugin/Minion/resources/public/minion/logo-black-2x.png -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/public/minion/logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojolicious/minion/b82fd176d7809106a71e72f8e82b23863e048a04/lib/Mojolicious/Plugin/Minion/resources/public/minion/logo-black.png -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/public/minion/pinstripe-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojolicious/minion/b82fd176d7809106a71e72f8e82b23863e048a04/lib/Mojolicious/Plugin/Minion/resources/public/minion/pinstripe-light.png -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojolicious/minion/b82fd176d7809106a71e72f8e82b23863e048a04/lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojolicious/minion/b82fd176d7809106a71e72f8e82b23863e048a04/lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojolicious/minion/b82fd176d7809106a71e72f8e82b23863e048a04/lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojolicious/minion/b82fd176d7809106a71e72f8e82b23863e048a04/lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojolicious/minion/b82fd176d7809106a71e72f8e82b23863e048a04/lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojolicious/minion/b82fd176d7809106a71e72f8e82b23863e048a04/lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojolicious/minion/b82fd176d7809106a71e72f8e82b23863e048a04/lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojolicious/minion/b82fd176d7809106a71e72f8e82b23863e048a04/lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojolicious/minion/b82fd176d7809106a71e72f8e82b23863e048a04/lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojolicious/minion/b82fd176d7809106a71e72f8e82b23863e048a04/lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojolicious/minion/b82fd176d7809106a71e72f8e82b23863e048a04/lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojolicious/minion/b82fd176d7809106a71e72f8e82b23863e048a04/lib/Mojolicious/Plugin/Minion/resources/public/minion/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/templates/layouts/minion.html.ep: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= title() || 'Minion' %> 4 | 5 | %= javascript '/minion/jquery/jquery.js' 6 | %= javascript '/minion/bootstrap/bootstrap.js' 7 | %= stylesheet '/minion/bootstrap/bootstrap.css' 8 | %= javascript '/minion/moment/moment.js' 9 | %= javascript '/minion/d3/d3.js' 10 | %= javascript '/minion/epoch/epoch.js' 11 | %= stylesheet '/minion/epoch/epoch.css' 12 | %= stylesheet '/minion/fontawesome/fontawesome.css' 13 | %= stylesheet '/minion/app.css' 14 | %= javascript '/minion/app.js' 15 | 22 | %= content_for 'head' 23 | 24 | 25 | 85 |
86 | 134 | %= content 135 |
136 |
137 | 155 |
156 | 157 | 158 | -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/templates/minion/_limit.html.ep: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/templates/minion/_notifications.html.ep: -------------------------------------------------------------------------------- 1 |
2 |
3 | % if (my $info = flash 'info') { 4 | 10 | % } 11 | % if (my $success = flash 'success') { 12 | 18 | % } 19 | % if (my $danger = flash 'danger') { 20 | 26 | % } 27 |
28 |
29 | -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/templates/minion/_pagination.html.ep: -------------------------------------------------------------------------------- 1 | % use POSIX; 2 | 3 | % my $last_page = POSIX::ceil($total / $limit); 4 | % my $current_page = int($offset / $limit) + 1; 5 | % my $prev_offset = ($offset - $limit) < 0 ? 0 : $offset - $limit; 6 | % my $next_offset = $offset + $limit; 7 | % my $last_offset = ($last_page - 1) * $limit; 8 | 9 | 68 | -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/templates/minion/dashboard.html.ep: -------------------------------------------------------------------------------- 1 | % layout 'minion', title => 'Minion - Dashboard'; 2 | 3 | % content_for head => begin 4 | 65 | % end 66 | 67 |
68 | 69 |
70 |
71 |
72 | <%= $Minion::VERSION %> 73 |
74 |
Minion Version
75 |
76 |
77 | 78 |
79 |
80 |
81 | <%= (split '::', ref minion->backend)[-1] %> 82 |
83 |
Backend
84 |
85 |
86 | 87 |
88 |
89 |
0
90 |
Uptime in Days
91 |
92 |
93 | 94 |
95 |
96 |
97 | % my $days = minion->remove_after / 86400; 98 | <%= int $days == $days ? $days : sprintf '%.2f', $days %> 99 |
100 |
Days of Results
101 |
102 |
103 | 104 |
105 |
106 |
0
107 |
Processed Jobs
108 |
109 |
110 | 111 |
112 |
113 |
0
114 |
Delayed Jobs
115 |
116 |
117 | 118 |
119 | 120 |

Real-time

121 | 122 |
123 |
124 |
125 |
126 |
127 | 128 |

History

129 | 130 |
131 |
132 |
133 |
134 |
135 | -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/templates/minion/jobs.html.ep: -------------------------------------------------------------------------------- 1 | % layout 'minion', title => 'Minion - Jobs'; 2 | 3 | % my $related = begin 4 | % my $job = shift; 5 | 6 | % my ($parents, $children, $id) = @{$job}{qw(parents children worker)}; 7 | % if (@$parents || @$children || $id) { 8 | 28 | % } 29 | % end 30 | 31 | %= include 'minion/_notifications' 32 | 33 | %= form_for 'minion_manage_jobs' => begin 34 |
35 |
36 |
37 | 40 | 43 | 46 |
47 | 51 | 65 |
66 |
67 |
68 |

69 | % if (@$jobs) { 70 | <%= $offset + 1 %>-<%= $offset + @$jobs %> of <%= $total %> 71 | % } 72 |

73 | %= include 'minion/_limit' 74 |
75 | 76 |
77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | % if (@$jobs) { 93 | % my $i = 0; 94 | % for my $job (@$jobs) { 95 | % $i++; 96 | % my $base = url_with->query({offset => 0}); 97 | 98 | 99 | 102 | 107 | 112 | 117 | 118 | 119 | 133 | % if (grep { $job->{state} eq $_ } qw(active finished failed)) { 134 | % my $end = $job->{state} eq 'active' ? 'time' : 'finished'; 135 | % my $runtime = $job->{$end} - $job->{started}; 136 | 139 | % } 140 | % elsif ($job->{delayed} > $job->{time}) { 141 | 147 | % } 148 | % else { 149 | 150 | % } 151 | 154 | 155 | 156 | 161 | 162 | 163 | % } 164 | % } 165 | % else { 166 | 167 | % } 168 |
Job IDTaskQueueCreatedRuntime
100 | 101 | 103 | 104 | <%= $job->{id} %> 105 | 106 | 108 | 109 | <%= $job->{task} %> 110 | 111 | 113 | 114 | <%= $job->{queue} %> 115 | 116 | <%= $job->{created} %><%= $related->($job) %>
157 |
158 |
<%= Minion::_dump(Minion::_datetime($job)) %>
159 |
160 |

No jobs found

169 |
170 |
171 | % end 172 | 173 |
174 | %= include 'minion/_pagination' 175 |
176 | -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/templates/minion/locks.html.ep: -------------------------------------------------------------------------------- 1 | % layout 'minion', title => 'Minion - Locks'; 2 | 3 | %= include 'minion/_notifications' 4 | 5 | %= form_for 'minion_unlock' => begin 6 |
7 |
8 | 11 |
12 |

13 | % if (@$locks) { 14 | <%= $offset + 1 %>-<%= $offset + @$locks %> of <%= $total %> 15 | % } 16 |

17 | %= include 'minion/_limit' 18 |
19 | 20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | % if (@$locks) { 32 | % my $i = 0; 33 | % for my $lock (@$locks) { 34 | % $i++; 35 | % my $base = url_with->query(offset => 0); 36 | 37 | 38 | 41 | 44 | 49 | 50 | 51 | 52 | % } 53 | % } 54 | % else { 55 | 56 | % } 57 |
Lock IDNameExpires
39 | 40 | 42 | <%= $lock->{id} %> 43 | 45 | 46 | <%= $lock->{name} %> 47 | 48 | <%= $lock->{expires} %>

No locks found

58 |
59 |
60 | % end 61 | 62 |
63 | %= include 'minion/_pagination' 64 |
65 | -------------------------------------------------------------------------------- /lib/Mojolicious/Plugin/Minion/resources/templates/minion/workers.html.ep: -------------------------------------------------------------------------------- 1 | % layout 'minion', title => 'Minion - Workers'; 2 | 3 | % my $related = begin 4 | % my $worker = shift; 5 | % my $jobs = $worker->{jobs}; 6 | % if (@$jobs) { 7 | 21 |
22 |
23 |

24 | % if (@$workers) { 25 | <%= $offset + 1 %>-<%= $offset + @$workers %> of <%= $total %> 26 | % } 27 |

28 |
29 | %= include 'minion/_limit' 30 | 31 | 32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | % if (@$workers) { 47 | % my $i = 0; 48 | % for my $worker (@$workers) { 49 | % $i++; 50 | 51 | 52 | 57 | 58 | 59 | 60 | 61 | 69 | 72 | 73 | 74 | 79 | 80 | 81 | % } 82 | % } 83 | % else { 84 | 85 | % } 86 |
Worker IDHostProcess IDStarted
53 | 54 | <%= $worker->{id} %> 55 | 56 | <%= $worker->{host} %><%= $worker->{pid} %><%= $worker->{started} %><%= $related->($worker) %>
75 |
76 |
<%= Minion::_dump(Minion::_datetime($worker)) %>
77 |
78 |

No workers found

87 |
88 |
89 | 90 |
91 | %= include 'minion/_pagination' 92 |
93 | -------------------------------------------------------------------------------- /t/backend.t: -------------------------------------------------------------------------------- 1 | use Mojo::Base -strict; 2 | 3 | use Test::More; 4 | use Minion::Backend; 5 | 6 | subtest 'Abstract methods' => sub { 7 | eval { Minion::Backend->broadcast }; 8 | like $@, qr/Method "broadcast" not implemented by subclass/, 'right error'; 9 | eval { Minion::Backend->dequeue }; 10 | like $@, qr/Method "dequeue" not implemented by subclass/, 'right error'; 11 | eval { Minion::Backend->enqueue }; 12 | like $@, qr/Method "enqueue" not implemented by subclass/, 'right error'; 13 | eval { Minion::Backend->fail_job }; 14 | like $@, qr/Method "fail_job" not implemented by subclass/, 'right error'; 15 | eval { Minion::Backend->finish_job }; 16 | like $@, qr/Method "finish_job" not implemented by subclass/, 'right error'; 17 | eval { Minion::Backend->history }; 18 | like $@, qr/Method "history" not implemented by subclass/, 'right error'; 19 | eval { Minion::Backend->list_jobs }; 20 | like $@, qr/Method "list_jobs" not implemented by subclass/, 'right error'; 21 | eval { Minion::Backend->list_locks }; 22 | like $@, qr/Method "list_locks" not implemented by subclass/, 'right error'; 23 | eval { Minion::Backend->list_workers }; 24 | like $@, qr/Method "list_workers" not implemented by subclass/, 'right error'; 25 | eval { Minion::Backend->lock }; 26 | like $@, qr/Method "lock" not implemented by subclass/, 'right error'; 27 | eval { Minion::Backend->note }; 28 | like $@, qr/Method "note" not implemented by subclass/, 'right error'; 29 | eval { Minion::Backend->receive }; 30 | like $@, qr/Method "receive" not implemented by subclass/, 'right error'; 31 | eval { Minion::Backend->register_worker }; 32 | like $@, qr/Method "register_worker" not implemented by subclass/, 'right error'; 33 | eval { Minion::Backend->remove_job }; 34 | like $@, qr/Method "remove_job" not implemented by subclass/, 'right error'; 35 | eval { Minion::Backend->repair }; 36 | like $@, qr/Method "repair" not implemented by subclass/, 'right error'; 37 | eval { Minion::Backend->reset }; 38 | like $@, qr/Method "reset" not implemented by subclass/, 'right error'; 39 | eval { Minion::Backend->retry_job }; 40 | like $@, qr/Method "retry_job" not implemented by subclass/, 'right error'; 41 | eval { Minion::Backend->stats }; 42 | like $@, qr/Method "stats" not implemented by subclass/, 'right error'; 43 | eval { Minion::Backend->unlock }; 44 | like $@, qr/Method "unlock" not implemented by subclass/, 'right error'; 45 | eval { Minion::Backend->unregister_worker }; 46 | like $@, qr/Method "unregister_worker" not implemented by subclass/, 'right error'; 47 | }; 48 | 49 | done_testing(); 50 | -------------------------------------------------------------------------------- /t/commands.t: -------------------------------------------------------------------------------- 1 | use Mojo::Base -strict; 2 | 3 | use Test::More; 4 | 5 | subtest 'minion' => sub { 6 | require Minion::Command::minion; 7 | my $minion = Minion::Command::minion->new; 8 | ok $minion->description, 'has a description'; 9 | like $minion->message, qr/minion/, 'has a message'; 10 | like $minion->hint, qr/help/, 'has a hint'; 11 | }; 12 | 13 | subtest 'job' => sub { 14 | require Minion::Command::minion::job; 15 | my $job = Minion::Command::minion::job->new; 16 | ok $job->description, 'has a description'; 17 | like $job->usage, qr/job/, 'has usage information'; 18 | }; 19 | 20 | subtest 'worker' => sub { 21 | require Minion::Command::minion::worker; 22 | my $worker = Minion::Command::minion::worker->new; 23 | ok $worker->description, 'has a description'; 24 | like $worker->usage, qr/worker/, 'has usage information'; 25 | }; 26 | 27 | done_testing(); 28 | -------------------------------------------------------------------------------- /t/lib/MinionTest/AddTestTask.pm: -------------------------------------------------------------------------------- 1 | package MinionTest::AddTestTask; 2 | use Mojo::Base 'Minion::Job'; 3 | 4 | sub run { 5 | my ($self, @args) = @_; 6 | $self->finish('My result is ' . ($args[0] + $args[1])); 7 | } 8 | 9 | 1; 10 | -------------------------------------------------------------------------------- /t/lib/MinionTest/BadTestTask.pm: -------------------------------------------------------------------------------- 1 | package MinionTest::BadTestTask; 2 | use Mojo::Base -base; 3 | 4 | 1; 5 | -------------------------------------------------------------------------------- /t/lib/MinionTest/EmptyTestTask.pm: -------------------------------------------------------------------------------- 1 | package MinionTest::EmptyTestTask; 2 | use Mojo::Base 'Minion::Job'; 3 | 4 | 1; 5 | -------------------------------------------------------------------------------- /t/lib/MinionTest/FailTestTask.pm: -------------------------------------------------------------------------------- 1 | package MinionTest::FailTestTask; 2 | use Mojo::Base 'Minion::Job'; 3 | 4 | sub run { 5 | my ($self, @args) = @_; 6 | my $task = $self->task; 7 | $self->fail("$task failed"); 8 | } 9 | 10 | 1; 11 | -------------------------------------------------------------------------------- /t/lib/MinionTest/NoResultTestTask.pm: -------------------------------------------------------------------------------- 1 | package MinionTest::NoResultTestTask; 2 | use Mojo::Base 'MinionTest::AddTestTask'; 3 | 4 | sub run { } 5 | 6 | 1; 7 | -------------------------------------------------------------------------------- /t/lib/MinionTest/SyntaxErrorTestTask.pm: -------------------------------------------------------------------------------- 1 | package MinionTest::SyntaxErrorTestTask; 2 | use Mojo::Base 'Minion::Job'; 3 | 4 | sub run { 5 | 6 | 1; 7 | -------------------------------------------------------------------------------- /t/pg_admin.t: -------------------------------------------------------------------------------- 1 | use Mojo::Base -strict; 2 | 3 | BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' } 4 | 5 | use Test::More; 6 | 7 | plan skip_all => 'set TEST_ONLINE to enable this test' unless $ENV{TEST_ONLINE}; 8 | 9 | use Mojolicious::Lite; 10 | use Test::Mojo; 11 | 12 | # Isolate tests 13 | require Mojo::Pg; 14 | my $pg = Mojo::Pg->new($ENV{TEST_ONLINE}); 15 | $pg->db->query('DROP SCHEMA IF EXISTS minion_admin_test CASCADE'); 16 | $pg->db->query('CREATE SCHEMA minion_admin_test'); 17 | plugin Minion => {Pg => $ENV{TEST_ONLINE}}; 18 | app->minion->backend->pg->search_path(['minion_admin_test']); 19 | 20 | app->minion->add_task(test => sub { }); 21 | my $finished = app->minion->enqueue('test'); 22 | app->minion->perform_jobs; 23 | my $inactive = app->minion->enqueue('test'); 24 | get '/home' => 'test_home'; 25 | 26 | plugin 'Minion::Admin'; 27 | 28 | my $t = Test::Mojo->new; 29 | 30 | subtest 'Dashboard' => sub { 31 | $t->get_ok('/minion')->status_is(200)->content_like(qr/Dashboard/)->element_exists('a[href=/]'); 32 | }; 33 | 34 | subtest 'Stats' => sub { 35 | $t->get_ok('/minion/stats') 36 | ->status_is(200) 37 | ->json_is('/active_jobs' => 0) 38 | ->json_is('/active_locks' => 0) 39 | ->json_is('/active_workers' => 0) 40 | ->json_is('/delayed_jobs' => 0) 41 | ->json_is('/enqueued_jobs' => 2) 42 | ->json_is('/failed_jobs' => 0) 43 | ->json_is('/finished_jobs' => 1) 44 | ->json_is('/inactive_jobs' => 1) 45 | ->json_is('/inactive_workers' => 0) 46 | ->json_has('/uptime'); 47 | }; 48 | 49 | subtest 'Jobs' => sub { 50 | $t->get_ok('/minion/jobs?state=inactive') 51 | ->status_is(200) 52 | ->text_like('tbody td a' => qr/$inactive/) 53 | ->text_unlike('tbody td a' => qr/$finished/); 54 | $t->get_ok('/minion/jobs?state=finished') 55 | ->status_is(200) 56 | ->text_like('tbody td a' => qr/$finished/) 57 | ->text_unlike('tbody td a' => qr/$inactive/); 58 | }; 59 | 60 | subtest 'Workers' => sub { 61 | $t->get_ok('/minion/workers')->status_is(200)->element_exists_not('tbody td a'); 62 | my $worker = app->minion->worker->register; 63 | $t->get_ok('/minion/workers') 64 | ->status_is(200) 65 | ->element_exists('tbody td a') 66 | ->text_like('tbody td a' => qr/@{[$worker->id]}/); 67 | $worker->unregister; 68 | $t->get_ok('/minion/workers')->status_is(200)->element_exists_not('tbody td a'); 69 | }; 70 | 71 | subtest 'Locks' => sub { 72 | $t->app->minion->lock('foo', 3600); 73 | $t->get_ok('/minion/stats')->status_is(200)->json_is('/active_locks' => 1); 74 | $t->app->minion->lock('bar', 3600); 75 | $t->get_ok('/minion/stats')->status_is(200)->json_is('/active_locks' => 2); 76 | $t->ua->max_redirects(5); 77 | $t->get_ok('/minion/locks')->status_is(200)->text_like('tbody td a' => qr/bar/); 78 | $t->get_ok('/minion/locks')->status_is(200)->text_like('tbody td#lock_id' => qr/2/); 79 | $t->get_ok('/minion/locks?name=foo')->status_is(200)->text_like('tbody td a' => qr/foo/); 80 | $t->get_ok('/minion/locks?name=foo')->status_is(200)->text_like('tbody td#lock_id' => qr/1/); 81 | $t->post_ok('/minion/locks?_method=DELETE&name=bar') 82 | ->status_is(200) 83 | ->text_like('tbody td a' => qr/foo/) 84 | ->text_like('.alert-success', qr/All selected named locks released/); 85 | is $t->tx->previous->res->code, 302, 'right status'; 86 | like $t->tx->previous->res->headers->location, qr/locks/, 'right "Location" value'; 87 | $t->post_ok('/minion/locks?_method=DELETE&name=foo') 88 | ->status_is(200) 89 | ->element_exists_not('tbody td a') 90 | ->text_like('.alert-success', qr/All selected named locks released/); 91 | is $t->tx->previous->res->code, 302, 'right status'; 92 | like $t->tx->previous->res->headers->location, qr/locks/, 'right "Location" value'; 93 | }; 94 | 95 | subtest 'Manage jobs' => sub { 96 | is app->minion->job($finished)->info->{state}, 'finished', 'right state'; 97 | $t->post_ok('/minion/jobs?_method=PATCH' => form => {id => $finished, do => 'retry'}) 98 | ->text_like('.alert-success', qr/All selected jobs retried/); 99 | is $t->tx->previous->res->code, 302, 'right status'; 100 | like $t->tx->previous->res->headers->location, qr/id=$finished/, 'right "Location" value'; 101 | is app->minion->job($finished)->info->{state}, 'inactive', 'right state'; 102 | $t->post_ok('/minion/jobs?_method=PATCH' => form => {id => $finished, do => 'stop'}) 103 | ->text_like('.alert-info', qr/Trying to stop all selected jobs/); 104 | is $t->tx->previous->res->code, 302, 'right status'; 105 | like $t->tx->previous->res->headers->location, qr/id=$finished/, 'right "Location" value'; 106 | 107 | my $subscribers = $t->app->log->subscribers('message'); 108 | my $level = $t->app->log->level; 109 | $t->app->log->unsubscribe('message'); 110 | my $log = ''; 111 | my $cb = $t->app->log->level('debug')->on(message => sub { $log .= pop }); 112 | $t->post_ok('/minion/jobs?_method=PATCH' => form => {id => $finished, do => 'remove'}) 113 | ->text_like('.alert-success', qr/All selected jobs removed/); 114 | $t->app->log->level($level)->unsubscribe(message => $cb); 115 | $t->app->log->on(message => $_) for @$subscribers; 116 | like $log, qr/Jobs removed by user ".+": $finished/, 'right log message'; 117 | is $t->tx->previous->res->code, 302, 'right status'; 118 | like $t->tx->previous->res->headers->location, qr/id=$finished/, 'right "Location" value'; 119 | is app->minion->job($finished), undef, 'job has been removed'; 120 | }; 121 | 122 | subtest 'Bundled static files' => sub { 123 | $t->get_ok('/minion/bootstrap/bootstrap.js')->status_is(200)->content_type_is('application/javascript'); 124 | $t->get_ok('/minion/bootstrap/bootstrap.css')->status_is(200)->content_type_is('text/css'); 125 | $t->get_ok('/minion/d3/d3.js')->status_is(200)->content_type_is('application/javascript'); 126 | $t->get_ok('/minion/epoch/epoch.js')->status_is(200)->content_type_is('application/javascript'); 127 | $t->get_ok('/minion/epoch/epoch.css')->status_is(200)->content_type_is('text/css'); 128 | $t->get_ok('/minion/fontawesome/fontawesome.css')->status_is(200)->content_type_is('text/css'); 129 | $t->get_ok('/minion/webfonts/fa-brands-400.eot')->status_is(200); 130 | $t->get_ok('/minion/webfonts/fa-brands-400.svg')->status_is(200); 131 | $t->get_ok('/minion/webfonts/fa-brands-400.ttf')->status_is(200); 132 | $t->get_ok('/minion/webfonts/fa-brands-400.woff')->status_is(200); 133 | $t->get_ok('/minion/webfonts/fa-brands-400.woff2')->status_is(200); 134 | $t->get_ok('/minion/webfonts/fa-regular-400.eot')->status_is(200); 135 | $t->get_ok('/minion/webfonts/fa-regular-400.ttf')->status_is(200); 136 | $t->get_ok('/minion/webfonts/fa-regular-400.svg')->status_is(200); 137 | $t->get_ok('/minion/webfonts/fa-regular-400.woff')->status_is(200); 138 | $t->get_ok('/minion/webfonts/fa-regular-400.woff2')->status_is(200); 139 | $t->get_ok('/minion/webfonts/fa-solid-900.eot')->status_is(200); 140 | $t->get_ok('/minion/webfonts/fa-solid-900.ttf')->status_is(200); 141 | $t->get_ok('/minion/webfonts/fa-solid-900.svg')->status_is(200); 142 | $t->get_ok('/minion/webfonts/fa-solid-900.woff')->status_is(200); 143 | $t->get_ok('/minion/webfonts/fa-solid-900.woff2')->status_is(200); 144 | $t->get_ok('/minion/moment/moment.js')->status_is(200)->content_type_is('application/javascript'); 145 | $t->get_ok('/minion/app.js')->status_is(200)->content_type_is('application/javascript'); 146 | $t->get_ok('/minion/app.css')->status_is(200)->content_type_is('text/css'); 147 | $t->get_ok('/minion/logo-black-2x.png')->status_is(200)->content_type_is('image/png'); 148 | $t->get_ok('/minion/logo-black.png')->status_is(200)->content_type_is('image/png'); 149 | }; 150 | 151 | subtest 'Different prefix and return route' => sub { 152 | plugin 'Minion::Admin' => {route => app->routes->any('/also_minion'), return_to => 'test_home'}; 153 | $t->get_ok('/also_minion')->status_is(200)->content_like(qr/Dashboard/)->element_exists('a[href=/home]'); 154 | }; 155 | 156 | # Clean up once we are done 157 | $pg->db->query('DROP SCHEMA minion_admin_test CASCADE'); 158 | 159 | done_testing(); 160 | -------------------------------------------------------------------------------- /t/pg_lite_app.t: -------------------------------------------------------------------------------- 1 | use Mojo::Base -strict; 2 | 3 | BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' } 4 | 5 | use Test::More; 6 | 7 | plan skip_all => 'set TEST_ONLINE to enable this test' unless $ENV{TEST_ONLINE}; 8 | 9 | use Mojo::IOLoop; 10 | use Mojo::Promise; 11 | use Mojolicious::Lite; 12 | use Test::Mojo; 13 | 14 | # Missing backend 15 | eval { plugin Minion => {Something => 'fun'} }; 16 | like $@, qr/^Backend "Minion::Backend::Something" missing/, 'right error'; 17 | 18 | # Isolate tests 19 | require Mojo::Pg; 20 | my $pg = Mojo::Pg->new($ENV{TEST_ONLINE}); 21 | $pg->db->query('DROP SCHEMA IF EXISTS minion_app_test CASCADE'); 22 | $pg->db->query('CREATE SCHEMA minion_app_test'); 23 | plugin Minion => {Pg => $pg->search_path(['minion_app_test'])}; 24 | 25 | app->minion->add_task( 26 | add => sub { 27 | my ($job, $first, $second) = @_; 28 | Mojo::IOLoop->next_tick(sub { 29 | $job->finish($first + $second); 30 | Mojo::IOLoop->stop; 31 | }); 32 | Mojo::IOLoop->start; 33 | } 34 | ); 35 | 36 | get '/add' => sub { 37 | my $c = shift; 38 | my $id = $c->minion->enqueue(add => [$c->param('first'), $c->param('second')] => {queue => 'test'}); 39 | $c->render(text => $id); 40 | }; 41 | 42 | get '/result' => sub { 43 | my $c = shift; 44 | $c->render(text => $c->minion->job($c->param('id'))->info->{result}); 45 | }; 46 | 47 | my $t = Test::Mojo->new; 48 | 49 | subtest 'Perform jobs automatically' => sub { 50 | $t->get_ok('/add' => form => {first => 1, second => 2})->status_is(200); 51 | $t->app->minion->perform_jobs_in_foreground({queues => ['test']}); 52 | $t->get_ok('/result' => form => {id => $t->tx->res->text})->status_is(200)->content_is('3'); 53 | $t->get_ok('/add' => form => {first => 2, second => 3})->status_is(200); 54 | my $first = $t->tx->res->text; 55 | $t->get_ok('/add' => form => {first => 4, second => 5})->status_is(200); 56 | my $second = $t->tx->res->text; 57 | Mojo::Promise->new->resolve->then(sub { $t->app->minion->perform_jobs({queues => ['test']}) })->wait; 58 | $t->get_ok('/result' => form => {id => $first})->status_is(200)->content_is('5'); 59 | $t->get_ok('/result' => form => {id => $second})->status_is(200)->content_is('9'); 60 | }; 61 | 62 | # Clean up once we are done 63 | $pg->db->query('DROP SCHEMA minion_app_test CASCADE'); 64 | 65 | done_testing(); 66 | -------------------------------------------------------------------------------- /t/pg_worker.t: -------------------------------------------------------------------------------- 1 | use Mojo::Base -strict; 2 | 3 | BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' } 4 | 5 | use Test::More; 6 | 7 | plan skip_all => 'set TEST_ONLINE to enable this test' unless $ENV{TEST_ONLINE}; 8 | 9 | use Minion; 10 | use Mojo::IOLoop; 11 | 12 | # Isolate tests 13 | require Mojo::Pg; 14 | my $pg = Mojo::Pg->new($ENV{TEST_ONLINE}); 15 | $pg->db->query('DROP SCHEMA IF EXISTS minion_worker_test CASCADE'); 16 | $pg->db->query('CREATE SCHEMA minion_worker_test'); 17 | my $minion = Minion->new(Pg => $ENV{TEST_ONLINE}); 18 | $minion->backend->pg->search_path(['minion_worker_test']); 19 | 20 | subtest 'Basics' => sub { 21 | $minion->add_task( 22 | test => sub { 23 | my $job = shift; 24 | $job->finish({just => 'works!'}); 25 | } 26 | ); 27 | my $worker = $minion->worker; 28 | $worker->status->{dequeue_timeout} = 0; 29 | $worker->on( 30 | dequeue => sub { 31 | my ($worker, $job) = @_; 32 | $job->on(reap => sub { kill 'INT', $$ }); 33 | } 34 | ); 35 | my $id = $minion->enqueue('test'); 36 | my $max; 37 | $worker->once(wait => sub { $max = shift->status->{jobs} }); 38 | $worker->run; 39 | is $max, 4, 'right value'; 40 | is_deeply $minion->job($id)->info->{result}, {just => 'works!'}, 'right result'; 41 | }; 42 | 43 | subtest 'Clean up event loop' => sub { 44 | my $timer = 0; 45 | Mojo::IOLoop->recurring(0 => sub { $timer++ }); 46 | my $worker = $minion->worker; 47 | $worker->status->{dequeue_timeout} = 0; 48 | $worker->on( 49 | dequeue => sub { 50 | my ($worker, $job) = @_; 51 | $job->on(reap => sub { kill 'INT', $$ }); 52 | } 53 | ); 54 | my $id = $minion->enqueue('test'); 55 | $worker->run; 56 | is_deeply $minion->job($id)->info->{result}, {just => 'works!'}, 'right result'; 57 | is $timer, 0, 'timer has been cleaned up'; 58 | }; 59 | 60 | subtest 'Signals' => sub { 61 | $minion->add_task( 62 | int => sub { 63 | my $job = shift; 64 | my $forever = 1; 65 | my %received; 66 | local $SIG{INT} = sub { $forever = 0 }; 67 | local $SIG{USR1} = sub { $received{usr1}++ }; 68 | local $SIG{USR2} = sub { $received{usr2}++ }; 69 | $job->minion->broadcast('kill', [$_, $job->id]) for qw(USR1 USR2 INT); 70 | while ($forever) { sleep 1 } 71 | $job->finish({msg => 'signals: ' . join(' ', sort keys %received)}); 72 | } 73 | ); 74 | my $worker = $minion->worker; 75 | $worker->status->{command_interval} = 1; 76 | $worker->on( 77 | dequeue => sub { 78 | my ($worker, $job) = @_; 79 | $job->on(reap => sub { kill 'INT', $$ }); 80 | } 81 | ); 82 | my $id = $minion->enqueue('int'); 83 | $worker->run; 84 | is_deeply $minion->job($id)->info->{result}, {msg => 'signals: usr1 usr2'}, 'right result'; 85 | 86 | my $status = $worker->status; 87 | is $status->{command_interval}, 1, 'right value'; 88 | is $status->{dequeue_timeout}, 5, 'right value'; 89 | is $status->{heartbeat_interval}, 300, 'right value'; 90 | is $status->{jobs}, 4, 'right value'; 91 | is_deeply $status->{queues}, ['default'], 'right structure'; 92 | is $status->{performed}, 1, 'right value'; 93 | ok $status->{repair_interval}, 'has a value'; 94 | is $status->{spare}, 1, 'right value'; 95 | is $status->{spare_min_priority}, 1, 'right value'; 96 | is $status->{type}, 'Perl', 'right value'; 97 | }; 98 | 99 | # Clean up once we are done 100 | $pg->db->query('DROP SCHEMA minion_worker_test CASCADE'); 101 | 102 | done_testing(); 103 | -------------------------------------------------------------------------------- /t/pod.t: -------------------------------------------------------------------------------- 1 | use Mojo::Base -strict; 2 | 3 | use Test::More; 4 | 5 | plan skip_all => 'set TEST_POD to enable this test (developer only!)' unless $ENV{TEST_POD}; 6 | plan skip_all => 'Test::Pod 1.14+ required for this test!' unless eval 'use Test::Pod 1.14; 1'; 7 | 8 | all_pod_files_ok(); 9 | -------------------------------------------------------------------------------- /t/pod_coverage.t: -------------------------------------------------------------------------------- 1 | use Mojo::Base -strict; 2 | 3 | use Test::More; 4 | 5 | plan skip_all => 'set TEST_POD to enable this test (developer only!)' unless $ENV{TEST_POD}; 6 | plan skip_all => 'Test::Pod::Coverage 1.04+ required for this test!' unless eval 'use Test::Pod::Coverage 1.04; 1'; 7 | 8 | all_pod_coverage_ok(); 9 | --------------------------------------------------------------------------------