Minion-Backend-SQLite-v5.0.6000755001750001750 014143604471 15632 5ustar00grinnzgrinnz000000000000README100644001750001750 3621214143604471 16617 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6NAME Minion::Backend::SQLite - SQLite backend for Minion job queue SYNOPSIS use Minion::Backend::SQLite; my $backend = Minion::Backend::SQLite->new('sqlite:test.db'); # Minion use Minion; my $minion = Minion->new(SQLite => 'sqlite:test.db'); # Mojolicious (via Mojolicious::Plugin::Minion) $self->plugin(Minion => { SQLite => 'sqlite:test.db' }); # Mojolicious::Lite (via Mojolicious::Plugin::Minion) plugin Minion => { SQLite => 'sqlite:test.db' }; # Share the database connection cache helper sqlite => sub { state $sqlite = Mojo::SQLite->new('sqlite:test.db') }; plugin Minion => { SQLite => app->sqlite }; DESCRIPTION Minion::Backend::SQLite is a backend for Minion based on Mojo::SQLite. All necessary tables will be created automatically with a set of migrations named minion. If no connection string or :temp: is provided, the database will be created in a temporary directory. ATTRIBUTES Minion::Backend::SQLite inherits all attributes from Minion::Backend and implements the following new ones. dequeue_interval my $seconds = $backend->dequeue_interval; $backend = $backend->dequeue_interval($seconds); Interval in seconds between "dequeue" attempts. Defaults to 0.5. sqlite my $sqlite = $backend->sqlite; $backend = $backend->sqlite(Mojo::SQLite->new); Mojo::SQLite object used to store all data. METHODS Minion::Backend::SQLite inherits all methods from Minion::Backend and implements the following new ones. new my $backend = Minion::Backend::SQLite->new; my $backend = Minion::Backend::SQLite->new(':temp:'); my $backend = Minion::Backend::SQLite->new('sqlite:test.db'); my $backend = Minion::Backend::SQLite->new->tap(sub { $_->sqlite->from_filename('C:\\foo\\bar.db') }); my $backend = Minion::Backend::SQLite->new(Mojo::SQLite->new); Construct a new Minion::Backend::SQLite object. broadcast my $bool = $backend->broadcast('some_command'); my $bool = $backend->broadcast('some_command', [@args]); my $bool = $backend->broadcast('some_command', [@args], [$id1, $id2, $id3]); Broadcast remote control command to one or more workers. dequeue my $job_info = $backend->dequeue($worker_id, 0.5); my $job_info = $backend->dequeue($worker_id, 0.5, {queues => ['important']}); Wait a given amount of time in seconds for a job, dequeue it and transition from inactive to active state, or return undef if queues were empty. Jobs will be checked for in intervals defined by "dequeue_interval" until the timeout is reached. These options are currently available: id id => '10023' Dequeue a specific job. min_priority min_priority => 3 Do not dequeue jobs with a lower priority. queues queues => ['important'] One or more queues to dequeue jobs from, defaults to default. These fields are currently available: args args => ['foo', 'bar'] Job arguments. id id => '10023' Job ID. retries retries => 3 Number of times job has been retried. task task => 'foo' Task name. enqueue my $job_id = $backend->enqueue('foo'); my $job_id = $backend->enqueue(foo => [@args]); my $job_id = $backend->enqueue(foo => [@args] => {priority => 1}); Enqueue a new job with inactive state. These options are currently available: attempts attempts => 25 Number of times performing this job will be attempted, with a delay based on "backoff" in Minion after the first attempt, defaults to 1. delay delay => 10 Delay job for this many seconds (from now). expire expire => 300 Job is valid for this many seconds (from now) before it expires. Note that this option is EXPERIMENTAL and might change without warning! lax lax => 1 Existing jobs this job depends on may also have transitioned to the failed state to allow for it to be processed, defaults to false. Note that this option is EXPERIMENTAL and might change without warning! notes notes => {foo => 'bar', baz => [1, 2, 3]} Hash reference with arbitrary metadata for this job. parents parents => [$id1, $id2, $id3] One or more existing jobs this job depends on, and that need to have transitioned to the state finished before it can be processed. priority priority => 5 Job priority, defaults to 0. Jobs with a higher priority get performed first. queue queue => 'important' Queue to put job in, defaults to default. fail_job my $bool = $backend->fail_job($job_id, $retries); my $bool = $backend->fail_job($job_id, $retries, 'Something went wrong!'); my $bool = $backend->fail_job( $job_id, $retries, {msg => 'Something went wrong!'}); Transition from active to failed state with or without a result, and if there are attempts remaining, transition back to inactive with an exponentially increasing delay based on "backoff" in Minion. finish_job my $bool = $backend->finish_job($job_id, $retries); my $bool = $backend->finish_job($job_id, $retries, 'All went well!'); my $bool = $backend->finish_job($job_id, $retries, {msg => 'All went well!'}); Transition from active to finished state with or without a result. history my $history = $backend->history; Get history information for job queue. These fields are currently available: daily daily => [{epoch => 12345, finished_jobs => 95, failed_jobs => 2}, ...] Hourly counts for processed jobs from the past day. list_jobs my $results = $backend->list_jobs($offset, $limit); my $results = $backend->list_jobs($offset, $limit, {states => ['inactive']}); Returns the information about jobs in batches. # Get the total number of results (without limit) my $num = $backend->list_jobs(0, 100, {queues => ['important']})->{total}; # Check job state my $results = $backend->list_jobs(0, 1, {ids => [$job_id]}); my $state = $results->{jobs}[0]{state}; # Get job result my $results = $backend->list_jobs(0, 1, {ids => [$job_id]}); my $result = $results->{jobs}[0]{result}; These options are currently available: before before => 23 List only jobs before this id. ids ids => ['23', '24'] List only jobs with these ids. queues queues => ['important', 'unimportant'] List only jobs in these queues. states states => ['inactive', 'active'] List only jobs in these states. tasks tasks => ['foo', 'bar'] List only jobs for these tasks. These fields are currently available: args args => ['foo', 'bar'] Job arguments. attempts attempts => 25 Number of times performing this job will be attempted. children children => ['10026', '10027', '10028'] Jobs depending on this job. created created => 784111777 Epoch time job was created. delayed delayed => 784111777 Epoch time job was delayed to. expires expires => 784111777 Epoch time job is valid until before it expires. finished finished => 784111777 Epoch time job was finished. id id => 10025 Job id. lax lax => 0 Existing jobs this job depends on may also have failed to allow for it to be processed. notes notes => {foo => 'bar', baz => [1, 2, 3]} Hash reference with arbitrary metadata for this job. parents parents => ['10023', '10024', '10025'] Jobs this job depends on. priority priority => 3 Job priority. queue queue => 'important' Queue name. result result => 'All went well!' Job result. retried retried => 784111777 Epoch time job has been retried. retries retries => 3 Number of times job has been retried. started started => 784111777 Epoch time job was started. state state => 'inactive' Current job state, usually active, failed, finished or inactive. task task => 'foo' Task name. time time => 78411177 Current time. worker worker => '154' Id of worker that is processing the job. list_locks my $results = $backend->list_locks($offset, $limit); my $results = $backend->list_locks($offset, $limit, {names => ['foo']}); Returns information about locks in batches. # Get the total number of results (without limit) my $num = $backend->list_locks(0, 100, {names => ['bar']})->{total}; # Check expiration time my $results = $backend->list_locks(0, 1, {names => ['foo']}); my $expires = $results->{locks}[0]{expires}; These options are currently available: names names => ['foo', 'bar'] List only locks with these names. These fields are currently available: expires expires => 784111777 Epoch time this lock will expire. name name => 'foo' Lock name. list_workers my $results = $backend->list_workers($offset, $limit); my $results = $backend->list_workers($offset, $limit, {ids => [23]}); Returns information about workers in batches. # Get the total number of results (without limit) my $num = $backend->list_workers(0, 100)->{total}; # Check worker host my $results = $backend->list_workers(0, 1, {ids => [$worker_id]}); my $host = $results->{workers}[0]{host}; These options are currently available: before before => 23 List only workers before this id. ids ids => ['23', '24'] List only workers with these ids. These fields are currently available: id id => 22 Worker id. host host => 'localhost' Worker host. jobs jobs => ['10023', '10024', '10025', '10029'] Ids of jobs the worker is currently processing. notified notified => 784111777 Epoch time worker sent the last heartbeat. pid pid => 12345 Process id of worker. started started => 784111777 Epoch time worker was started. status status => {queues => ['default', 'important']} Hash reference with whatever status information the worker would like to share. lock my $bool = $backend->lock('foo', 3600); my $bool = $backend->lock('foo', 3600, {limit => 20}); Try to acquire a named lock that will expire automatically after the given amount of time in seconds. An expiration time of 0 can be used to check if a named lock already exists without creating one. These options are currently available: limit limit => 20 Number of shared locks with the same name that can be active at the same time, defaults to 1. note my $bool = $backend->note($job_id, {mojo => 'rocks', minion => 'too'}); Change one or more metadata fields for a job. Setting a value to undef will remove the field. It is currently an error to attempt to set a metadata field with a name containing the characters ., [, or ]. receive my $commands = $backend->receive($worker_id); Receive remote control commands for worker. register_worker my $worker_id = $backend->register_worker; my $worker_id = $backend->register_worker($worker_id); my $worker_id = $backend->register_worker( $worker_id, {status => {queues => ['default', 'important']}}); Register worker or send heartbeat to show that this worker is still alive. These options are currently available: status status => {queues => ['default', 'important']} Hash reference with whatever status information the worker would like to share. remove_job my $bool = $backend->remove_job($job_id); Remove failed, finished or inactive job from queue. repair $backend->repair; Repair worker registry and job queue if necessary. reset $backend->reset({all => 1}); Reset job queue. These options are currently available: all all => 1 Reset everything. locks locks => 1 Reset only locks. retry_job my $bool = $backend->retry_job($job_id, $retries); my $bool = $backend->retry_job($job_id, $retries, {delay => 10}); Transition job back to inactive state, already inactive jobs may also be retried to change options. These options are currently available: attempts attempts => 25 Number of times performing this job will be attempted. delay delay => 10 Delay job for this many seconds (from now). expire expire => 300 Job is valid for this many seconds (from now) before it expires. Note that this option is EXPERIMENTAL and might change without warning! lax lax => 1 Existing jobs this job depends on may also have transitioned to the failed state to allow for it to be processed, defaults to false. Note that this option is EXPERIMENTAL and might change without warning! parents parents => [$id1, $id2, $id3] Jobs this job depends on. priority priority => 5 Job priority. queue queue => 'important' Queue to put job in. stats my $stats = $backend->stats; Get statistics for the job queue. These fields are currently available: active_jobs active_jobs => 100 Number of jobs in active state. active_locks active_locks => 100 Number of active named locks. active_workers active_workers => 100 Number of workers that are currently processing a job. delayed_jobs delayed_jobs => 100 Number of jobs in inactive state that are scheduled to run at specific time in the future. enqueued_jobs enqueued_jobs => 100000 Rough estimate of how many jobs have ever been enqueued. failed_jobs failed_jobs => 100 Number of jobs in failed state. finished_jobs finished_jobs => 100 Number of jobs in finished state. inactive_jobs inactive_jobs => 100 Number of jobs in inactive state. inactive_workers inactive_workers => 100 Number of workers that are currently not processing a job. uptime uptime => undef Uptime in seconds. Always undefined for SQLite. unlock my $bool = $backend->unlock('foo'); Release a named lock. unregister_worker $backend->unregister_worker($worker_id); Unregister worker. BUGS Report any issues on the public bugtracker. AUTHOR Dan Book COPYRIGHT AND LICENSE This software is Copyright (c) 2015 by Dan Book. This is free software, licensed under: The Artistic License 2.0 (GPL Compatible) SEE ALSO Minion, Mojo::SQLite Changes100644001750001750 1256214143604471 17234 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6v5.0.6 2021-11-12 19:39:19 EST - Improve efficiency of stats query (#19, Sebastian Riedel) v5.0.5 2021-06-15 21:18:41 EDT - Support min_priority option in dequeue method. - Update IRC metadata to libera.chat v5.0.4 2021-02-16 13:30:11 EST - Use Mojo::Promise in tests instead of the deprecated and decored Mojo::IOLoop::Delay (#17, Stefan Adams) v5.0.3 2020-08-02 18:15:20 EDT - Minion requirement bumped to 10.13. - Removed experimental support for job sequences. - Added EXPERIMENTAL expire option to enqueue method to support expiring jobs. - Added EXPERIMENTAL lax option to enqueue method to support lax dependencies. - Removed next and previous fields from list_jobs method. - Added expires and lax fields to list_jobs method. - Fixed a bug where manual retries would count towards the attempts limit for automatic retries. - Optimized checking parent job state in job dequeue. v5.0.2 2020-07-23 00:30:45 EDT - Minion requirement bumped to 10.10. - Support removing stuck jobs in repair method. - Added EXPERIMENTAL sequence option to enqueue method and sequences option to list_jobs method to support job sequences. - Added next and previous fields to list_jobs method. v5.0.1 2020-06-17 23:20:46 EDT - Fixed tests to be less sensitive to error message contents. v5.0.0 2020-06-17 01:22:45 EDT - Minion requirement bumped to 10.03. - Changed reset method to require options for what to reset and allow for locks to be reset without resetting the whole queue. - Added before options to list_jobs and list_workers methods to support iterating jobs and workers. 4.005 2019-08-05 11:00:01 EDT - Allow fields to be removed with note method. 4.004 2019-07-09 00:53:20 EDT - Add time field to list_jobs method. 4.003 2019-06-21 18:06:07 EDT - Ignore missing workers for jobs in the minion_foreground named queue to make debugging very slow jobs easier. 4.002 2018-10-03 13:58:15 EDT - Optimize repair query (yrjustice, #15, #16) 4.001 2018-04-21 19:29:24 EDT - Implement EXPERIMENTAL history method used for Minion Admin plugin history graph. 4.000 2018-04-16 14:58:35 EDT - Minion requirement bumped to 9.0. - Replace queue, state, and task options of list_jobs method with queues, states, and tasks options. - Replace name option of list_locks method with names option. - Replace key/value arguments of note method with a hash reference. - Add parents option to retry_job method. - Re-add active_locks stats field. 3.003 2017-12-10 16:00:33 EST - Remove active_locks stats field as it was incorrect. - Fix list_locks to exclude already expired locks. 3.002 2017-12-09 21:42:19 EST - Add list_locks method. - Add active_locks field to stats. 3.001 2017-11-28 21:57:15 EST - Fix condition in dequeue that could lead to calling usleep with a negative time. (toratora, #12) 3.000 2017-11-17 20:20:58 EST - Minion requirement bumped to 8.0. - Remove job_info and worker_info methods. - Support ids option and return total from list_jobs and list_workers methods. - Add uptime field to stats method (always undef for SQLite). 2.004 2017-11-11 16:17:27 EST - Add dequeue_interval attribute and check for jobs in 0.5 second intervals by default. (#10) 2.003 2017-08-07 16:04:18 EDT - Fix tests for rethrown job exceptions in Minion 7.05. 2.002 2017-08-05 12:01:08 EDT - Add id option to dequeue method to support dequeueing a specific job. - Add attempts option to retry_job method. 2.001 2017-07-20 02:40:46 EDT - Bump Mojo::SQLite requirement to 3.000 to support sharing the database connection cache with existing Mojo::SQLite objects. 2.000 2017-06-26 00:42:49 EDT - Add support for rate limiting and unique jobs with lock and unlock methods. - Add support for job metadata with note method, notes option for enqueue method, and notes field in job_info method. 1.000 2017-04-14 14:54:28 EDT - Support retrying active jobs in retry_job. - Support sharing worker status information in register_worker and worker_info. 0.009 2016-12-19 20:34:58 EST - Increase dependency on Mojo::SQLite for memory leak fix 0.008 2016-12-16 22:36:53 EST - Correct ordering of dequeued jobs that are created in the same second. 0.007 2016-09-19 20:30:32 EDT - Add support for worker remote control commands with broadcast and receive methods. - Fix tests for compatibility with Minion 6.0. 0.006 2016-09-06 23:22:20 EDT - Add support for EXPERIMENTAL enqueued_jobs field in stats method. 0.005 2016-07-02 20:46:32 EDT - Add support for EXPERIMENTAL delayed_jobs field in stats method. - Add queue option to list_jobs method. - Add support for job dependencies. - Add parents option to enqueue method. - Add children and parents fields to job_info method. 0.004 2016-03-16 21:52:15 EDT - Bump Mojo::SQLite dependency to 0.020 for JSON1 support - Use JSON1 fields for job args and result - Use new Mojo::SQLite auto_migrate feature - Various optimizations from Minion::Backend::Pg including much faster dequeue 0.003 2015-11-13 20:19:45 EST - Updated to support Minion 4.0 - Allow retry methods to change options for already inactive jobs 0.002 2015-10-30 17:35:29 EDT - Added support for retrying failed jobs automatically in Minion 3.01 0.001 2015-10-28 21:59:55 EDT - First release LICENSE100644001750001750 2151514143604471 16744 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6This software is Copyright (c) 2015 by Dan Book. This is free software, licensed under: The Artistic License 2.0 (GPL Compatible) The Artistic License 2.0 Copyright (c) 2000-2006, The Perl Foundation. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble This license establishes the terms under which a given free software Package may be copied, modified, distributed, and/or redistributed. The intent is that the Copyright Holder maintains some artistic control over the development of that Package while still keeping the Package available as open source and free software. You are always permitted to make arrangements wholly outside of this license directly with the Copyright Holder of a given Package. If the terms of this license do not permit the full use that you propose to make of the Package, you should contact the Copyright Holder and seek a different licensing arrangement. Definitions "Copyright Holder" means the individual(s) or organization(s) named in the copyright notice for the entire Package. "Contributor" means any party that has contributed code or other material to the Package, in accordance with the Copyright Holder's procedures. "You" and "your" means any person who would like to copy, distribute, or modify the Package. "Package" means the collection of files distributed by the Copyright Holder, and derivatives of that collection and/or of those files. A given Package may consist of either the Standard Version, or a Modified Version. "Distribute" means providing a copy of the Package or making it accessible to anyone else, or in the case of a company or organization, to others outside of your company or organization. "Distributor Fee" means any fee that you charge for Distributing this Package or providing support for this Package to another party. It does not mean licensing fees. "Standard Version" refers to the Package if it has not been modified, or has been modified only in ways explicitly requested by the Copyright Holder. "Modified Version" means the Package, if it has been changed, and such changes were not explicitly requested by the Copyright Holder. "Original License" means this Artistic License as Distributed with the Standard Version of the Package, in its current version or as it may be modified by The Perl Foundation in the future. "Source" form means the source code, documentation source, and configuration files for the Package. "Compiled" form means the compiled bytecode, object code, binary, or any other form resulting from mechanical transformation or translation of the Source form. Permission for Use and Modification Without Distribution (1) You are permitted to use the Standard Version and create and use Modified Versions for any purpose without restriction, provided that you do not Distribute the Modified Version. Permissions for Redistribution of the Standard Version (2) You may Distribute verbatim copies of the Source form of the Standard Version of this Package in any medium without restriction, either gratis or for a Distributor Fee, provided that you duplicate all of the original copyright notices and associated disclaimers. At your discretion, such verbatim copies may or may not include a Compiled form of the Package. (3) You may apply any bug fixes, portability changes, and other modifications made available from the Copyright Holder. The resulting Package will still be considered the Standard Version, and as such will be subject to the Original License. Distribution of Modified Versions of the Package as Source (4) You may Distribute your Modified Version as Source (either gratis or for a Distributor Fee, and with or without a Compiled form of the Modified Version) provided that you clearly document how it differs from the Standard Version, including, but not limited to, documenting any non-standard features, executables, or modules, and provided that you do at least ONE of the following: (a) make the Modified Version available to the Copyright Holder of the Standard Version, under the Original License, so that the Copyright Holder may include your modifications in the Standard Version. (b) ensure that installation of your Modified Version does not prevent the user installing or running the Standard Version. In addition, the Modified Version must bear a name that is different from the name of the Standard Version. (c) allow anyone who receives a copy of the Modified Version to make the Source form of the Modified Version available to others under (i) the Original License or (ii) a license that permits the licensee to freely copy, modify and redistribute the Modified Version using the same licensing terms that apply to the copy that the licensee received, and requires that the Source form of the Modified Version, and of any works derived from it, be made freely available in that license fees are prohibited but Distributor Fees are allowed. Distribution of Compiled Forms of the Standard Version or Modified Versions without the Source (5) You may Distribute Compiled forms of the Standard Version without the Source, provided that you include complete instructions on how to get the Source of the Standard Version. Such instructions must be valid at the time of your distribution. If these instructions, at any time while you are carrying out such distribution, become invalid, you must provide new instructions on demand or cease further distribution. If you provide valid instructions or cease distribution within thirty days after you become aware that the instructions are invalid, then you do not forfeit any of your rights under this license. (6) You may Distribute a Modified Version in Compiled form without the Source, provided that you comply with Section 4 with respect to the Source of the Modified Version. Aggregating or Linking the Package (7) You may aggregate the Package (either the Standard Version or Modified Version) with other packages and Distribute the resulting aggregation provided that you do not charge a licensing fee for the Package. Distributor Fees are permitted, and licensing fees for other components in the aggregation are permitted. The terms of this license apply to the use and Distribution of the Standard or Modified Versions as included in the aggregation. (8) You are permitted to link Modified and Standard Versions with other works, to embed the Package in a larger work of your own, or to build stand-alone binary or bytecode versions of applications that include the Package, and Distribute the result without restriction, provided the result does not expose a direct interface to the Package. Items That are Not Considered Part of a Modified Version (9) Works (including, but not limited to, modules and scripts) that merely extend or make use of the Package, do not, by themselves, cause the Package to be a Modified Version. In addition, such works are not considered parts of the Package itself, and are not subject to the terms of this license. General Provisions (10) Any use, modification, and distribution of the Standard or Modified Versions is governed by this Artistic License. By using, modifying or distributing the Package, you accept this license. Do not use, modify, or distribute the Package, if you do not accept this license. (11) If your Modified Version has been derived from a Modified Version made by someone other than you, you are nevertheless required to ensure that your Modified Version complies with the requirements of this license. (12) This license does not grant you the right to use any trademark, service mark, tradename, or logo of the Copyright Holder. (13) This license includes the non-exclusive, worldwide, free-of-charge patent license to make, have made, use, offer to sell, sell, import and otherwise transfer the Package with respect to any patent claims licensable by the Copyright Holder that are necessarily infringed by the Package. If you institute patent litigation (including a cross-claim or counterclaim) against any party alleging that the Package constitutes direct or contributory patent infringement, then this Artistic License to you shall terminate on the date that such litigation is filed. (14) Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. INSTALL100644001750001750 473514143604471 16755 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6This is the Perl distribution Minion-Backend-SQLite. Installing Minion-Backend-SQLite is straightforward. ## Installation with cpanm If you have cpanm, you only need one line: % cpanm Minion::Backend::SQLite If it does not have permission to install modules to the current perl, cpanm will automatically set up and install to a local::lib in your home directory. See the local::lib documentation (https://metacpan.org/pod/local::lib) for details on enabling it in your environment. ## Installing with the CPAN shell Alternatively, if your CPAN shell is set up, you should just be able to do: % cpan Minion::Backend::SQLite ## Manual installation As a last resort, you can manually install it. If you have not already downloaded the release tarball, you can find the download link on the module's MetaCPAN page: https://metacpan.org/pod/Minion::Backend::SQLite Untar the tarball, install configure prerequisites (see below), then build it: % perl Build.PL % ./Build && ./Build test Then install it: % ./Build install Or the more portable variation: % perl Build.PL % perl Build % perl Build test % perl Build install If your perl is system-managed, you can create a local::lib in your home directory to install modules to. For details, see the local::lib documentation: https://metacpan.org/pod/local::lib The prerequisites of this distribution will also have to be installed manually. The prerequisites are listed in one of the files: `MYMETA.yml` or `MYMETA.json` generated by running the manual build process described above. ## Configure Prerequisites This distribution requires other modules to be installed before this distribution's installer can be run. They can be found under the "configure_requires" key of META.yml or the "{prereqs}{configure}{requires}" key of META.json. ## Other Prerequisites This distribution may require additional modules to be installed after running Build.PL. Look for prerequisites in the following phases: * to run ./Build, PHASE = build * to use the module code itself, PHASE = runtime * to run tests, PHASE = test They can all be found in the "PHASE_requires" key of MYMETA.yml or the "{prereqs}{PHASE}{requires}" key of MYMETA.json. ## Documentation Minion-Backend-SQLite documentation is available as POD. You can run `perldoc` from a shell to read the documentation: % perldoc Minion::Backend::SQLite For more information on installing Perl modules via CPAN, please see: https://www.cpan.org/modules/INSTALL.html dist.ini100644001750001750 41214143604471 17334 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6name = Minion-Backend-SQLite author = Dan Book license = Artistic_2_0 copyright_holder = Dan Book copyright_year = 2015 [@Author::DBOOK] :version = v1.0.3 installer = ModuleBuildTiny::Fallback irc = ircs://irc.libera.chat/#mojo pod_tests = 1 META.yml100644001750001750 251214143604471 17164 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6--- abstract: 'SQLite backend for Minion job queue' author: - 'Dan Book ' build_requires: File::Spec: '0' Module::Metadata: '0' Test::More: '0.96' configure_requires: Module::Build::Tiny: '0.034' dynamic_config: 0 generated_by: 'Dist::Zilla version 6.022, CPAN::Meta::Converter version 2.150010' license: artistic_2 meta-spec: url: http://module-build.sourceforge.net/META-spec-v1.4.html version: '1.4' name: Minion-Backend-SQLite no_index: directory: - eg - examples - inc - share - t - xt provides: Minion::Backend::SQLite: file: lib/Minion/Backend/SQLite.pm version: v5.0.6 requires: List::Util: '0' Minion: '10.13' Mojo::SQLite: '3.000' Mojolicious: '7.49' Sys::Hostname: '0' Time::HiRes: '0' perl: '5.010001' resources: IRC: ircs://irc.libera.chat/#mojo bugtracker: https://github.com/Grinnz/Minion-Backend-SQLite/issues homepage: https://github.com/Grinnz/Minion-Backend-SQLite repository: https://github.com/Grinnz/Minion-Backend-SQLite.git version: v5.0.6 x_contributors: - 'Dan Book ' - 'Sebastian Riedel ' - 'Stefan Adams ' - 'yrjustice <43676784+yrjustice@users.noreply.github.com>' x_generated_by_perl: v5.34.0 x_serialization_backend: 'YAML::Tiny version 1.73' x_spdx_expression: Artistic-2.0 MANIFEST100644001750001750 63714143604471 17032 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6# This file was automatically generated by Dist::Zilla::Plugin::Manifest v6.022. Build.PL CONTRIBUTING.md Changes INSTALL LICENSE MANIFEST META.json META.yml README dist.ini examples/minion_bench.pl lib/Minion/Backend/SQLite.pm prereqs.yml t/00-report-prereqs.dd t/00-report-prereqs.t t/sqlite.t t/sqlite_worker.t xt/author/pod-coverage.t xt/author/pod-syntax.t xt/author/sqlite_admin.t xt/author/sqlite_lite_app.t Build.PL100644001750001750 671614143604471 17221 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6# This Build.PL for Minion-Backend-SQLite was generated by # Dist::Zilla::Plugin::ModuleBuildTiny::Fallback 0.025 use strict; use warnings; my %configure_requires = ( 'Module::Build::Tiny' => '0.034', ); my %errors = map { eval "require $_; $_->VERSION($configure_requires{$_}); 1"; $_ => $@, } keys %configure_requires; if (!grep { $_ } values %errors) { # This section for Minion-Backend-SQLite was generated by Dist::Zilla::Plugin::ModuleBuildTiny 0.015. use strict; use warnings; use 5.010001; # use Module::Build::Tiny 0.034; Module::Build::Tiny::Build_PL(); } else { if (not $ENV{PERL_MB_FALLBACK_SILENCE_WARNING}) { warn <<'EOW' *** WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING *** If you're seeing this warning, your toolchain is really, really old* and you'll almost certainly have problems installing CPAN modules from this century. But never fear, dear user, for we have the technology to fix this! If you're using CPAN.pm to install things, then you can upgrade it using: cpan CPAN If you're using CPANPLUS to install things, then you can upgrade it using: cpanp CPANPLUS If you're using cpanminus, you shouldn't be seeing this message in the first place, so please file an issue on github. This public service announcement was brought to you by the Perl Toolchain Gang, the irc.perl.org #toolchain IRC channel, and the number 42. ---- * Alternatively, you are running this file manually, in which case you need to learn to first fulfill all configure requires prerequisites listed in META.yml or META.json -- or use a cpan client to install this distribution. You can also silence this warning for future installations by setting the PERL_MB_FALLBACK_SILENCE_WARNING environment variable, but please don't do that until you fix your toolchain as described above. Errors from configure prereqs: EOW . do { require Data::Dumper; Data::Dumper->new([ \%errors ])->Indent(2)->Terse(1)->Sortkeys(1)->Dump; }; sleep 10 if -t STDIN && (-t STDOUT || !(-f STDOUT || -c STDOUT)); } # This section was automatically generated by Dist::Zilla::Plugin::ModuleBuild v6.022. use strict; use warnings; require Module::Build; Module::Build->VERSION(0.28); my %module_build_args = ( "configure_requires" => { "Module::Build::Tiny" => "0.034" }, "dist_abstract" => "SQLite backend for Minion job queue", "dist_author" => [ "Dan Book " ], "dist_name" => "Minion-Backend-SQLite", "dist_version" => "v5.0.6", "license" => "artistic_2", "module_name" => "Minion::Backend::SQLite", "recursive_test_files" => 1, "requires" => { "List::Util" => 0, "Minion" => "10.13", "Mojo::SQLite" => "3.000", "Mojolicious" => "7.49", "Sys::Hostname" => 0, "Time::HiRes" => 0, "perl" => "5.010001" }, "test_requires" => { "File::Spec" => 0, "Module::Metadata" => 0, "Test::More" => "0.96" } ); my %fallback_build_requires = ( "File::Spec" => 0, "Module::Metadata" => 0, "Test::More" => "0.96" ); unless ( eval { Module::Build->VERSION(0.4004) } ) { delete $module_build_args{test_requires}; $module_build_args{build_requires} = \%fallback_build_requires; } my $build = Module::Build->new(%module_build_args); $build->create_build_script; } META.json100644001750001750 472214143604471 17341 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6{ "abstract" : "SQLite backend for Minion job queue", "author" : [ "Dan Book " ], "dynamic_config" : 0, "generated_by" : "Dist::Zilla version 6.022, CPAN::Meta::Converter version 2.150010", "license" : [ "artistic_2" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : 2 }, "name" : "Minion-Backend-SQLite", "no_index" : { "directory" : [ "eg", "examples", "inc", "share", "t", "xt" ] }, "prereqs" : { "configure" : { "requires" : { "Module::Build::Tiny" : "0.034" } }, "develop" : { "requires" : { "Pod::Coverage::TrustPod" : "0", "Test::Pod" : "1.41", "Test::Pod::Coverage" : "1.08" } }, "runtime" : { "requires" : { "List::Util" : "0", "Minion" : "10.13", "Mojo::SQLite" : "3.000", "Mojolicious" : "7.49", "Sys::Hostname" : "0", "Time::HiRes" : "0", "perl" : "5.010001" }, "suggests" : { "Mojo::JSON::MaybeXS" : "0" } }, "test" : { "recommends" : { "CPAN::Meta" : "2.120900" }, "requires" : { "File::Spec" : "0", "Module::Metadata" : "0", "Test::More" : "0.96" } } }, "provides" : { "Minion::Backend::SQLite" : { "file" : "lib/Minion/Backend/SQLite.pm", "version" : "v5.0.6" } }, "release_status" : "stable", "resources" : { "bugtracker" : { "web" : "https://github.com/Grinnz/Minion-Backend-SQLite/issues" }, "homepage" : "https://github.com/Grinnz/Minion-Backend-SQLite", "repository" : { "type" : "git", "url" : "https://github.com/Grinnz/Minion-Backend-SQLite.git", "web" : "https://github.com/Grinnz/Minion-Backend-SQLite" }, "x_IRC" : "ircs://irc.libera.chat/#mojo" }, "version" : "v5.0.6", "x_contributors" : [ "Dan Book ", "Sebastian Riedel ", "Stefan Adams ", "yrjustice <43676784+yrjustice@users.noreply.github.com>" ], "x_generated_by_perl" : "v5.34.0", "x_serialization_backend" : "Cpanel::JSON::XS version 4.26", "x_spdx_expression" : "Artistic-2.0" } t000755001750001750 014143604471 16016 5ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6sqlite.t100644001750001750 17332114143604471 17713 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6/tuse Mojo::Base -strict; BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' } use Test::More; use Config; use Minion; use Mojo::IOLoop; use Mojo::Promise; use Sys::Hostname qw(hostname); use Time::HiRes qw(usleep); use constant HAS_PSEUDOFORK => $Config{d_pseudofork}; my $minion = Minion->new('SQLite'); subtest 'Nothing to repair' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; my $worker = $minion->repair->worker; isa_ok $worker->minion->app, 'Mojolicious', 'has default application'; }; subtest 'Migrate up and down' => sub { is $minion->backend->sqlite->migrations->active, 10, 'active version is 10'; is $minion->backend->sqlite->migrations->migrate(0)->active, 0, 'active version is 0'; is $minion->backend->sqlite->migrations->migrate->active, 10, 'active version is 10'; }; subtest 'Register and unregister' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; my $worker = $minion->worker; $worker->register; like $worker->info->{started}, qr/^[\d.]+$/, 'has timestamp'; my $notified = $worker->info->{notified}; like $notified, qr/^[\d.]+$/, 'has timestamp'; my $id = $worker->id; is $worker->register->id, $id, 'same id'; is $worker->unregister->info, undef, 'no information'; my $host = hostname; is $worker->register->info->{host}, $host, 'right host'; is $worker->info->{pid}, $$, 'right pid'; is $worker->unregister->info, undef, 'no information'; }; subtest 'Job results' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; $minion->add_task(test => sub { }); my $worker = $minion->worker->register; my $id = $minion->enqueue('test'); my (@finished, @failed); my $promise = $minion->result_p($id, {interval => 0})->then(sub { @finished = @_ })->catch(sub { @failed = @_ }); ok my $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'same id'; Mojo::IOLoop->one_tick; is_deeply \@finished, [], 'not finished'; is_deeply \@failed, [], 'not failed'; $job->finish({just => 'works!'}); $job->note(foo => 'bar'); $promise->wait; is_deeply $finished[0]{result}, {just => 'works!'}, 'right result'; is_deeply $finished[0]{notes}, {foo => 'bar'}, 'right note'; ok !$finished[1], 'no more results'; is_deeply \@failed, [], 'not failed'; (@finished, @failed) = (); my $id2 = $minion->enqueue('test'); $promise = $minion->result_p($id2, {interval => 0})->then(sub { @finished = @_ })->catch(sub { @failed = @_ }); ok $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id2, 'same id'; $job->fail({works => 'too!'}); $promise->wait; is_deeply \@finished, [], 'not finished'; is_deeply $failed[0]{result}, {works => 'too!'}, 'right result'; ok !$failed[1], 'no more results'; $worker->unregister; (@finished, @failed) = (); $minion->result_p($id)->then(sub { @finished = @_ })->catch(sub { @failed = @_ })->wait; is_deeply $finished[0]{result}, {just => 'works!'}, 'right result'; is_deeply $finished[0]{notes}, {foo => 'bar'}, 'right note'; ok !$finished[1], 'no more results'; is_deeply \@failed, [], 'not failed'; (@finished, @failed) = (); $minion->job($id)->retry; $minion->result_p($id)->timeout(0.25)->then(sub { @finished = @_ })->catch(sub { @failed = @_ })->wait; is_deeply \@finished, [], 'not finished'; is_deeply \@failed, ['Promise timeout'], 'failed'; Mojo::IOLoop->start; (@finished, @failed) = (); $minion->job($id)->remove; $minion->result_p($id)->then(sub { @finished = (@_, 'finished') })->catch(sub { @failed = (@_, 'failed') })->wait; is_deeply \@finished, ['finished'], 'job no longer exists'; is_deeply \@failed, [], 'not failed'; }; subtest 'Repair missing worker' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; $minion->add_task(test => sub { }); my $worker = $minion->worker->register; my $worker2 = $minion->worker->register; isnt $worker2->id, $worker->id, 'new id'; my $id = $minion->enqueue('test'); ok my $job = $worker2->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; is $worker2->info->{jobs}[0], $job->id, 'right id'; $id = $worker2->id; undef $worker2; is $job->info->{state}, 'active', 'job is still active'; ok !!$minion->backend->list_workers(0, 1, {ids => [$id]})->{workers}[0], 'is registered'; $minion->backend->sqlite->db->query( q{update minion_workers set notified = datetime('now', '-' || ? || ' seconds') where id = ?}, $minion->missing_after + 1, $id ); $minion->repair; ok !$minion->backend->list_workers(0, 1, {ids => [$id]})->{workers}[0], 'not registered'; like $job->info->{finished}, qr/^[\d.]+$/, 'has finished timestamp'; is $job->info->{state}, 'failed', 'job is no longer active'; is $job->info->{result}, 'Worker went away', 'right result'; $worker->unregister; }; subtest 'Repair abandoned job' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; my $worker = $minion->worker->register; my $id = $minion->enqueue('test'); ok my $job = $worker->dequeue(1), 'job dequeued'; is $job->id, $id, 'right id'; $worker->unregister; $minion->repair; is $job->info->{state}, 'failed', 'job is no longer active'; is $job->info->{result}, 'Worker went away', 'right result'; }; subtest 'Repair abandoned job in minion_foreground queue (have to be handled manually)' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; my $worker = $minion->worker->register; my $id = $minion->enqueue('test', [], {queue => 'minion_foreground'}); ok my $job = $worker->dequeue(0, {queues => ['minion_foreground']}), 'job dequeued'; is $job->id, $id, 'right id'; $worker->unregister; $minion->repair; is $job->info->{state}, 'active', 'job is still active'; is $job->info->{result}, undef, 'no result'; }; subtest 'Repair old jobs' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; my $worker = $minion->worker->register; my $id = $minion->enqueue('test'); my $id2 = $minion->enqueue('test'); my $id3 = $minion->enqueue('test'); $worker->dequeue(0)->perform for 1 .. 3; my $finished = $minion->backend->sqlite->db->query( q{select strftime('%s',finished) as finished from minion_jobs where id = ?}, $id2 )->hash->{finished}; $minion->backend->sqlite->db->query( q{update minion_jobs set finished = datetime(?,'unixepoch') where id = ?}, $finished - ($minion->remove_after + 1), $id2); $finished = $minion->backend->sqlite->db->query( q{select strftime('%s',finished) as finished from minion_jobs where id = ?}, $id3 )->hash->{finished}; $minion->backend->sqlite->db->query( q{update minion_jobs set finished = datetime(?,'unixepoch') where id = ?}, $finished - ($minion->remove_after + 1), $id3); $worker->unregister; $minion->repair; ok $minion->job($id), 'job has not been cleaned up'; ok !$minion->job($id2), 'job has been cleaned up'; ok !$minion->job($id3), 'job has been cleaned up'; }; subtest 'Repair stuck jobs' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; my $worker = $minion->worker->register; my $id = $minion->enqueue('test'); my $id2 = $minion->enqueue('test'); my $id3 = $minion->enqueue('test'); my $id4 = $minion->enqueue('test'); $minion->backend->sqlite->db->query( q{update minion_jobs set delayed = datetime('now', '-' || ? || ' seconds') where id = ?}, $minion->stuck_after + 1, $_) for $id, $id2, $id3, $id4; ok $worker->dequeue(0, {id => $id4})->finish('Works!'), 'job finished'; ok my $job2 = $worker->dequeue(0, {id => $id2}), 'job dequeued'; $minion->repair; is $job2->info->{state}, 'active', 'job is still active'; ok $job2->finish, 'job finished'; my $job = $minion->job($id); is $job->info->{state}, 'failed', 'job is no longer active'; is $job->info->{result}, 'Job appears stuck in queue', 'right result'; my $job3 = $minion->job($id3); is $job3->info->{state}, 'failed', 'job is no longer active'; is $job3->info->{result}, 'Job appears stuck in queue', 'right result'; my $job4 = $minion->job($id4); is $job4->info->{state}, 'finished', 'job is still finished'; is $job4->info->{result}, 'Works!', 'right result'; $worker->unregister; }; subtest 'List workers' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; my $worker = $minion->worker->register; my $worker2 = $minion->worker->status({whatever => 'works!'})->register; my $results = $minion->backend->list_workers(0, 10); my $host = hostname; is $results->{total}, 2, 'two workers total'; my $batch = $results->{workers}; ok $batch->[0]{id}, 'has id'; is $batch->[0]{host}, $host, 'right host'; is $batch->[0]{pid}, $$, 'right pid'; like $batch->[0]{started}, qr/^[\d.]+$/, 'has timestamp'; is $batch->[1]{host}, $host, 'right host'; is $batch->[1]{pid}, $$, 'right pid'; ok !$batch->[2], 'no more results'; $results = $minion->backend->list_workers(0, 1); $batch = $results->{workers}; is $results->{total}, 2, 'two workers total'; is $batch->[0]{id}, $worker2->id, 'right id'; is_deeply $batch->[0]{status}, {whatever => 'works!'}, 'right status'; ok !$batch->[1], 'no more results'; $worker2->status({whatever => 'works too!'})->register; $batch = $minion->backend->list_workers(0, 1)->{workers}; is_deeply $batch->[0]{status}, {whatever => 'works too!'}, 'right status'; $batch = $minion->backend->list_workers(1, 1)->{workers}; is $batch->[0]{id}, $worker->id, 'right id'; ok !$batch->[1], 'no more results'; $worker->unregister; $worker2->unregister; $minion->reset({all => 1}); $worker = $minion->worker->status({test => 'one'})->register; $worker2 = $minion->worker->status({test => 'two'})->register; my $worker3 = $minion->worker->status({test => 'three'})->register; my $worker4 = $minion->worker->status({test => 'four'})->register; my $worker5 = $minion->worker->status({test => 'five'})->register; my $workers = $minion->workers->fetch(2); is $workers->options->{before}, undef, 'no before'; is $workers->next->{status}{test}, 'five', 'right status'; is $workers->options->{before}, 4, 'before 4'; is $workers->next->{status}{test}, 'four', 'right status'; is $workers->next->{status}{test}, 'three', 'right status'; is $workers->options->{before}, 2, 'before 2'; is $workers->next->{status}{test}, 'two', 'right status'; is $workers->next->{status}{test}, 'one', 'right status'; is $workers->options->{before}, 1, 'before 1'; is $workers->next, undef, 'no more results'; $workers = $minion->workers({ids => [2, 4, 1]}); is $workers->options->{before}, undef, 'no before'; is $workers->next->{status}{test}, 'four', 'right status'; is $workers->options->{before}, 1, 'before 1'; is $workers->next->{status}{test}, 'two', 'right status'; is $workers->next->{status}{test}, 'one', 'right status'; is $workers->next, undef, 'no more results'; $workers = $minion->workers->fetch(2); is $workers->next->{status}{test}, 'five', 'right status'; is $workers->next->{status}{test}, 'four', 'right status'; $worker5->unregister; $worker4->unregister; $worker3->unregister; is $workers->next->{status}{test}, 'two', 'right status'; is $workers->next->{status}{test}, 'one', 'right status'; is $workers->next, undef, 'no more results'; $worker->unregister; $worker2->unregister; }; subtest 'Exclusive lock' => sub { ok $minion->lock('foo', 3600), 'locked'; ok !$minion->lock('foo', 3600), 'not locked again'; ok $minion->unlock('foo'), 'unlocked'; ok !$minion->unlock('foo'), 'not unlocked again'; ok $minion->lock('foo', -3600), 'locked'; ok $minion->lock('foo', 0), 'locked again'; ok !$minion->is_locked('foo'), 'lock does not exist'; ok $minion->lock('foo', 3600), 'locked again'; ok $minion->is_locked('foo'), 'lock exists'; ok !$minion->lock('foo', -3600), 'not locked again'; ok !$minion->lock('foo', 3600), 'not locked again'; ok $minion->unlock('foo'), 'unlocked'; ok !$minion->unlock('foo'), 'not unlocked again'; ok $minion->lock('yada', 3600, {limit => 1}), 'locked'; ok !$minion->lock('yada', 3600, {limit => 1}), 'not locked again'; }; subtest 'Shared lock' => sub { ok $minion->lock('bar', 3600, {limit => 3}), 'locked'; ok $minion->lock('bar', 3600, {limit => 3}), 'locked again'; ok $minion->is_locked('bar'), 'lock exists'; ok $minion->lock('bar', -3600, {limit => 3}), 'locked again'; ok $minion->lock('bar', 3600, {limit => 3}), 'locked again'; ok !$minion->lock('bar', 3600, {limit => 2}), 'not locked again'; ok $minion->lock('baz', 3600, {limit => 3}), 'locked'; ok $minion->unlock('bar'), 'unlocked'; ok $minion->lock('bar', 3600, {limit => 3}), 'locked again'; ok $minion->unlock('bar'), 'unlocked again'; ok $minion->unlock('bar'), 'unlocked again'; ok $minion->unlock('bar'), 'unlocked again'; ok !$minion->unlock('bar'), 'not unlocked again'; ok !$minion->is_locked('bar'), 'lock does not exist'; ok $minion->unlock('baz'), 'unlocked'; ok !$minion->unlock('baz'), 'not unlocked again'; }; subtest 'List locks' => sub { is $minion->stats->{active_locks}, 1, 'one active lock'; my $results = $minion->backend->list_locks(0, 2); is $results->{locks}[0]{name}, 'yada', 'right name'; like $results->{locks}[0]{expires}, qr/^[\d.]+$/, 'expires'; is $results->{locks}[1], undef, 'no more locks'; is $results->{total}, 1, 'one result'; $minion->unlock('yada'); $minion->lock('yada', 3600, {limit => 2}); $minion->lock('test', 3600, {limit => 1}); $minion->lock('yada', 3600, {limit => 2}); is $minion->stats->{active_locks}, 3, 'three active locks'; $results = $minion->backend->list_locks(1, 1); is $results->{locks}[0]{name}, 'test', 'right name'; like $results->{locks}[0]{expires}, qr/^[\d.]+$/, 'expires'; is $results->{locks}[1], undef, 'no more locks'; is $results->{total}, 3, 'three results'; $results = $minion->backend->list_locks(0, 10, {names => ['yada']}); is $results->{locks}[0]{name}, 'yada', 'right name'; like $results->{locks}[0]{expires}, qr/^[\d.]+$/, 'expires'; is $results->{locks}[1]{name}, 'yada', 'right name'; like $results->{locks}[1]{expires}, qr/^[\d.]+$/, 'expires'; is $results->{locks}[2], undef, 'no more locks'; is $results->{total}, 2, 'two results'; $minion->backend->sqlite->db->query( q{update minion_locks set expires = datetime('now', '-1 second') where name = 'yada'}, ); is $minion->backend->list_locks(0, 10, {names => ['yada']})->{total}, 0, 'no results'; $minion->unlock('test'); is $minion->backend->list_locks(0, 10)->{total}, 0, 'no results'; }; subtest 'Lock with guard' => sub { ok my $guard = $minion->guard('foo', 3600, {limit => 1}), 'locked'; ok !$minion->guard('foo', 3600, {limit => 1}), 'not locked again'; undef $guard; ok $guard = $minion->guard('foo', 3600), 'locked'; ok !$minion->guard('foo', 3600), 'not locked again'; undef $guard; ok $minion->guard('foo', 3600, {limit => 1}), 'locked again'; ok $minion->guard('foo', 3600, {limit => 1}), 'locked again'; ok $guard = $minion->guard('bar', 3600, {limit => 2}), 'locked'; ok my $guard2 = $minion->guard('bar', 0, {limit => 2}), 'locked'; ok my $guard3 = $minion->guard('bar', 3600, {limit => 2}), 'locked'; undef $guard2; ok !$minion->guard('bar', 3600, {limit => 2}), 'not locked again'; undef $guard; undef $guard3; }; subtest 'Reset (locks)' => sub { $minion->enqueue('test'); $minion->lock('test', 3600); $minion->worker->register unless HAS_PSEUDOFORK; ok $minion->backend->list_jobs(0, 1)->{total}, 'jobs'; ok $minion->backend->list_locks(0, 1)->{total}, 'locks'; SKIP: { skip 'Minion workers do not support fork emulation', 1 if HAS_PSEUDOFORK; ok $minion->backend->list_workers(0, 1)->{total}, 'workers'; } $minion->reset({locks => 1}); ok $minion->backend->list_jobs(0, 1)->{total}, 'jobs'; ok !$minion->backend->list_locks(0, 1)->{total}, 'no locks'; SKIP: { skip 'Minion workers do not support fork emulation', 1 if HAS_PSEUDOFORK; ok $minion->backend->list_workers(0, 1)->{total}, 'workers'; } }; subtest 'Reset (all)' => sub { $minion->lock('test', 3600); ok $minion->backend->list_jobs(0, 1)->{total}, 'jobs'; ok $minion->backend->list_locks(0, 1)->{total}, 'locks'; SKIP: { skip 'Minion workers do not support fork emulation', 1 if HAS_PSEUDOFORK; ok $minion->backend->list_workers(0, 1)->{total}, 'workers'; } $minion->reset({all => 1})->repair; ok !$minion->backend->list_jobs(0, 1)->{total}, 'no jobs'; ok !$minion->backend->list_locks(0, 1)->{total}, 'no locks'; ok !$minion->backend->list_workers(0, 1)->{total}, 'no workers'; }; subtest 'Stats' => sub { $minion->add_task( add => sub { my ($job, $first, $second) = @_; $job->finish({added => $first + $second}); } ); $minion->add_task(fail => sub { die "Intentional failure!\n" }); my $stats = $minion->stats; is $stats->{active_workers}, 0, 'no active workers'; is $stats->{inactive_workers}, 0, 'no inactive workers'; is $stats->{enqueued_jobs}, 0, 'no enqueued jobs'; is $stats->{active_jobs}, 0, 'no active jobs'; is $stats->{failed_jobs}, 0, 'no failed jobs'; is $stats->{finished_jobs}, 0, 'no finished jobs'; is $stats->{inactive_jobs}, 0, 'no inactive jobs'; is $stats->{delayed_jobs}, 0, 'no delayed jobs'; is $stats->{active_locks}, 0, 'no active locks'; is $stats->{uptime}, undef, 'uptime is undefined'; my $worker; SKIP: { skip 'Minion workers do not support fork emulation', 1 if HAS_PSEUDOFORK; $worker = $minion->worker->register; is $minion->stats->{inactive_workers}, 1, 'one inactive worker'; } $minion->enqueue('fail'); is $minion->stats->{enqueued_jobs}, 1, 'one enqueued job'; $minion->enqueue('fail'); is $minion->stats->{enqueued_jobs}, 2, 'two enqueued jobs'; is $minion->stats->{inactive_jobs}, 2, 'two inactive jobs'; my $job; SKIP: { skip 'Minion workers do not support fork emulation', 4 if HAS_PSEUDOFORK; ok $job = $worker->dequeue(0), 'job dequeued'; my $stats = $minion->stats; is $stats->{active_workers}, 1, 'one active worker'; is $stats->{active_jobs}, 1, 'one active job'; is $stats->{inactive_jobs}, 1, 'one inactive job'; } $minion->enqueue('fail'); SKIP: { skip 'Minion workers do not support fork emulation', 20 if HAS_PSEUDOFORK; ok my $job2 = $worker->dequeue(0), 'job dequeued'; my $stats = $minion->stats; is $stats->{active_workers}, 1, 'one active worker'; is $stats->{active_jobs}, 2, 'two active jobs'; is $stats->{inactive_jobs}, 1, 'one inactive job'; ok $job2->finish, 'job finished'; ok $job->finish, 'job finished'; is $minion->stats->{finished_jobs}, 2, 'two finished jobs'; ok my $job = $worker->dequeue(0), 'job dequeued'; ok $job->fail, 'job failed'; is $minion->stats->{failed_jobs}, 1, 'one failed job'; ok $job->retry, 'job retried'; is $minion->stats->{failed_jobs}, 0, 'no failed jobs'; ok $worker->dequeue(0)->finish(['works']), 'job finished'; $worker->unregister; $stats = $minion->stats; is $stats->{active_workers}, 0, 'no active workers'; is $stats->{inactive_workers}, 0, 'no inactive workers'; is $stats->{active_jobs}, 0, 'no active jobs'; is $stats->{failed_jobs}, 0, 'no failed jobs'; is $stats->{finished_jobs}, 3, 'three finished jobs'; is $stats->{inactive_jobs}, 0, 'no inactive jobs'; is $stats->{delayed_jobs}, 0, 'no delayed jobs'; } }; subtest 'History' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; $minion->enqueue('fail'); my $worker = $minion->worker->register; ok my $job = $worker->dequeue(0), 'job dequeued'; ok $job->fail, 'job failed'; $worker->unregister; my $history = $minion->history; is $#{$history->{daily}}, 23, 'data for 24 hours'; is $history->{daily}[-1]{finished_jobs} + $history->{daily}[-2]{finished_jobs}, 3, 'one failed job in the last hour'; is $history->{daily}[-1]{failed_jobs} + $history->{daily}[-2]{failed_jobs}, 1, 'three finished jobs in the last hour'; is $history->{daily}[0]{finished_jobs}, 0, 'no finished jobs 24 hours ago'; is $history->{daily}[0]{failed_jobs}, 0, 'no failed jobs 24 hours ago'; ok defined $history->{daily}[0]{epoch}, 'has date value'; ok defined $history->{daily}[1]{epoch}, 'has date value'; ok defined $history->{daily}[12]{epoch}, 'has date value'; ok defined $history->{daily}[-1]{epoch}, 'has date value'; $job->remove; }; subtest 'List jobs' => sub { my $id = $minion->enqueue('add'); is $minion->backend->list_jobs(1, 1)->{total}, 4, 'four total with offset and limit'; my $results = $minion->backend->list_jobs(0, 10); my $batch = $results->{jobs}; is $results->{total}, 4, 'four jobs total'; ok $batch->[0]{id}, 'has id'; is $batch->[0]{task}, 'add', 'right task'; is $batch->[0]{state}, 'inactive', 'right state'; is $batch->[0]{retries}, 0, 'job has not been retried'; like $batch->[0]{created}, qr/^[\d.]+$/, 'has created timestamp'; is $batch->[1]{task}, 'fail', 'right task'; is_deeply $batch->[1]{args}, [], 'right arguments'; is_deeply $batch->[1]{notes}, {}, 'right metadata'; SKIP: { skip 'Minion workers do not support fork emulation', 2 if HAS_PSEUDOFORK; is_deeply $batch->[1]{result}, ['works'], 'right result'; is $batch->[1]{state}, 'finished', 'right state'; }; is $batch->[1]{priority}, 0, 'right priority'; is_deeply $batch->[1]{parents}, [], 'right parents'; is_deeply $batch->[1]{children}, [], 'right children'; SKIP: { skip 'Minion workers do not support fork emulation', 1 if HAS_PSEUDOFORK; is $batch->[1]{retries}, 1, 'job has been retried'; } like $batch->[1]{created}, qr/^[\d.]+$/, 'has created timestamp'; like $batch->[1]{delayed}, qr/^[\d.]+$/, 'has delayed timestamp'; SKIP: { skip 'Minion workers do not support fork emulation', 3 if HAS_PSEUDOFORK; like $batch->[1]{finished}, qr/^[\d.]+$/, 'has finished timestamp'; like $batch->[1]{retried}, qr/^[\d.]+$/, 'has retried timestamp'; like $batch->[1]{started}, qr/^[\d.]+$/, 'has started timestamp'; } is $batch->[2]{task}, 'fail', 'right task'; SKIP: { skip 'Minion workers do not support fork emulation', 1 if HAS_PSEUDOFORK; is $batch->[2]{state}, 'finished', 'right state'; } is $batch->[2]{retries}, 0, 'job has not been retried'; is $batch->[3]{task}, 'fail', 'right task'; SKIP: { skip 'Minion workers do not support fork emulation', 1 if HAS_PSEUDOFORK; is $batch->[3]{state}, 'finished', 'right state'; } is $batch->[3]{retries}, 0, 'job has not been retried'; ok !$batch->[4], 'no more results'; $batch = $minion->backend->list_jobs(0, 10, {states => ['inactive']})->{jobs}; is $batch->[0]{state}, 'inactive', 'right state'; is $batch->[0]{retries}, 0, 'job has not been retried'; SKIP: { skip 'Minion workers do not support fork emulation', 1 if HAS_PSEUDOFORK; ok !$batch->[1], 'no more results'; } $batch = $minion->backend->list_jobs(0, 10, {tasks => ['add']})->{jobs}; is $batch->[0]{task}, 'add', 'right task'; is $batch->[0]{retries}, 0, 'job has not been retried'; ok !$batch->[1], 'no more results'; $batch = $minion->backend->list_jobs(0, 10, {tasks => ['add', 'fail']})->{jobs}; is $batch->[0]{task}, 'add', 'right task'; is $batch->[1]{task}, 'fail', 'right task'; is $batch->[2]{task}, 'fail', 'right task'; is $batch->[3]{task}, 'fail', 'right task'; ok !$batch->[4], 'no more results'; $batch = $minion->backend->list_jobs(0, 10, {queues => ['default']})->{jobs}; is $batch->[0]{queue}, 'default', 'right queue'; is $batch->[1]{queue}, 'default', 'right queue'; is $batch->[2]{queue}, 'default', 'right queue'; is $batch->[3]{queue}, 'default', 'right queue'; ok !$batch->[4], 'no more results'; TODO: { todo_skip 'filtering jobs by notes not yet implemented', 3; my $id2 = $minion->enqueue('test' => [] => {notes => {is_test => 1}}); my $batch = $minion->backend->list_jobs(0, 10, {notes => ['is_test']})->{jobs}; is $batch->[0]{task}, 'test', 'right task'; ok !$batch->[4], 'no more results'; ok $minion->job($id2)->remove, 'job removed'; } $batch = $minion->backend->list_jobs(0, 10, {queues => ['does_not_exist']})->{jobs}; is_deeply $batch, [], 'no results'; $results = $minion->backend->list_jobs(0, 1); $batch = $results->{jobs}; is $results->{total}, 4, 'four jobs total'; is $batch->[0]{state}, 'inactive', 'right state'; is $batch->[0]{retries}, 0, 'job has not been retried'; ok !$batch->[1], 'no more results'; $batch = $minion->backend->list_jobs(1, 1)->{jobs}; SKIP: { skip 'Minion workers do not support fork emulation', 2 if HAS_PSEUDOFORK; is $batch->[0]{state}, 'finished', 'right state'; is $batch->[0]{retries}, 1, 'job has been retried'; } ok !$batch->[1], 'no more results'; my $jobs = $minion->jobs; is $jobs->next->{task}, 'add', 'right task'; is $jobs->options->{before}, 1, 'before 1'; is $jobs->next->{task}, 'fail', 'right task'; is $jobs->next->{task}, 'fail', 'right task'; is $jobs->next->{task}, 'fail', 'right task'; is $jobs->next, undef, 'no more results'; $jobs = $minion->jobs->fetch(2); is $jobs->options->{before}, undef, 'no before'; is $jobs->next->{task}, 'add', 'right task'; is $jobs->options->{before}, 3, 'before 3'; is $jobs->next->{task}, 'fail', 'right task'; is $jobs->options->{before}, 3, 'before 3'; is $jobs->next->{task}, 'fail', 'right task'; is $jobs->options->{before}, 1, 'before 1'; is $jobs->next->{task}, 'fail', 'right task'; is $jobs->options->{before}, 1, 'before 1'; is $jobs->next, undef, 'no more results'; $jobs = $minion->jobs({states => ['inactive']}); is $jobs->next->{task}, 'add', 'right task'; SKIP: { skip 'Minion workers do not support fork emulation', 1 if HAS_PSEUDOFORK; is $jobs->next, undef, 'no more results'; } $jobs = $minion->jobs({states => ['active']}); is $jobs->next, undef, 'no more results'; $jobs = $minion->jobs->fetch(1); is $jobs->next->{task}, 'add', 'right task'; my $id2 = $minion->enqueue('add'); my $next = $jobs->next; is $next->{task}, 'fail', 'right task'; ok $minion->job($next->{id} - 1)->remove, 'job removed'; is $jobs->next->{task}, 'fail', 'right task'; is $jobs->next, undef, 'no more results'; ok $minion->job($id)->remove, 'job removed'; ok $minion->job($id2)->remove, 'job removed'; }; subtest 'Enqueue, dequeue and perform' => sub { is $minion->job(12345), undef, 'job does not exist'; my $id = $minion->enqueue(add => [2, 2]); ok $minion->job($id), 'job does exist'; my $info = $minion->job($id)->info; is_deeply $info->{args}, [2, 2], 'right arguments'; is $info->{priority}, 0, 'right priority'; is $info->{state}, 'inactive', 'right state'; SKIP: { skip 'Minion workers do not support fork emulation', 20 if HAS_PSEUDOFORK; my $worker = $minion->worker; is $worker->dequeue(0), undef, 'not registered'; ok !$minion->job($id)->info->{started}, 'no started timestamp'; ok my $job = $worker->register->dequeue(0), 'job dequeued'; is $worker->info->{jobs}[0], $job->id, 'right job'; like $job->info->{created}, qr/^[\d.]+$/, 'has created timestamp'; like $job->info->{started}, qr/^[\d.]+$/, 'has started timestamp'; like $job->info->{time}, qr/[\d.]+$/, 'has current time'; is_deeply $job->args, [2, 2], 'right arguments'; is $job->info->{state}, 'active', 'right state'; is $job->task, 'add', 'right task'; is $job->retries, 0, 'job has not been retried'; my $id = $job->info->{worker}; is $minion->backend->list_workers(0, 1, {ids => [$id]})->{workers}[0]{pid}, $$, 'right worker'; ok !$job->info->{finished}, 'no finished timestamp'; $job->perform; is $worker->info->{jobs}[0], undef, 'no jobs'; like $job->info->{finished}, qr/^[\d.]+$/, 'has finished timestamp'; is_deeply $job->info->{result}, {added => 4}, 'right result'; is $job->info->{state}, 'finished', 'right state'; $worker->unregister; $job = $minion->job($job->id); is_deeply $job->args, [2, 2], 'right arguments'; is $job->retries, 0, 'job has not been retried'; is $job->info->{state}, 'finished', 'right state'; is $job->task, 'add', 'right task'; } }; subtest 'Retry and remove' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; my $id = $minion->enqueue(add => [5, 6]); my $worker = $minion->worker->register; ok my $job = $worker->dequeue(0), 'job dequeued'; is $job->info->{attempts}, 1, 'job will be attempted once'; is $job->info->{retries}, 0, 'job has not been retried'; is $job->id, $id, 'right id'; ok $job->finish, 'job finished'; ok !$worker->dequeue(0), 'no more jobs'; $job = $minion->job($id); ok !$job->info->{retried}, 'no retried timestamp'; ok $job->retry, 'job retried'; like $job->info->{retried}, qr/^[\d.]+$/, 'has retried timestamp'; is $job->info->{state}, 'inactive', 'right state'; is $job->info->{retries}, 1, 'job has been retried once'; ok $job = $worker->dequeue(0), 'job dequeued'; is $job->retries, 1, 'job has been retried once'; ok $job->retry, 'job retried'; is $job->id, $id, 'right id'; is $job->info->{retries}, 2, 'job has been retried twice'; ok $job = $worker->dequeue(0), 'job dequeued'; is $job->info->{state}, 'active', 'right state'; ok $job->finish, 'job finished'; ok $job->remove, 'job has been removed'; ok !$job->retry, 'job not retried'; is $job->info, undef, 'no information'; $id = $minion->enqueue(add => [6, 5]); $job = $minion->job($id); is $job->info->{state}, 'inactive', 'right state'; is $job->info->{retries}, 0, 'job has not been retried'; ok $job->retry, 'job retried'; is $job->info->{state}, 'inactive', 'right state'; is $job->info->{retries}, 1, 'job has been retried once'; ok $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; ok $job->fail, 'job failed'; ok $job->remove, 'job has been removed'; is $job->info, undef, 'no information'; $id = $minion->enqueue(add => [5, 5]); $job = $minion->job("$id"); ok $job->remove, 'job has been removed'; $worker->unregister; }; subtest 'Jobs with priority' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; $minion->enqueue(add => [1, 2]); my $id = $minion->enqueue(add => [2, 4], {priority => 1}); my $worker = $minion->worker->register; ok my $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; is $job->info->{priority}, 1, 'right priority'; ok $job->finish, 'job finished'; isnt $worker->dequeue(0)->id, $id, 'different id'; $id = $minion->enqueue(add => [2, 5]); ok $job = $worker->register->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; is $job->info->{priority}, 0, 'right priority'; ok $job->finish, 'job finished'; ok $job->retry({priority => 100}), 'job retried with higher priority'; ok $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; is $job->info->{retries}, 1, 'job has been retried once'; is $job->info->{priority}, 100, 'high priority'; ok $job->finish, 'job finished'; ok $job->retry({priority => 0}), 'job retried with lower priority'; ok $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; is $job->info->{retries}, 2, 'job has been retried twice'; is $job->info->{priority}, 0, 'low priority'; ok $job->finish, 'job finished'; $id = $minion->enqueue(add => [2, 6], {priority => 2}); ok !$worker->dequeue(0, {min_priority => 5}); ok !$worker->dequeue(0, {min_priority => 3}); ok $job = $worker->dequeue(0, {min_priority => 2}); is $job->id, $id, 'right id'; is $job->info->{priority}, 2, 'expected priority'; ok $job->finish, 'job finished'; $minion->enqueue(add => [2, 8], {priority => 0}); $minion->enqueue(add => [2, 7], {priority => 5}); $minion->enqueue(add => [2, 8], {priority => -2}); ok !$worker->dequeue(0, {min_priority => 6}); ok $job = $worker->dequeue(0, {min_priority => 0}); is $job->info->{priority}, 5, 'expected priority'; ok $job->finish, 'job finished'; ok $job = $worker->dequeue(0, {min_priority => 0}); is $job->info->{priority}, 0, 'expected priority'; ok $job->finish, 'job finished'; ok !$worker->dequeue(0, {min_priority => 0}); ok $job = $worker->dequeue(0, {min_priority => -10}); is $job->info->{priority}, -2, 'expected priority'; ok $job->finish, 'job finished'; $worker->unregister; }; subtest 'Delayed jobs' => sub { my $id = $minion->enqueue(add => [2, 1] => {delay => 100}); is $minion->stats->{delayed_jobs}, 1, 'one delayed job'; SKIP: { skip 'Minion workers do not support fork emulation', 17 if HAS_PSEUDOFORK; my $worker = $minion->worker->register; is $worker->dequeue(0), undef, 'too early for job'; my $job = $minion->job($id); ok $job->info->{delayed} > $job->info->{created}, 'delayed timestamp'; $minion->backend->sqlite->db->query( q{update minion_jobs set delayed = datetime('now','-1 day') where id = ?}, $id); ok $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; like $job->info->{delayed}, qr/^[\d.]+$/, 'has delayed timestamp'; ok $job->finish, 'job finished'; ok $job->retry, 'job retried'; my $info = $minion->job($id)->info; ok $info->{delayed} <= $info->{retried}, 'no delayed timestamp'; ok $job->remove, 'job removed'; ok !$job->retry, 'job not retried'; my $id = $minion->enqueue(add => [6, 9]); ok $job = $worker->dequeue(0), 'job dequeued'; $info = $minion->job($id)->info; ok $info->{delayed} <= $info->{created}, 'no delayed timestamp'; ok $job->fail, 'job failed'; ok $job->retry({delay => 100}), 'job retried with delay'; $info = $minion->job($id)->info; is $info->{retries}, 1, 'job has been retried once'; ok $info->{delayed} > $info->{retried}, 'delayed timestamp'; ok $minion->job($id)->remove, 'job has been removed'; $worker->unregister; } }; subtest 'Events' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; my ($enqueue, $pid_start, $pid_stop); my ($failed, $finished) = (0, 0); $minion->once(enqueue => sub { $enqueue = pop }); $minion->once( worker => sub { my ($minion, $worker) = @_; $worker->on( dequeue => sub { my ($worker, $job) = @_; $job->on(failed => sub { $failed++ }); $job->on(finished => sub { $finished++ }); $job->on(spawn => sub { $pid_start = pop }); $job->on(reap => sub { $pid_stop = pop }); $job->on( start => sub { my $job = shift; return unless $job->task eq 'switcheroo'; $job->task('add')->args->[-1] += 1; } ); $job->on( finish => sub { my $job = shift; return unless defined(my $old = $job->info->{notes}{finish_count}); $job->note(finish_count => $old + 1, finish_pid => $$); } ); $job->on( cleanup => sub { my $job = shift; return unless defined(my $old = $job->info->{notes}{finish_count}); $job->note(cleanup_count => $old + 1, cleanup_pid => $$); } ); } ); } ); my $worker = $minion->worker->register; my $id = $minion->enqueue(add => [3, 3]); is $enqueue, $id, 'enqueue event has been emitted'; $minion->enqueue(add => [4, 3]); ok my $job = $worker->dequeue(0), 'job dequeued'; is $failed, 0, 'failed event has not been emitted'; is $finished, 0, 'finished event has not been emitted'; my $result; $job->on(finished => sub { $result = pop }); ok $job->finish('Everything is fine!'), 'job finished'; $job->perform; is $result, 'Everything is fine!', 'right result'; is $failed, 0, 'failed event has not been emitted'; is $finished, 1, 'finished event has been emitted once'; isnt $pid_start, $$, 'new process id'; isnt $pid_stop, $$, 'new process id'; is $pid_start, $pid_stop, 'same process id'; ok $job = $worker->dequeue(0), 'job dequeued'; my $err; $job->on(failed => sub { $err = pop }); $job->fail("test\n"); $job->fail; is $err, "test\n", 'right error'; is $failed, 1, 'failed event has been emitted once'; is $finished, 1, 'finished event has been emitted once'; $minion->add_task(switcheroo => sub { }); $minion->enqueue(switcheroo => [5, 3] => {notes => {finish_count => 0, before => 23}}); ok $job = $worker->dequeue(0), 'job dequeued'; $job->perform; is_deeply $job->info->{result}, {added => 9}, 'right result'; is $job->info->{notes}{finish_count}, 1, 'finish event has been emitted once'; ok $job->info->{notes}{finish_pid}, 'has a process id'; isnt $job->info->{notes}{finish_pid}, $$, 'different process id'; is $job->info->{notes}{before}, 23, 'value still exists'; is $job->info->{notes}{cleanup_count}, 2, 'cleanup event has been emitted once'; ok $job->info->{notes}{cleanup_pid}, 'has a process id'; isnt $job->info->{notes}{cleanup_pid}, $$, 'different process id'; $worker->unregister; }; subtest 'Queues' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; my $id = $minion->enqueue(add => [100, 1]); my $worker = $minion->worker->register; is $worker->dequeue(0 => {queues => ['test1']}), undef, 'wrong queue'; ok my $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; is $job->info->{queue}, 'default', 'right queue'; ok $job->finish, 'job finished'; $id = $minion->enqueue(add => [100, 3] => {queue => 'test1'}); is $worker->dequeue(0), undef, 'wrong queue'; ok $job = $worker->dequeue(0 => {queues => ['test1']}), 'job dequeued'; is $job->id, $id, 'right id'; is $job->info->{queue}, 'test1', 'right queue'; ok $job->finish, 'job finished'; ok $job->retry({queue => 'test2'}), 'job retried'; ok $job = $worker->dequeue(0 => {queues => ['default', 'test2']}), 'job dequeued'; is $job->id, $id, 'right id'; is $job->info->{queue}, 'test2', 'right queue'; ok $job->finish, 'job finished'; $worker->unregister; }; subtest 'Failed jobs' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; my $id = $minion->enqueue(add => [5, 6]); my $worker = $minion->worker->register; ok my $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; is $job->info->{result}, undef, 'no result'; ok $job->fail, 'job failed'; ok !$job->finish, 'job not finished'; is $job->info->{state}, 'failed', 'right state'; is $job->info->{result}, 'Unknown error', 'right result'; $id = $minion->enqueue(add => [6, 7]); ok $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; ok $job->fail('Something bad happened!'), 'job failed'; is $job->info->{state}, 'failed', 'right state'; is $job->info->{result}, 'Something bad happened!', 'right result'; $id = $minion->enqueue('fail'); ok $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; $job->perform; is $job->info->{state}, 'failed', 'right state'; is $job->info->{result}, "Intentional failure!\n", 'right result'; $worker->unregister; }; subtest 'Nested data structures' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; $minion->add_task( nested => sub { my ($job, $hash, $array) = @_; $job->note(bar => {baz => [1, 2, 3]}); $job->note(baz => 'yada'); $job->finish([{23 => $hash->{first}[0]{second} x $array->[0][0]}]); } ); $minion->enqueue('nested', [{first => [{second => 'test'}]}, [[3]]], {notes => {foo => [4, 5, 6]}}); my $worker = $minion->worker->register; ok my $job = $worker->dequeue(0), 'job dequeued'; $job->perform; is $job->info->{state}, 'finished', 'right state'; ok $job->note(yada => ['works']), 'added metadata'; ok !$minion->backend->note(-1, {yada => ['failed']}), 'not added metadata'; my $notes = {foo => [4, 5, 6], bar => {baz => [1, 2, 3]}, baz => 'yada', yada => ['works']}; is_deeply $job->info->{notes}, $notes, 'right metadata'; is_deeply $job->info->{result}, [{23 => 'testtesttest'}], 'right structure'; ok $job->note(yada => undef, bar => undef), 'removed metadata'; $notes = {foo => [4, 5, 6], baz => 'yada'}; is_deeply $job->info->{notes}, $notes, 'right metadata'; $worker->unregister; }; subtest 'Perform job in a running event loop' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; my $id = $minion->enqueue(add => [8, 9]); Mojo::Promise->new->resolve->then(sub { $minion->perform_jobs })->wait; is $minion->job($id)->info->{state}, 'finished', 'right state'; is_deeply $minion->job($id)->info->{result}, {added => 17}, 'right result'; }; subtest 'Job terminated unexpectedly' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; $minion->add_task(exit => sub { exit 1 }); my $id = $minion->enqueue('exit'); my $worker = $minion->worker->register; ok my $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; $job->perform; is $job->info->{state}, 'failed', 'right state'; ok $job->info->{result}, 'error message in result'; $worker->unregister; }; subtest 'Multiple attempts while processing' => sub { is $minion->backoff->(0), 15, 'right result'; is $minion->backoff->(1), 16, 'right result'; is $minion->backoff->(2), 31, 'right result'; is $minion->backoff->(3), 96, 'right result'; is $minion->backoff->(4), 271, 'right result'; is $minion->backoff->(5), 640, 'right result'; is $minion->backoff->(25), 390640, 'right result'; SKIP: { skip 'Minion workers do not support fork emulation', 31 if HAS_PSEUDOFORK; my $id = $minion->enqueue(exit => [] => {attempts => 3}); my $worker = $minion->worker->register; ok my $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; is $job->retries, 0, 'job has not been retried'; my $info = $job->info; is $info->{attempts}, 3, 'three attempts'; is $info->{state}, 'active', 'right state'; $job->perform; $info = $job->info; is $info->{attempts}, 2, 'two attempts'; is $info->{state}, 'inactive', 'right state'; ok $info->{result}, 'error message in result'; ok $info->{retried} < $info->{delayed}, 'delayed timestamp'; $minion->backend->sqlite->db->query( q{update minion_jobs set delayed = datetime('now') where id = ?}, $id); ok $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; is $job->retries, 1, 'job has been retried'; $info = $job->info; is $info->{attempts}, 2, 'two attempts'; is $info->{state}, 'active', 'right state'; $job->perform; $info = $job->info; is $info->{attempts}, 1, 'one attempt'; is $info->{state}, 'inactive', 'right state'; $minion->backend->sqlite->db->query( q{update minion_jobs set delayed = datetime('now') where id = ?}, $id); ok $job = $worker->register->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; is $job->retries, 2, 'two retries'; $info = $job->info; is $info->{attempts}, 1, 'one attempt'; is $info->{state}, 'active', 'right state'; $job->perform; $info = $job->info; is $info->{attempts}, 1, 'one attempt'; is $info->{state}, 'failed', 'right state'; ok $info->{result}, 'error message in result'; ok $job->retry({attempts => 2}), 'job retried'; ok $job = $worker->register->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; $job->perform; is $job->info->{state}, 'inactive', 'right state'; $minion->backend->sqlite->db->query( q{update minion_jobs set delayed = datetime('now') where id = ?}, $id); ok $job = $worker->register->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; $job->perform; is $job->info->{state}, 'failed', 'right state'; $worker->unregister; } }; subtest 'Multiple attempts during maintenance' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; my $id = $minion->enqueue(exit => [] => {attempts => 2}); my $worker = $minion->worker->register; ok my $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; is $job->retries, 0, 'job has not been retried'; is $job->info->{attempts}, 2, 'job will be attempted twice'; is $job->info->{state}, 'active', 'right state'; $worker->unregister; $minion->repair; is $job->info->{state}, 'inactive', 'right state'; is $job->info->{result}, 'Worker went away', 'right result'; ok $job->info->{retried} < $job->info->{delayed}, 'delayed timestamp'; $minion->backend->sqlite->db->query( q{update minion_jobs set delayed = datetime('now') where id = ?}, $id); ok $job = $worker->register->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; is $job->retries, 1, 'job has been retried once'; $worker->unregister; $minion->repair; is $job->info->{state}, 'failed', 'right state'; is $job->info->{result}, 'Worker went away', 'right result'; }; subtest 'A job needs to be dequeued again after a retry' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; $minion->add_task(restart => sub { }); my $id = $minion->enqueue('restart'); my $worker = $minion->worker->register; ok my $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; ok $job->finish, 'job finished'; is $job->info->{state}, 'finished', 'right state'; ok $job->retry, 'job retried'; is $job->info->{state}, 'inactive', 'right state'; ok my $job2 = $worker->dequeue(0), 'job dequeued'; is $job->info->{state}, 'active', 'right state'; ok !$job->finish, 'job not finished'; is $job->info->{state}, 'active', 'right state'; is $job2->id, $id, 'right id'; ok $job2->finish, 'job finished'; ok !$job->retry, 'job not retried'; is $job->info->{state}, 'finished', 'right state'; $worker->unregister; }; subtest 'Perform jobs concurrently' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; my $id = $minion->enqueue(add => [10, 11]); my $id2 = $minion->enqueue(add => [12, 13]); my $id3 = $minion->enqueue('test'); my $id4 = $minion->enqueue('exit'); my $worker = $minion->worker->register; ok my $job = $worker->dequeue(0), 'job dequeued'; ok my $job2 = $worker->dequeue(0), 'job dequeued'; ok my $job3 = $worker->dequeue(0), 'job dequeued'; ok my $job4 = $worker->dequeue(0), 'job dequeued'; my $pid = $job->start; my $pid2 = $job2->start; my $pid3 = $job3->start; my $pid4 = $job4->start; my ($first, $second, $third, $fourth); usleep 50000 until $first ||= $job->is_finished and $second ||= $job2->is_finished and $third ||= $job3->is_finished and $fourth ||= $job4->is_finished; is $minion->job($id)->info->{state}, 'finished', 'right state'; is_deeply $minion->job($id)->info->{result}, {added => 21}, 'right result'; is $minion->job($id2)->info->{state}, 'finished', 'right state'; is_deeply $minion->job($id2)->info->{result}, {added => 25}, 'right result'; is $minion->job($id3)->info->{state}, 'finished', 'right state'; is $minion->job($id3)->info->{result}, undef, 'no result'; is $minion->job($id4)->info->{state}, 'failed', 'right state'; ok $minion->job($id4)->info->{result}, 'error message in result'; $worker->unregister; }; subtest 'Stopping jobs' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; $minion->add_task( long_running => sub { shift->note(started => 1); sleep 1000; } ); my $worker = $minion->worker->register; $minion->enqueue('long_running'); ok my $job = $worker->dequeue(0), 'job dequeued'; ok $job->start->pid, 'has a process id'; ok !$job->is_finished, 'job is not finished'; $job->stop; usleep 5000 until $job->is_finished; is $job->info->{state}, 'failed', 'right state'; ok $job->info->{result}, 'error message in result'; $minion->enqueue('long_running'); ok $job = $worker->dequeue(0), 'job dequeued'; ok $job->start->pid, 'has a process id'; ok !$job->is_finished, 'job is not finished'; usleep 5000 until $job->info->{notes}{started}; $job->kill('USR1'); $job->kill('USR2'); is $job->info->{state}, 'active', 'right state'; $job->kill('INT'); usleep 5000 until $job->is_finished; is $job->info->{state}, 'failed', 'right state'; ok $job->info->{result}, 'error message in result'; $worker->unregister; }; subtest 'Job dependencies' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; my $worker = $minion->remove_after(0)->worker->register; is $minion->repair->stats->{finished_jobs}, 0, 'no finished jobs'; my $id = $minion->enqueue('test'); my $id2 = $minion->enqueue('test'); my $id3 = $minion->enqueue(test => [] => {parents => [$id, $id2]}); ok my $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; is_deeply $job->info->{children}, [$id3], 'right children'; is_deeply $job->info->{parents}, [], 'right parents'; ok my $job2 = $worker->dequeue(0), 'job dequeued'; is $job2->id, $id2, 'right id'; is_deeply $job2->info->{children}, [$id3], 'right children'; is_deeply $job2->info->{parents}, [], 'right parents'; ok !$worker->dequeue(0), 'parents are not ready yet'; ok $job->finish, 'job finished'; ok !$worker->dequeue(0), 'parents are not ready yet'; ok $job2->fail, 'job failed'; ok !$worker->dequeue(0), 'parents are not ready yet'; ok $job2->retry, 'job retried'; ok $job2 = $worker->dequeue(0), 'job dequeued'; is $job2->id, $id2, 'right id'; ok $job2->finish, 'job finished'; ok $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id3, 'right id'; is_deeply $job->info->{children}, [], 'right children'; is_deeply $job->info->{parents}, [$id, $id2], 'right parents'; is $minion->stats->{finished_jobs}, 2, 'two finished jobs'; is $minion->repair->stats->{finished_jobs}, 2, 'two finished jobs'; ok $job->finish, 'job finished'; is $minion->stats->{finished_jobs}, 3, 'three finished jobs'; is $minion->repair->remove_after(172800)->stats->{finished_jobs}, 0, 'no finished jobs'; $id = $minion->enqueue(test => [] => {parents => [-1]}); ok $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; ok $job->finish, 'job finished'; $id = $minion->enqueue(test => [] => {parents => [-1]}); ok $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; is_deeply $job->info->{parents}, [-1], 'right parents'; $job->retry({parents => [-1, -2]}); ok $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; is_deeply $job->info->{parents}, [-1, -2], 'right parents'; ok $job->finish, 'job finished'; my $id4 = $minion->enqueue('test'); my $id5 = $minion->enqueue('test'); my $id6 = $minion->enqueue(test => [] => {parents => [$id4, $id5]}); my $child = $minion->job($id6); my $parents = $child->parents; is $parents->size, 2, 'two parents'; is $parents->[0]->id, $id4, 'first parent'; is $parents->[1]->id, $id5, 'second parent'; $_->remove for $parents->each; is $child->parents->size, 0, 'no parents'; ok $child->remove, 'job removed'; $worker->unregister; }; subtest 'Job dependencies (lax)' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; my $worker = $minion->worker->register; my $id = $minion->enqueue('test'); my $id2 = $minion->enqueue('test'); my $id3 = $minion->enqueue(test => [] => {lax => 1, parents => [$id, $id2]}); ok my $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; is_deeply $job->info->{children}, [$id3], 'right children'; is_deeply $job->info->{parents}, [], 'right parents'; ok my $job2 = $worker->dequeue(0), 'job dequeued'; is $job2->id, $id2, 'right id'; is_deeply $job2->info->{children}, [$id3], 'right children'; is_deeply $job2->info->{parents}, [], 'right parents'; ok !$worker->dequeue(0), 'parents are not ready yet'; ok $job->finish, 'job finished'; ok !$worker->dequeue(0), 'parents are not ready yet'; ok $job2->fail, 'job failed'; ok my $job3 = $worker->dequeue(0), 'job dequeued'; is $job3->id, $id3, 'right id'; is_deeply $job3->info->{children}, [], 'right children'; is_deeply $job3->info->{parents}, [$id, $id2], 'right parents'; ok $job3->finish, 'job finished'; my $id4 = $minion->enqueue('test'); my $id5 = $minion->enqueue(test => [] => {parents => [$id4]}); ok my $job4 = $worker->dequeue(0), 'job dequeued'; is $job4->id, $id4, 'right id'; ok !$worker->dequeue(0), 'parents are not ready yet'; ok $job4->fail, 'job finished'; ok !$worker->dequeue(0), 'parents are not ready yet'; ok $minion->job($id5)->retry({lax => 1}), 'job is now lax'; ok my $job5 = $worker->dequeue(0), 'job dequeued'; is $job5->id, $id5, 'right id'; is_deeply $job5->info->{children}, [], 'right children'; is_deeply $job5->info->{parents}, [$id4], 'right parents'; ok $job5->finish, 'job finished'; ok $job4->remove, 'job removed'; is $minion->jobs({ids => [$id5]})->next->{lax}, 1, 'lax'; ok $minion->job($id5)->retry, 'job is still lax'; is $minion->jobs({ids => [$id5]})->next->{lax}, 1, 'lax'; ok $minion->job($id5)->retry({lax => 0}), 'job is not lax anymore'; is $minion->jobs({ids => [$id5]})->next->{lax}, 0, 'not lax'; ok $minion->job($id5)->retry, 'job is still not lax'; is $minion->jobs({ids => [$id5]})->next->{lax}, 0, 'not lax'; ok $minion->job($id5)->remove, 'job removed'; $worker->unregister; }; subtest 'Expiring jobs' => sub { my $id = $minion->enqueue('test'); is $minion->job($id)->info->{expires}, undef, 'no expires timestamp'; $minion->job($id)->remove; $id = $minion->enqueue('test' => [] => {expire => 300}); like $minion->job($id)->info->{expires}, qr/^[\d.]+$/, 'has expires timestamp'; SKIP: { skip 'Minion workers do not support fork emulation', 35 if HAS_PSEUDOFORK; my $worker = $minion->worker->register; ok my $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; my $expires = $job->info->{expires}; like $expires, qr/^[\d.]+$/, 'has expires timestamp'; ok $job->finish, 'job finished'; ok $job->retry({expire => 600}), 'job retried'; my $info = $minion->job($id)->info; is $info->{state}, 'inactive', 'rigth state'; like $info->{expires}, qr/^[\d.]+$/, 'has expires timestamp'; isnt $info->{expires}, $expires, 'retried with new expires timestamp'; is $minion->repair->jobs({states => ['inactive']})->total, 1, 'job has not expired yet'; ok $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; ok $job->finish, 'job finished'; $id = $minion->enqueue('test' => [] => {expire => 300}); is $minion->repair->jobs({states => ['inactive']})->total, 1, 'job has not expired yet'; my $sql = $minion->backend->sqlite; $sql->db->query(q{update minion_jobs set expires = datetime('now', '-1 day') where id = ?}, $id); is $minion->jobs({states => ['inactive']})->total, 0, 'job has expired'; ok !$worker->dequeue(0), 'job has expired'; ok $sql->db->select('minion_jobs', '*', {id => $id})->hash, 'job still exists in database'; $minion->repair; ok !$sql->db->select('minion_jobs', '*', {id => $id})->hash, 'job no longer exists in database'; $id = $minion->enqueue('test' => [] => {expire => 300}); ok $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; ok $job->finish, 'job finished'; $sql->db->query(q{update minion_jobs set expires = datetime('now', '-1 day') where id = ?}, $id); $minion->repair; ok $job = $minion->job($id), 'job still exists'; is $job->info->{state}, 'finished', 'right state'; $id = $minion->enqueue('test' => [] => {expire => 300}); ok $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; ok $job->fail, 'job failed'; $sql->db->query(q{update minion_jobs set expires = datetime('now', '-1 day') where id = ?}, $id); $minion->repair; ok $job = $minion->job($id), 'job still exists'; is $job->info->{state}, 'failed', 'right state'; $id = $minion->enqueue('test' => [] => {expire => 300}); ok $job = $worker->dequeue(0), 'job dequeued'; is $job->id, $id, 'right id'; $sql->db->query(q{update minion_jobs set expires = datetime('now', '-1 day') where id = ?}, $id); $minion->repair; ok $job = $minion->job($id), 'job still exists'; is $job->info->{state}, 'active', 'right state'; ok $job->finish, 'job finished'; $id = $minion->enqueue('test' => [] => {expire => 300}); my $id2 = $minion->enqueue('test' => [] => {parents => [$id]}); ok !$worker->dequeue(0, {id => $id2}), 'parent is still inactive'; $sql->db->query(q{update minion_jobs set expires = datetime('now', '-1 day') where id = ?}, $id); ok $job = $worker->dequeue(0, {id => $id2}), 'parent has expired'; ok $job->finish, 'job finished'; $worker->unregister; } }; subtest 'Foreground' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; my $id = $minion->enqueue(test => [] => {attempts => 2}); my $id2 = $minion->enqueue('test'); my $id3 = $minion->enqueue(test => [] => {parents => [$id, $id2]}); ok !$minion->foreground($id3 + 1), 'job does not exist'; ok !$minion->foreground($id3), 'job is not ready yet'; my $info = $minion->job($id)->info; is $info->{attempts}, 2, 'job will be attempted twice'; is $info->{state}, 'inactive', 'right state'; is $info->{queue}, 'default', 'right queue'; ok $minion->foreground($id), 'performed first job'; $info = $minion->job($id)->info; is $info->{attempts}, 1, 'job will be attempted once'; is $info->{retries}, 1, 'job has been retried'; is $info->{state}, 'finished', 'right state'; is $info->{queue}, 'minion_foreground', 'right queue'; ok $minion->foreground($id2), 'performed second job'; $info = $minion->job($id2)->info; is $info->{retries}, 1, 'job has been retried'; is $info->{state}, 'finished', 'right state'; is $info->{queue}, 'minion_foreground', 'right queue'; ok $minion->foreground($id3), 'performed third job'; $info = $minion->job($id3)->info; is $info->{retries}, 2, 'job has been retried twice'; is $info->{state}, 'finished', 'right state'; is $info->{queue}, 'minion_foreground', 'right queue'; $id = $minion->enqueue('fail'); eval { $minion->foreground($id) }; like $@, qr/Intentional failure!/, 'right error'; $info = $minion->job($id)->info; ok $info->{worker}, 'has worker'; ok !$minion->backend->list_workers(0, 1, {ids => [$info->{worker}]})->{workers}[0], 'not registered'; is $info->{retries}, 1, 'job has been retried'; is $info->{state}, 'failed', 'right state'; is $info->{queue}, 'minion_foreground', 'right queue'; is $info->{result}, "Intentional failure!\n", 'right result'; }; subtest 'Worker remote control commands' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; my $worker = $minion->worker->register->process_commands; my $worker2 = $minion->worker->register; my @commands; $_->add_command(test_id => sub { push @commands, shift->id }) for $worker, $worker2; $worker->add_command(test_args => sub { shift and push @commands, [@_] })->register; ok $minion->backend->broadcast('test_id', [], [$worker->id]), 'sent command'; ok $minion->backend->broadcast('test_id', [], [$worker->id, $worker2->id]), 'sent command'; $worker->process_commands->register; $worker2->process_commands; is_deeply \@commands, [$worker->id, $worker->id, $worker2->id], 'right structure'; @commands = (); ok $minion->backend->broadcast('test_id'), 'sent command'; ok $minion->backend->broadcast('test_whatever'), 'sent command'; ok $minion->backend->broadcast('test_args', [23], []), 'sent command'; ok $minion->backend->broadcast('test_args', [1, [2], {3 => 'three'}], [$worker->id]), 'sent command'; $_->process_commands for $worker, $worker2; is_deeply \@commands, [$worker->id, [23], [1, [2], {3 => 'three'}], $worker2->id], 'right structure'; $_->unregister for $worker, $worker2; ok !$minion->backend->broadcast('test_id', []), 'command not sent'; }; subtest 'Single process worker' => sub { plan skip_all => 'Minion workers do not support fork emulation' if HAS_PSEUDOFORK; my $worker = $minion->repair->worker->register; $minion->add_task( good_job => sub { my ($job, $message) = @_; $job->finish("$message Mojo!"); } ); $minion->add_task( bad_job => sub { my ($job, $message) = @_; die 'Bad job!'; } ); my $id = $minion->enqueue('good_job', ['Hello']); my $id2 = $minion->enqueue('bad_job', ['Hello']); while (my $job = $worker->dequeue(0)) { next unless my $err = $job->execute; $job->fail("Error: $err"); } $worker->unregister; my $job = $minion->job($id); is $job->info->{state}, 'finished', 'right state'; is $job->info->{result}, 'Hello Mojo!', 'right result'; my $job2 = $minion->job($id2); is $job2->info->{state}, 'failed', 'right state'; like $job2->info->{result}, qr/Error: Bad job!/, 'right error'; }; $minion->reset({all => 1}); done_testing(); prereqs.yml100644001750001750 37214143604471 20101 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6runtime: requires: perl: '5.010001' List::Util: 0 Minion: '10.13' Mojolicious: '7.49' Mojo::SQLite: '3.000' Sys::Hostname: 0 Time::HiRes: 0 suggests: Mojo::JSON::MaybeXS: 0 test: requires: Test::More: '0.96' CONTRIBUTING.md100644001750001750 1055514143604471 20172 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6# HOW TO CONTRIBUTE Thank you for considering contributing to this distribution. This file contains instructions that will help you work with the source code. The distribution is managed with [Dist::Zilla](https://metacpan.org/pod/Dist::Zilla). This means that many of the usual files you might expect are not in the repository, but are generated at release time. Some generated files are kept in the repository as a convenience (e.g. Build.PL/Makefile.PL and META.json). Generally, **you do not need Dist::Zilla to contribute patches**. You may need Dist::Zilla to create a tarball. See below for guidance. ## Getting dependencies If you have App::cpanminus 1.6 or later installed, you can use [cpanm](https://metacpan.org/pod/cpanm) to satisfy dependencies like this: $ cpanm --installdeps --with-develop . You can also run this command (or any other cpanm command) without installing App::cpanminus first, using the fatpacked `cpanm` script via curl or wget: $ curl -L https://cpanmin.us | perl - --installdeps --with-develop . $ wget -qO - https://cpanmin.us | perl - --installdeps --with-develop . Otherwise, look for either a `cpanfile`, `prereqs.json`/`prereqs.yml`, or `META.json` file for a list of dependencies to satisfy. ## Running tests You can run tests directly using the `prove` tool: $ prove -l $ prove -lv t/some_test_file.t For most of my distributions, `prove` is entirely sufficient for you to test any patches you have. I use `prove` for 99% of my testing during development. ## Code style and tidying Please try to match any existing coding style. If there is a `.perltidyrc` file, please install Perl::Tidy and use perltidy before submitting patches. ## Installing and using Dist::Zilla [Dist::Zilla](https://metacpan.org/pod/Dist::Zilla) is a very powerful authoring tool, optimized for maintaining a large number of distributions with a high degree of automation, but it has a large dependency chain, a bit of a learning curve and requires a number of author-specific plugins. To install it from CPAN, I recommend one of the following approaches for the quickest installation: # using CPAN.pm, but bypassing non-functional pod tests $ cpan TAP::Harness::Restricted $ PERL_MM_USE_DEFAULT=1 HARNESS_CLASS=TAP::Harness::Restricted cpan Dist::Zilla # using cpanm, bypassing *all* tests $ cpanm -n Dist::Zilla In either case, it's probably going to take about 10 minutes. Go for a walk, go get a cup of your favorite beverage, take a bathroom break, or whatever. When you get back, Dist::Zilla should be ready for you. Then you need to install any plugins specific to this distribution: $ dzil authordeps --missing | cpanm You can use Dist::Zilla to install the distribution's dependencies if you haven't already installed them with cpanm: $ dzil listdeps --missing --develop | cpanm You can instead combine these two steps into one command by installing Dist::Zilla::App::Command::installdeps then running: $ dzil installdeps Once everything is installed, here are some dzil commands you might try: $ dzil build $ dzil test $ dzil regenerate You can learn more about Dist::Zilla at http://dzil.org/ ## Other notes This distribution maintains the generated `META.json` and either `Makefile.PL` or `Build.PL` in the repository. This allows two things: [Travis CI](https://travis-ci.org/) can build and test the distribution without requiring Dist::Zilla, and the distribution can be installed directly from Github or a local git repository using `cpanm` for testing (again, not requiring Dist::Zilla). $ cpanm git://github.com/Author/Distribution-Name.git $ cd Distribution-Name; cpanm . Contributions are preferred in the form of a Github pull request. See [Using pull requests](https://help.github.com/articles/using-pull-requests/) for further information. You can use the Github issue tracker to report issues without an accompanying patch. # CREDITS This file was adapted from an initial `CONTRIBUTING.mkdn` file from David Golden under the terms of the [CC0](https://creativecommons.org/share-your-work/public-domain/cc0/), with inspiration from the contributing documents from [Dist::Zilla::Plugin::Author::KENTNL::CONTRIBUTING](https://metacpan.org/pod/Dist::Zilla::Plugin::Author::KENTNL::CONTRIBUTING) and [Dist::Zilla::PluginBundle::Author::ETHER](https://metacpan.org/pod/Dist::Zilla::PluginBundle::Author::ETHER). sqlite_worker.t100600001750001750 404214143604471 21225 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6/tuse Mojo::Base -strict; BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' } use Test::More; use Config; use Minion; plan skip_all => 'Minion workers do not support fork emulation' if $Config{d_pseudofork}; my $minion = Minion->new('SQLite'); subtest 'Basics' => sub { $minion->add_task( test => sub { my $job = shift; $job->finish({just => 'works!'}); } ); my $worker = $minion->worker; $worker->on( dequeue => sub { my ($worker, $job) = @_; $job->on(reap => sub { kill 'INT', $$ }); } ); my $id = $minion->enqueue('test'); my $max; $worker->once(wait => sub { $max = shift->status->{jobs} }); $worker->run; is $max, 4, 'right value'; is_deeply $minion->job($id)->info->{result}, {just => 'works!'}, 'right result'; }; subtest 'Signals' => sub { $minion->add_task( int => sub { my $job = shift; my $forever = 1; my %received; local $SIG{INT} = sub { $forever = 0 }; local $SIG{USR1} = sub { $received{usr1}++ }; local $SIG{USR2} = sub { $received{usr2}++ }; $job->minion->broadcast('kill', [$_, $job->id]) for qw(USR1 USR2 INT); while ($forever) { sleep 1 } $job->finish({msg => 'signals: ' . join(' ', sort keys %received)}); } ); my $worker = $minion->worker; $worker->status->{command_interval} = 1; $worker->on( dequeue => sub { my ($worker, $job) = @_; $job->on(reap => sub { kill 'INT', $$ }); } ); my $id = $minion->enqueue('int'); $worker->run; is_deeply $minion->job($id)->info->{result}, {msg => 'signals: usr1 usr2'}, 'right result'; my $status = $worker->status; is $status->{command_interval}, 1, 'right value'; is $status->{dequeue_timeout}, 5, 'right value'; is $status->{heartbeat_interval}, 300, 'right value'; is $status->{jobs}, 4, 'right value'; is_deeply $status->{queues}, ['default'], 'right structure'; is $status->{performed}, 1, 'right value'; ok $status->{repair_interval}, 'has a value'; }; $minion->reset({all => 1}); done_testing(); 00-report-prereqs.t100644001750001750 1347614143604471 21605 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6/t#!perl use strict; use warnings; # This test was generated by Dist::Zilla::Plugin::Test::ReportPrereqs 0.028 use Test::More tests => 1; use Module::Metadata; use File::Spec; # from $version::LAX my $lax_version_re = qr/(?: undef | (?: (?:[0-9]+) (?: \. | (?:\.[0-9]+) (?:_[0-9]+)? )? | (?:\.[0-9]+) (?:_[0-9]+)? ) | (?: v (?:[0-9]+) (?: (?:\.[0-9]+)+ (?:_[0-9]+)? )? | (?:[0-9]+)? (?:\.[0-9]+){2,} (?:_[0-9]+)? ) )/x; # hide optional CPAN::Meta modules from prereq scanner # and check if they are available my $cpan_meta = "CPAN::Meta"; my $cpan_meta_pre = "CPAN::Meta::Prereqs"; my $HAS_CPAN_META = eval "require $cpan_meta; $cpan_meta->VERSION('2.120900')" && eval "require $cpan_meta_pre"; ## no critic # Verify requirements? my $DO_VERIFY_PREREQS = 1; sub _max { my $max = shift; $max = ( $_ > $max ) ? $_ : $max for @_; return $max; } sub _merge_prereqs { my ($collector, $prereqs) = @_; # CPAN::Meta::Prereqs object if (ref $collector eq $cpan_meta_pre) { return $collector->with_merged_prereqs( CPAN::Meta::Prereqs->new( $prereqs ) ); } # Raw hashrefs for my $phase ( keys %$prereqs ) { for my $type ( keys %{ $prereqs->{$phase} } ) { for my $module ( keys %{ $prereqs->{$phase}{$type} } ) { $collector->{$phase}{$type}{$module} = $prereqs->{$phase}{$type}{$module}; } } } return $collector; } my @include = qw( ); my @exclude = qw( ); # Add static prereqs to the included modules list my $static_prereqs = do './t/00-report-prereqs.dd'; # Merge all prereqs (either with ::Prereqs or a hashref) my $full_prereqs = _merge_prereqs( ( $HAS_CPAN_META ? $cpan_meta_pre->new : {} ), $static_prereqs ); # Add dynamic prereqs to the included modules list (if we can) my ($source) = grep { -f } 'MYMETA.json', 'MYMETA.yml'; my $cpan_meta_error; if ( $source && $HAS_CPAN_META && (my $meta = eval { CPAN::Meta->load_file($source) } ) ) { $full_prereqs = _merge_prereqs($full_prereqs, $meta->prereqs); } else { $cpan_meta_error = $@; # capture error from CPAN::Meta->load_file($source) $source = 'static metadata'; } my @full_reports; my @dep_errors; my $req_hash = $HAS_CPAN_META ? $full_prereqs->as_string_hash : $full_prereqs; # Add static includes into a fake section for my $mod (@include) { $req_hash->{other}{modules}{$mod} = 0; } for my $phase ( qw(configure build test runtime develop other) ) { next unless $req_hash->{$phase}; next if ($phase eq 'develop' and not $ENV{AUTHOR_TESTING}); for my $type ( qw(requires recommends suggests conflicts modules) ) { next unless $req_hash->{$phase}{$type}; my $title = ucfirst($phase).' '.ucfirst($type); my @reports = [qw/Module Want Have/]; for my $mod ( sort keys %{ $req_hash->{$phase}{$type} } ) { next if $mod eq 'perl'; next if grep { $_ eq $mod } @exclude; my $file = $mod; $file =~ s{::}{/}g; $file .= ".pm"; my ($prefix) = grep { -e File::Spec->catfile($_, $file) } @INC; my $want = $req_hash->{$phase}{$type}{$mod}; $want = "undef" unless defined $want; $want = "any" if !$want && $want == 0; my $req_string = $want eq 'any' ? 'any version required' : "version '$want' required"; if ($prefix) { my $have = Module::Metadata->new_from_file( File::Spec->catfile($prefix, $file) )->version; $have = "undef" unless defined $have; push @reports, [$mod, $want, $have]; if ( $DO_VERIFY_PREREQS && $HAS_CPAN_META && $type eq 'requires' ) { if ( $have !~ /\A$lax_version_re\z/ ) { push @dep_errors, "$mod version '$have' cannot be parsed ($req_string)"; } elsif ( ! $full_prereqs->requirements_for( $phase, $type )->accepts_module( $mod => $have ) ) { push @dep_errors, "$mod version '$have' is not in required range '$want'"; } } } else { push @reports, [$mod, $want, "missing"]; if ( $DO_VERIFY_PREREQS && $type eq 'requires' ) { push @dep_errors, "$mod is not installed ($req_string)"; } } } if ( @reports ) { push @full_reports, "=== $title ===\n\n"; my $ml = _max( map { length $_->[0] } @reports ); my $wl = _max( map { length $_->[1] } @reports ); my $hl = _max( map { length $_->[2] } @reports ); if ($type eq 'modules') { splice @reports, 1, 0, ["-" x $ml, "", "-" x $hl]; push @full_reports, map { sprintf(" %*s %*s\n", -$ml, $_->[0], $hl, $_->[2]) } @reports; } else { splice @reports, 1, 0, ["-" x $ml, "-" x $wl, "-" x $hl]; push @full_reports, map { sprintf(" %*s %*s %*s\n", -$ml, $_->[0], $wl, $_->[1], $hl, $_->[2]) } @reports; } push @full_reports, "\n"; } } } if ( @full_reports ) { diag "\nVersions for all modules listed in $source (including optional ones):\n\n", @full_reports; } if ( $cpan_meta_error || @dep_errors ) { diag "\n*** WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING ***\n"; } if ( $cpan_meta_error ) { my ($orig_source) = grep { -f } 'MYMETA.json', 'MYMETA.yml'; diag "\nCPAN::Meta->load_file('$orig_source') failed with: $cpan_meta_error\n"; } if ( @dep_errors ) { diag join("\n", "\nThe following REQUIRED prerequisites were not satisfied:\n", @dep_errors, "\n" ); } pass('Reported prereqs'); # vim: ts=4 sts=4 sw=4 et: author000755001750001750 014143604471 17510 5ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6/xtpod-syntax.t100644001750001750 25214143604471 22122 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6/xt/author#!perl # This file was automatically generated by Dist::Zilla::Plugin::PodSyntaxTests. use strict; use warnings; use Test::More; use Test::Pod 1.41; all_pod_files_ok(); 00-report-prereqs.dd100644001750001750 321014143604471 21672 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6/tdo { my $x = { 'configure' => { 'requires' => { 'Module::Build::Tiny' => '0.034' } }, 'develop' => { 'requires' => { 'Pod::Coverage::TrustPod' => '0', 'Test::Pod' => '1.41', 'Test::Pod::Coverage' => '1.08' } }, 'runtime' => { 'requires' => { 'List::Util' => '0', 'Minion' => '10.13', 'Mojo::SQLite' => '3.000', 'Mojolicious' => '7.49', 'Sys::Hostname' => '0', 'Time::HiRes' => '0', 'perl' => '5.010001' }, 'suggests' => { 'Mojo::JSON::MaybeXS' => '0' } }, 'test' => { 'recommends' => { 'CPAN::Meta' => '2.120900' }, 'requires' => { 'File::Spec' => '0', 'Module::Metadata' => '0', 'Test::More' => '0.96' } } }; $x; }pod-coverage.t100644001750001750 33414143604471 22370 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6/xt/author#!perl # This file was automatically generated by Dist::Zilla::Plugin::PodCoverageTests. use Test::Pod::Coverage 1.08; use Pod::Coverage::TrustPod; all_pod_coverage_ok({ coverage_class => 'Pod::Coverage::TrustPod' }); examples000755001750001750 014143604471 17371 5ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6minion_bench.pl100644001750001750 656614143604471 22533 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6/examplesuse Mojo::Base -strict; use Minion; use Mojo::File 'tempdir'; use Mojo::URL; use Time::HiRes 'time'; my $ENQUEUE = 10000; my $DEQUEUE = 1000; my $REPETITIONS = 2; my $WORKERS = 4; my $INFO = 100; my $STATS = 100; my $REPAIR = 100; my $LOCK = 1000; my $UNLOCK = 1000; my $tempdir = tempdir; my $url = Mojo::URL->new->scheme('sqlite')->path($tempdir->child('temp.db')); { # XXX: Scope minion object for cleanup of tempdir # A benchmark script for comparing backends and evaluating the performance # impact of proposed changes my $minion = Minion->new(SQLite => $url); $minion->add_task(foo => sub { }); $minion->add_task(bar => sub { }); $minion->reset({all => 1}); # Enqueue say "Clean start with $ENQUEUE jobs"; my @parents = map { $minion->enqueue('foo') } 1 .. 5; my $before = time; $minion->enqueue($_ % 2 ? 'foo' : 'bar' => [] => {parents => \@parents}) for 1 .. $ENQUEUE; my $elapsed = time - $before; my $avg = sprintf '%.3f', $ENQUEUE / $elapsed; say "Enqueued $ENQUEUE jobs in $elapsed seconds ($avg/s)"; #$minion->backend->sqlite->db->query('analyze minion_jobs'); # XXX: disconnect open database handle before forking to prevent database corruption $minion = Minion->new(SQLite => $url); $minion->add_task(foo => sub { }); $minion->add_task(bar => sub { }); # Dequeue sub dequeue { my @pids; for (1 .. $WORKERS) { die "Couldn't fork: $!" unless defined(my $pid = fork); unless ($pid) { my $worker = $minion->repair->worker->register; say "$$ will finish $DEQUEUE jobs"; my $before = time; $worker->dequeue(0.5)->finish for 1 .. $DEQUEUE; my $elapsed = time - $before; my $avg = sprintf '%.3f', $DEQUEUE / $elapsed; say "$$ finished $DEQUEUE jobs in $elapsed seconds ($avg/s)"; $worker->unregister; exit; } push @pids, $pid; } say "$$ has started $WORKERS workers"; my $before = time; waitpid $_, 0 for @pids; my $elapsed = time - $before; my $avg = sprintf '%.3f', ($DEQUEUE * $WORKERS) / $elapsed; say "$WORKERS workers finished $DEQUEUE jobs each in $elapsed seconds ($avg/s)"; } dequeue() for 1 .. $REPETITIONS; # Job info say "Requesting job info $INFO times"; $before = time; my $backend = $minion->backend; $backend->list_jobs(0, 1, {ids => [$_]}) for 1 .. $INFO; $elapsed = time - $before; $avg = sprintf '%.3f', $INFO / $elapsed; say "Received job info $INFO times in $elapsed seconds ($avg/s)"; # Stats say "Requesting stats $STATS times"; $before = time; $minion->stats for 1 .. $STATS; $elapsed = time - $before; $avg = sprintf '%.3f', $STATS / $elapsed; say "Received stats $STATS times in $elapsed seconds ($avg/s)"; # Repair say "Repairing $REPAIR times"; $before = time; $minion->repair for 1 .. $REPAIR; $elapsed = time - $before; $avg = sprintf '%.3f', $REPAIR / $elapsed; say "Repaired $REPAIR times in $elapsed seconds ($avg/s)"; # Lock say "Acquiring locks $LOCK times"; $before = time; $minion->lock($_ % 2 ? 'foo' : 'bar', 3600, {limit => int($LOCK / 2)}) for 1 .. $LOCK; $elapsed = time - $before; $avg = sprintf '%.3f', $LOCK / $elapsed; say "Acquired locks $LOCK times in $elapsed seconds ($avg/s)"; # Unlock say "Releasing locks $UNLOCK times"; $before = time; $minion->unlock($_ % 2 ? 'foo' : 'bar') for 1 .. $UNLOCK; $elapsed = time - $before; $avg = sprintf '%.3f', $UNLOCK / $elapsed; say "Releasing locks $UNLOCK times in $elapsed seconds ($avg/s)"; } sqlite_admin.t100644001750001750 1413414143604471 22531 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6/xt/authoruse Mojo::Base -strict; BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' } use Test::More; use Mojolicious::Lite; use Mojo::SQLite; use Test::Mojo; my $sql = Mojo::SQLite->new; plugin Minion => {SQLite => $sql}; app->minion->add_task(test => sub { }); my $finished = app->minion->enqueue('test'); app->minion->perform_jobs; my $inactive = app->minion->enqueue('test'); get '/home' => 'test_home'; plugin 'Minion::Admin'; my $t = Test::Mojo->new; subtest 'Dashboard' => sub { $t->get_ok('/minion')->status_is(200)->content_like(qr/Dashboard/)->element_exists('a[href=/]'); }; subtest 'Stats' => sub { $t->get_ok('/minion/stats')->status_is(200)->json_is('/active_jobs' => 0) ->json_is('/active_locks' => 0)->json_is('/active_workers' => 0) ->json_is('/delayed_jobs' => 0)->json_is('/enqueued_jobs' => 2) ->json_is('/failed_jobs' => 0)->json_is('/finished_jobs' => 1) ->json_is('/inactive_jobs' => 1)->json_is('/inactive_workers' => 0) ->json_has('/uptime'); }; subtest 'Jobs' => sub { $t->get_ok('/minion/jobs?state=inactive')->status_is(200) ->text_like('tbody td a' => qr/$inactive/)->text_unlike('tbody td a' => qr/$finished/); $t->get_ok('/minion/jobs?state=finished')->status_is(200) ->text_like('tbody td a' => qr/$finished/)->text_unlike('tbody td a' => qr/$inactive/); }; subtest 'Workers' => sub { $t->get_ok('/minion/workers')->status_is(200)->element_exists_not('tbody td a'); my $worker = app->minion->worker->register; $t->get_ok('/minion/workers')->status_is(200)->element_exists('tbody td a') ->text_like('tbody td a' => qr/@{[$worker->id]}/); $worker->unregister; $t->get_ok('/minion/workers')->status_is(200)->element_exists_not('tbody td a'); }; subtest 'Locks' => sub { $t->app->minion->lock('foo', 3600); $t->app->minion->lock('bar', 3600); $t->ua->max_redirects(5); $t->get_ok('/minion/locks')->status_is(200)->text_like('tbody td a' => qr/bar/); $t->get_ok('/minion/locks?name=foo')->status_is(200)->text_like('tbody td a' => qr/foo/); $t->post_ok('/minion/locks?_method=DELETE&name=bar')->status_is(200) ->text_like('tbody td a' => qr/foo/) ->text_like('.alert-success', qr/All selected named locks released/); is $t->tx->previous->res->code, 302, 'right status'; like $t->tx->previous->res->headers->location, qr/locks/, 'right "Location" value'; $t->post_ok('/minion/locks?_method=DELETE&name=foo')->status_is(200) ->element_exists_not('tbody td a') ->text_like('.alert-success', qr/All selected named locks released/); is $t->tx->previous->res->code, 302, 'right status'; like $t->tx->previous->res->headers->location, qr/locks/, 'right "Location" value'; }; subtest 'Manage jobs' => sub { is app->minion->job($finished)->info->{state}, 'finished', 'right state'; $t->post_ok('/minion/jobs?_method=PATCH' => form => {id => $finished, do => 'retry'}) ->text_like('.alert-success', qr/All selected jobs retried/); is $t->tx->previous->res->code, 302, 'right status'; like $t->tx->previous->res->headers->location, qr/id=$finished/, 'right "Location" value'; is app->minion->job($finished)->info->{state}, 'inactive', 'right state'; $t->post_ok('/minion/jobs?_method=PATCH' => form => {id => $finished, do => 'stop'}) ->text_like('.alert-info', qr/Trying to stop all selected jobs/); is $t->tx->previous->res->code, 302, 'right status'; like $t->tx->previous->res->headers->location, qr/id=$finished/, 'right "Location" value'; $t->post_ok('/minion/jobs?_method=PATCH' => form => {id => $finished, do => 'remove'}) ->text_like('.alert-success', qr/All selected jobs removed/); is $t->tx->previous->res->code, 302, 'right status'; like $t->tx->previous->res->headers->location, qr/id=$finished/, 'right "Location" value'; is app->minion->job($finished), undef, 'job has been removed'; }; subtest 'Bundled static files' => sub { $t->get_ok('/minion/bootstrap/bootstrap.js')->status_is(200)->content_type_is('application/javascript'); $t->get_ok('/minion/bootstrap/bootstrap.css')->status_is(200)->content_type_is('text/css'); $t->get_ok('/minion/d3/d3.js')->status_is(200)->content_type_is('application/javascript'); $t->get_ok('/minion/epoch/epoch.js')->status_is(200)->content_type_is('application/javascript'); $t->get_ok('/minion/epoch/epoch.css')->status_is(200)->content_type_is('text/css'); $t->get_ok('/minion/fontawesome/fontawesome.css')->status_is(200)->content_type_is('text/css'); $t->get_ok('/minion/webfonts/fa-brands-400.eot')->status_is(200); $t->get_ok('/minion/webfonts/fa-brands-400.svg')->status_is(200); $t->get_ok('/minion/webfonts/fa-brands-400.ttf')->status_is(200); $t->get_ok('/minion/webfonts/fa-brands-400.woff')->status_is(200); $t->get_ok('/minion/webfonts/fa-brands-400.woff2')->status_is(200); $t->get_ok('/minion/webfonts/fa-regular-400.eot')->status_is(200); $t->get_ok('/minion/webfonts/fa-regular-400.svg')->status_is(200); $t->get_ok('/minion/webfonts/fa-regular-400.ttf')->status_is(200); $t->get_ok('/minion/webfonts/fa-regular-400.woff')->status_is(200); $t->get_ok('/minion/webfonts/fa-regular-400.woff2')->status_is(200); $t->get_ok('/minion/webfonts/fa-solid-900.eot')->status_is(200); $t->get_ok('/minion/webfonts/fa-solid-900.svg')->status_is(200); $t->get_ok('/minion/webfonts/fa-solid-900.ttf')->status_is(200); $t->get_ok('/minion/webfonts/fa-solid-900.woff')->status_is(200); $t->get_ok('/minion/webfonts/fa-solid-900.woff2')->status_is(200); $t->get_ok('/minion/moment/moment.js')->status_is(200)->content_type_is('application/javascript'); $t->get_ok('/minion/app.js')->status_is(200)->content_type_is('application/javascript'); $t->get_ok('/minion/app.css')->status_is(200)->content_type_is('text/css'); $t->get_ok('/minion/logo-black-2x.png')->status_is(200)->content_type_is('image/png'); $t->get_ok('/minion/logo-black.png')->status_is(200)->content_type_is('image/png'); }; subtest 'Different prefix and return route' => sub { plugin 'Minion::Admin' => {route => app->routes->any('/also_minion'), return_to => 'test_home'}; $t->get_ok('/also_minion')->status_is(200)->content_like(qr/Dashboard/)->element_exists('a[href=/home]'); }; done_testing(); sqlite_lite_app.t100644001750001750 306114143604471 23213 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6/xt/authoruse Mojo::Base -strict; BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' } use Test::More; use Mojo::IOLoop; use Mojo::SQLite; use Mojolicious::Lite; use Test::Mojo; my $sql = Mojo::SQLite->new; plugin Minion => {SQLite => $sql}; app->minion->add_task( add => sub { my ($job, $first, $second) = @_; Mojo::IOLoop->next_tick(sub { $job->finish($first + $second); Mojo::IOLoop->stop; }); Mojo::IOLoop->start; } ); get '/add' => sub { my $c = shift; my $id = $c->minion->enqueue(add => [$c->param('first'), $c->param('second')] => { queue => 'test'}); $c->render(text => $id); }; get '/result' => sub { my $c = shift; $c->render(text => $c->minion->job($c->param('id'))->info->{result}); }; my $t = Test::Mojo->new; subtest 'Perform jobs automatically' => sub { $t->get_ok('/add' => form => {first => 1, second => 2})->status_is(200); $t->app->minion->perform_jobs({queues => ['test']}); $t->get_ok('/result' => form => {id => $t->tx->res->text})->status_is(200)->content_is('3'); $t->get_ok('/add' => form => {first => 2, second => 3})->status_is(200); my $first = $t->tx->res->text; $t->get_ok('/add' => form => {first => 4, second => 5})->status_is(200); my $second = $t->tx->res->text; Mojo::Promise->new->resolve->then(sub { $t->app->minion->perform_jobs({queues => ['test']}) })->wait; $t->get_ok('/result' => form => {id => $first})->status_is(200)->content_is('5'); $t->get_ok('/result' => form => {id => $second})->status_is(200)->content_is('9'); }; $t->app->minion->reset({all => 1}); done_testing(); Backend000755001750001750 014143604471 21101 5ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6/lib/MinionSQLite.pm100644001750001750 10440714143604471 23006 0ustar00grinnzgrinnz000000000000Minion-Backend-SQLite-v5.0.6/lib/Minion/Backendpackage Minion::Backend::SQLite; use Mojo::Base 'Minion::Backend'; use Carp 'croak'; use List::Util 'min'; use Mojo::SQLite; use Mojo::Util 'steady_time'; use Sys::Hostname 'hostname'; use Time::HiRes 'usleep'; our $VERSION = 'v5.0.6'; has dequeue_interval => 0.5; has 'sqlite'; sub new { my $self = shift->SUPER::new(sqlite => Mojo::SQLite->new(@_)); $self->sqlite->auto_migrate(1)->migrations->name('minion')->from_data; return $self; } sub broadcast { my ($self, $command, $args, $ids) = (shift, shift, shift || [], shift || []); my $ids_in = join ',', ('?')x@$ids; return !!$self->sqlite->db->query( q{update minion_workers set inbox = json_set(inbox, '$[' || json_array_length(inbox) || ']', json(?))} . (@$ids ? " where id in ($ids_in)" : ''), {json => [$command, @$args]}, @$ids )->rows; } sub dequeue { my ($self, $id, $wait, $options) = @_; my $job = $self->_try($id, $options); unless ($job) { my $int = $self->dequeue_interval; my $end = steady_time + $wait; my $remaining = $wait; usleep(min($int, $remaining) * 1000000) until ($remaining = $end - steady_time) <= 0 or $job = $self->_try($id, $options); } return $job || $self->_try($id, $options); } sub enqueue { my ($self, $task, $args, $options) = (shift, shift, shift || [], shift || {}); return $self->sqlite->db->query( q{insert into minion_jobs (args, attempts, delayed, expires, lax, notes, parents, priority, queue, task) values (?, ?, datetime('now', ? || ' seconds'), case when ? is not null then datetime('now', ? || ' seconds') end, ?, ?, ?, ?, ?, ?)}, {json => $args}, $options->{attempts} // 1, $options->{delay} // 0, @$options{qw(expire expire)}, $options->{lax} ? 1 : 0, {json => $options->{notes} || {}}, {json => ($options->{parents} || [])}, $options->{priority} // 0, $options->{queue} // 'default', $task )->last_insert_id; } sub fail_job { shift->_update(1, @_) } sub finish_job { shift->_update(0, @_) } sub history { my $self = shift; my $db = $self->sqlite->db; my $steps = $db->query( q{with recursive generate_series(ts) as ( select datetime('now','-23 hours') union all select datetime(ts,'+1 hour') from generate_series where datetime(ts,'+1 hour') <= datetime('now') ) select ts, strftime('%s',ts) as epoch, strftime('%d',ts,'localtime') as day, strftime('%H',ts,'localtime') as hour from generate_series order by epoch})->hashes; my $counts = $db->query( q{select strftime('%d',finished,'localtime') as day, strftime('%H',finished,'localtime') as hour, count(case state when 'failed' then 1 end) as failed_jobs, count(case state when 'finished' then 1 end) as finished_jobs from minion_jobs where finished > ? group by day, hour}, $steps->first->{ts})->hashes; my %daily = map { ("$_->{day}-$_->{hour}" => $_) } @$counts; my @daily_ordered; foreach my $step (@$steps) { my $hour_counts = $daily{"$step->{day}-$step->{hour}"} // {}; push @daily_ordered, { epoch => $step->{epoch}, failed_jobs => $hour_counts->{failed_jobs} // 0, finished_jobs => $hour_counts->{finished_jobs} // 0, }; } return {daily => \@daily_ordered}; } sub list_jobs { my ($self, $offset, $limit, $options) = @_; my (@where, @where_params); if (defined(my $before = $options->{before})) { push @where, 'id < ?'; push @where_params, $before; } if (defined(my $ids = $options->{ids})) { my $ids_in = join ',', ('?')x@$ids; push @where, @$ids ? "id in ($ids_in)" : 'id is null'; push @where_params, @$ids; } if (defined(my $notes = $options->{notes})) { croak 'Listing jobs by existence of notes is unimplemented'; } if (defined(my $queues = $options->{queues})) { my $queues_in = join ',', ('?')x@$queues; push @where, @$queues ? "queue in ($queues_in)" : 'queue is null'; push @where_params, @$queues; } if (defined(my $states = $options->{states})) { my $states_in = join ',', ('?')x@$states; push @where, @$states ? "state in ($states_in)" : 'state is null'; push @where_params, @$states; } if (defined(my $tasks = $options->{tasks})) { my $tasks_in = join ',', ('?')x@$tasks; push @where, @$tasks ? "task in ($tasks_in)" : 'task is null'; push @where_params, @$tasks; } push @where, q{(state != 'inactive' or expires is null or expires > datetime('now'))}; my $where_str = @where ? 'where ' . join(' and ', @where) : ''; my $jobs = $self->sqlite->db->query( qq{select id, args, attempts, (select json_group_array(distinct child.id) from minion_jobs as child, json_each(child.parents) as parent_id where j.id = parent_id.value) as children, strftime('%s',created) as created, strftime('%s',delayed) as delayed, strftime('%s',expires) as expires, strftime('%s',finished) as finished, lax, notes, parents, priority, queue, result, strftime('%s',retried) as retried, retries, strftime('%s',started) as started, state, task, strftime('%s','now') as time, worker from minion_jobs as j $where_str order by id desc limit ? offset ?}, @where_params, $limit, $offset )->expand(json => [qw(args children notes parents result)])->hashes->to_array; my $total = $self->sqlite->db->query(qq{select count(*) from minion_jobs as j $where_str}, @where_params)->arrays->first->[0]; return {jobs => $jobs, total => $total}; } sub list_locks { my ($self, $offset, $limit, $options) = @_; my (@where, @where_params); push @where, q{expires > datetime('now')}; if (defined(my $names = $options->{names})) { my $names_in = join ',', ('?')x@$names; push @where, @$names ? "name in ($names_in)" : 'name is null'; push @where_params, @$names; } my $where_str = 'where ' . join(' and ', @where); my $locks = $self->sqlite->db->query( qq{select name, strftime('%s',expires) as expires from minion_locks $where_str order by id desc limit ? offset ?}, @where_params, $limit, $offset )->hashes->to_array; my $total = $self->sqlite->db->query(qq{select count(*) from minion_locks $where_str}, @where_params)->arrays->first->[0]; return {locks => $locks, total => $total}; } sub list_workers { my ($self, $offset, $limit, $options) = @_; my (@where, @where_params); if (defined(my $before = $options->{before})) { push @where, 'w.id < ?'; push @where_params, $before; } if (defined(my $ids = $options->{ids})) { my $ids_in = join ',', ('?')x@$ids; push @where, @$ids ? "w.id in ($ids_in)" : 'w.id is null'; push @where_params, @$ids; } my $where_str = @where ? 'where ' . join(' and ', @where) : ''; my $workers = $self->sqlite->db->query( qq{select w.id, strftime('%s',w.notified) as notified, group_concat(j.id) as jobs, w.host, w.pid, w.status, strftime('%s',w.started) as started from minion_workers as w left join minion_jobs as j on j.worker = w.id and j.state = 'active' $where_str group by w.id order by w.id desc limit ? offset ?}, @where_params, $limit, $offset )->expand(json => 'status')->hashes->to_array; $_->{jobs} = [split /,/, ($_->{jobs} // '')] for @$workers; my $total = $self->sqlite->db->query(qq{select count(*) from minion_workers as w $where_str}, @where_params)->arrays->first->[0]; return {total => $total, workers => $workers}; } sub lock { my ($self, $name, $duration, $options) = (shift, shift, shift, shift // {}); my $db = $self->sqlite->db; $db->query(q{delete from minion_locks where expires < datetime('now')}); my $tx = $db->begin('exclusive'); my $locks = $db->query(q{select count(*) from minion_locks where name = ?}, $name)->arrays->first->[0]; return !!0 if defined $locks and $locks >= ($options->{limit} || 1); if (defined $duration and $duration > 0) { $db->query(q{insert into minion_locks (name, expires) values (?, datetime('now', ? || ' seconds'))}, $name, $duration); $tx->commit; } return !!1; } sub note { my ($self, $id, $merge) = @_; my (@set, @set_params, @remove, @remove_params); foreach my $key (keys %$merge) { croak qq{Invalid note key '$key'; must not contain '.', '[', or ']'} if $key =~ m/[\[\].]/; if (defined $merge->{$key}) { push @set, q{'$.' || ?}, 'json(?)'; push @set_params, $key, {json => $merge->{$key}}; } else { push @remove, q{'$.' || ?}; push @remove_params, $key; } } my $json_set = join ', ', @set; my $json_remove = join ', ', @remove; my $set_to = 'notes'; $set_to = "json_set($set_to, $json_set)" if @set; $set_to = "json_remove($set_to, $json_remove)" if @remove; return !!$self->sqlite->db->query( qq{update minion_jobs set notes = $set_to where id = ?}, @set_params, @remove_params, $id )->rows; } sub receive { my ($self, $id) = @_; my $db = $self->sqlite->db; my $tx = $db->begin; my $array = $db->query(q{select inbox from minion_workers where id = ?}, $id) ->expand(json => 'inbox')->array; $db->query(q{update minion_workers set inbox = '[]' where id = ?}, $id) if $array; $tx->commit; return $array ? $array->[0] : []; } sub register_worker { my ($self, $id, $options) = (shift, shift, shift || {}); return $id if $id && $self->sqlite->db->query( q{update minion_workers set notified = datetime('now'), status = ? where id = ?}, {json => $options->{status} // {}}, $id)->rows; return $self->sqlite->db->query( q{insert into minion_workers (host, pid, status) values (?, ?, ?)}, hostname, $$, {json => $options->{status} // {}})->last_insert_id; } sub remove_job { !!shift->sqlite->db->query( q{delete from minion_jobs where id = ? and state in ('inactive', 'failed', 'finished')}, shift )->rows; } sub repair { my $self = shift; # Workers without heartbeat my $db = $self->sqlite->db; my $minion = $self->minion; $db->query( q{delete from minion_workers where notified < datetime('now', '-' || ? || ' seconds')}, $minion->missing_after ); # Old jobs with no unresolved dependencies and expired jobs $db->query( q{delete from minion_jobs where (finished <= datetime('now', '-' || ? || ' seconds') and state = 'finished' and id not in (select distinct parent_id.value from minion_jobs as child, json_each(child.parents) as parent_id where child.state <> 'finished')) or (expires <= datetime('now') and state = 'inactive')}, $minion->remove_after); # Jobs with missing worker (can be retried) my $fail = $db->query( q{select id, retries from minion_jobs as j where state = 'active' and queue != 'minion_foreground' and not exists (select 1 from minion_workers where id = j.worker)} )->hashes; $fail->each(sub { $self->fail_job(@$_{qw(id retries)}, 'Worker went away') }); # Jobs in queue without workers or not enough workers (cannot be retried and requires admin attention) $db->query( q{update minion_jobs set state = 'failed', result = json_quote('Job appears stuck in queue') where state = 'inactive' and delayed < datetime('now', '-' || ? || ' seconds')}, $minion->stuck_after); } sub reset { my ($self, $options) = (shift, shift // {}); my $db = $self->sqlite->db; if ($options->{all}) { my $tx = $db->begin; $db->query('delete from minion_jobs'); $db->query('delete from minion_locks'); $db->query('delete from minion_workers'); $db->query(q{delete from sqlite_sequence where name in ('minion_jobs','minion_locks','minion_workers')}); $tx->commit; } elsif ($options->{locks}) { $db->query('delete from minion_locks'); } } sub retry_job { my ($self, $id, $retries, $options) = (shift, shift, shift, shift || {}); my $parents = defined $options->{parents} ? {json => $options->{parents}} : undef; return !!$self->sqlite->db->query( q{update minion_jobs set attempts = coalesce(?, attempts), delayed = datetime('now', ? || ' seconds'), expires = case when ? is not null then datetime('now', ? || ' seconds') else expires end, lax = coalesce(?, lax), parents = coalesce(?, parents), priority = coalesce(?, priority), queue = coalesce(?, queue), retried = datetime('now'), retries = retries + 1, state = 'inactive' where id = ? and retries = ?}, $options->{attempts}, $options->{delay} // 0, @$options{qw(expire expire)}, exists $options->{lax} ? $options->{lax} ? 1 : 0 : undef, $parents, @$options{qw(priority queue)}, $id, $retries )->rows; } sub stats { my $self = shift; my $stats = $self->sqlite->db->query( q{select (select count(*) from minion_jobs where state = 'inactive' and (expires is null or expires > datetime('now'))) as inactive_jobs, (select count(*) from minion_jobs where state = 'active') as active_jobs, (select count(*) from minion_jobs where state = 'failed') as failed_jobs, (select count(*) from minion_jobs where state = 'finished') as finished_jobs, (select count(*) from minion_jobs where state = 'inactive' and delayed > datetime('now')) as delayed_jobs, (select count(*) from minion_locks where expires > datetime('now')) as active_locks, (select count(distinct(worker)) from minion_jobs where state = 'active') as active_workers, ifnull((select seq from sqlite_sequence where name = 'minion_jobs'), 0) as enqueued_jobs, (select count(*) from minion_workers) as inactive_workers, null as uptime} )->hash; $stats->{inactive_workers} -= $stats->{active_workers}; return $stats; } sub unlock { !!shift->sqlite->db->query( q{delete from minion_locks where id = ( select id from minion_locks where expires > datetime('now') and name = ? order by expires limit 1)}, shift )->rows; } sub unregister_worker { shift->sqlite->db->query('delete from minion_workers where id = ?', shift); } sub _try { my ($self, $id, $options) = @_; my $db = $self->sqlite->db; my $queues = $options->{queues} || ['default']; my $tasks = [keys %{$self->minion->tasks}]; return undef unless @$queues and @$tasks; my $queues_in = join ',', ('?')x@$queues; my $tasks_in = join ',', ('?')x@$tasks; my $tx = $db->begin; my $res = $db->query( qq{select id from minion_jobs as j where delayed <= datetime('now') and id = coalesce(?, id) and (json_array_length(parents) = 0 or not exists ( select 1 from minion_jobs as parent, json_each(j.parents) as parent_id where parent.id = parent_id.value and case parent.state when 'active' then 1 when 'failed' then not j.lax when 'inactive' then (parent.expires is null or parent.expires > datetime('now')) end )) and priority >= coalesce(?, priority) and queue in ($queues_in) and state = 'inactive' and task in ($tasks_in) and (expires is null or expires > datetime('now')) order by priority desc, id limit 1}, $options->{id}, $options->{min_priority}, @$queues, @$tasks ); my $job_id = ($res->arrays->first // [])->[0] // return undef; $db->query( q{update minion_jobs set started = datetime('now'), state = 'active', worker = ? where id = ?}, $id, $job_id ); $tx->commit; my $info = $db->query( 'select id, args, retries, task from minion_jobs where id = ?', $job_id )->expand(json => 'args')->hash // return undef; return $info; } sub _update { my ($self, $fail, $id, $retries, $result) = @_; my $db = $self->sqlite->db; return undef unless $db->query( q{update minion_jobs set finished = datetime('now'), result = ?, state = ? where id = ? and retries = ? and state = 'active'}, {json => $result}, $fail ? 'failed' : 'finished', $id, $retries )->rows > 0; my $row = $db->query('select attempts from minion_jobs where id = ?', $id)->array; return $fail ? $self->auto_retry_job($id, $retries, $row->[0]) : 1; } 1; =encoding utf8 =head1 NAME Minion::Backend::SQLite - SQLite backend for Minion job queue =head1 SYNOPSIS use Minion::Backend::SQLite; my $backend = Minion::Backend::SQLite->new('sqlite:test.db'); # Minion use Minion; my $minion = Minion->new(SQLite => 'sqlite:test.db'); # Mojolicious (via Mojolicious::Plugin::Minion) $self->plugin(Minion => { SQLite => 'sqlite:test.db' }); # Mojolicious::Lite (via Mojolicious::Plugin::Minion) plugin Minion => { SQLite => 'sqlite:test.db' }; # Share the database connection cache helper sqlite => sub { state $sqlite = Mojo::SQLite->new('sqlite:test.db') }; plugin Minion => { SQLite => app->sqlite }; =head1 DESCRIPTION L is a backend for L based on L. All necessary tables will be created automatically with a set of migrations named C. If no connection string or C<:temp:> is provided, the database will be created in a temporary directory. =head1 ATTRIBUTES L inherits all attributes from L and implements the following new ones. =head2 dequeue_interval my $seconds = $backend->dequeue_interval; $backend = $backend->dequeue_interval($seconds); Interval in seconds between L attempts. Defaults to C<0.5>. =head2 sqlite my $sqlite = $backend->sqlite; $backend = $backend->sqlite(Mojo::SQLite->new); L object used to store all data. =head1 METHODS L inherits all methods from L and implements the following new ones. =head2 new my $backend = Minion::Backend::SQLite->new; my $backend = Minion::Backend::SQLite->new(':temp:'); my $backend = Minion::Backend::SQLite->new('sqlite:test.db'); my $backend = Minion::Backend::SQLite->new->tap(sub { $_->sqlite->from_filename('C:\\foo\\bar.db') }); my $backend = Minion::Backend::SQLite->new(Mojo::SQLite->new); Construct a new L object. =head2 broadcast my $bool = $backend->broadcast('some_command'); my $bool = $backend->broadcast('some_command', [@args]); my $bool = $backend->broadcast('some_command', [@args], [$id1, $id2, $id3]); Broadcast remote control command to one or more workers. =head2 dequeue my $job_info = $backend->dequeue($worker_id, 0.5); my $job_info = $backend->dequeue($worker_id, 0.5, {queues => ['important']}); Wait a given amount of time in seconds for a job, dequeue it and transition from C to C state, or return C if queues were empty. Jobs will be checked for in intervals defined by L until the timeout is reached. These options are currently available: =over 2 =item id id => '10023' Dequeue a specific job. =item min_priority min_priority => 3 Do not dequeue jobs with a lower priority. =item queues queues => ['important'] One or more queues to dequeue jobs from, defaults to C. =back These fields are currently available: =over 2 =item args args => ['foo', 'bar'] Job arguments. =item id id => '10023' Job ID. =item retries retries => 3 Number of times job has been retried. =item task task => 'foo' Task name. =back =head2 enqueue my $job_id = $backend->enqueue('foo'); my $job_id = $backend->enqueue(foo => [@args]); my $job_id = $backend->enqueue(foo => [@args] => {priority => 1}); Enqueue a new job with C state. These options are currently available: =over 2 =item attempts attempts => 25 Number of times performing this job will be attempted, with a delay based on L after the first attempt, defaults to C<1>. =item delay delay => 10 Delay job for this many seconds (from now). =item expire expire => 300 Job is valid for this many seconds (from now) before it expires. Note that this option is B and might change without warning! =item lax lax => 1 Existing jobs this job depends on may also have transitioned to the C state to allow for it to be processed, defaults to C. Note that this option is B and might change without warning! =item notes notes => {foo => 'bar', baz => [1, 2, 3]} Hash reference with arbitrary metadata for this job. =item parents parents => [$id1, $id2, $id3] One or more existing jobs this job depends on, and that need to have transitioned to the state C before it can be processed. =item priority priority => 5 Job priority, defaults to C<0>. Jobs with a higher priority get performed first. =item queue queue => 'important' Queue to put job in, defaults to C. =back =head2 fail_job my $bool = $backend->fail_job($job_id, $retries); my $bool = $backend->fail_job($job_id, $retries, 'Something went wrong!'); my $bool = $backend->fail_job( $job_id, $retries, {msg => 'Something went wrong!'}); Transition from C to C state with or without a result, and if there are attempts remaining, transition back to C with an exponentially increasing delay based on L. =head2 finish_job my $bool = $backend->finish_job($job_id, $retries); my $bool = $backend->finish_job($job_id, $retries, 'All went well!'); my $bool = $backend->finish_job($job_id, $retries, {msg => 'All went well!'}); Transition from C to C state with or without a result. =head2 history my $history = $backend->history; Get history information for job queue. These fields are currently available: =over 2 =item daily daily => [{epoch => 12345, finished_jobs => 95, failed_jobs => 2}, ...] Hourly counts for processed jobs from the past day. =back =head2 list_jobs my $results = $backend->list_jobs($offset, $limit); my $results = $backend->list_jobs($offset, $limit, {states => ['inactive']}); Returns the information about jobs in batches. # Get the total number of results (without limit) my $num = $backend->list_jobs(0, 100, {queues => ['important']})->{total}; # Check job state my $results = $backend->list_jobs(0, 1, {ids => [$job_id]}); my $state = $results->{jobs}[0]{state}; # Get job result my $results = $backend->list_jobs(0, 1, {ids => [$job_id]}); my $result = $results->{jobs}[0]{result}; These options are currently available: =over 2 =item before before => 23 List only jobs before this id. =item ids ids => ['23', '24'] List only jobs with these ids. =item queues queues => ['important', 'unimportant'] List only jobs in these queues. =item states states => ['inactive', 'active'] List only jobs in these states. =item tasks tasks => ['foo', 'bar'] List only jobs for these tasks. =back These fields are currently available: =over 2 =item args args => ['foo', 'bar'] Job arguments. =item attempts attempts => 25 Number of times performing this job will be attempted. =item children children => ['10026', '10027', '10028'] Jobs depending on this job. =item created created => 784111777 Epoch time job was created. =item delayed delayed => 784111777 Epoch time job was delayed to. =item expires expires => 784111777 Epoch time job is valid until before it expires. =item finished finished => 784111777 Epoch time job was finished. =item id id => 10025 Job id. =item lax lax => 0 Existing jobs this job depends on may also have failed to allow for it to be processed. =item notes notes => {foo => 'bar', baz => [1, 2, 3]} Hash reference with arbitrary metadata for this job. =item parents parents => ['10023', '10024', '10025'] Jobs this job depends on. =item priority priority => 3 Job priority. =item queue queue => 'important' Queue name. =item result result => 'All went well!' Job result. =item retried retried => 784111777 Epoch time job has been retried. =item retries retries => 3 Number of times job has been retried. =item started started => 784111777 Epoch time job was started. =item state state => 'inactive' Current job state, usually C, C, C or C. =item task task => 'foo' Task name. =item time time => 78411177 Current time. =item worker worker => '154' Id of worker that is processing the job. =back =head2 list_locks my $results = $backend->list_locks($offset, $limit); my $results = $backend->list_locks($offset, $limit, {names => ['foo']}); Returns information about locks in batches. # Get the total number of results (without limit) my $num = $backend->list_locks(0, 100, {names => ['bar']})->{total}; # Check expiration time my $results = $backend->list_locks(0, 1, {names => ['foo']}); my $expires = $results->{locks}[0]{expires}; These options are currently available: =over 2 =item names names => ['foo', 'bar'] List only locks with these names. =back These fields are currently available: =over 2 =item expires expires => 784111777 Epoch time this lock will expire. =item name name => 'foo' Lock name. =back =head2 list_workers my $results = $backend->list_workers($offset, $limit); my $results = $backend->list_workers($offset, $limit, {ids => [23]}); Returns information about workers in batches. # Get the total number of results (without limit) my $num = $backend->list_workers(0, 100)->{total}; # Check worker host my $results = $backend->list_workers(0, 1, {ids => [$worker_id]}); my $host = $results->{workers}[0]{host}; These options are currently available: =over 2 =item before before => 23 List only workers before this id. =item ids ids => ['23', '24'] List only workers with these ids. =back These fields are currently available: =over 2 =item id id => 22 Worker id. =item host host => 'localhost' Worker host. =item jobs jobs => ['10023', '10024', '10025', '10029'] Ids of jobs the worker is currently processing. =item notified notified => 784111777 Epoch time worker sent the last heartbeat. =item pid pid => 12345 Process id of worker. =item started started => 784111777 Epoch time worker was started. =item status status => {queues => ['default', 'important']} Hash reference with whatever status information the worker would like to share. =back =head2 lock my $bool = $backend->lock('foo', 3600); my $bool = $backend->lock('foo', 3600, {limit => 20}); Try to acquire a named lock that will expire automatically after the given amount of time in seconds. An expiration time of C<0> can be used to check if a named lock already exists without creating one. These options are currently available: =over 2 =item limit limit => 20 Number of shared locks with the same name that can be active at the same time, defaults to C<1>. =back =head2 note my $bool = $backend->note($job_id, {mojo => 'rocks', minion => 'too'}); Change one or more metadata fields for a job. Setting a value to C will remove the field. It is currently an error to attempt to set a metadata field with a name containing the characters C<.>, C<[>, or C<]>. =head2 receive my $commands = $backend->receive($worker_id); Receive remote control commands for worker. =head2 register_worker my $worker_id = $backend->register_worker; my $worker_id = $backend->register_worker($worker_id); my $worker_id = $backend->register_worker( $worker_id, {status => {queues => ['default', 'important']}}); Register worker or send heartbeat to show that this worker is still alive. These options are currently available: =over 2 =item status status => {queues => ['default', 'important']} Hash reference with whatever status information the worker would like to share. =back =head2 remove_job my $bool = $backend->remove_job($job_id); Remove C, C or C job from queue. =head2 repair $backend->repair; Repair worker registry and job queue if necessary. =head2 reset $backend->reset({all => 1}); Reset job queue. These options are currently available: =over 2 =item all all => 1 Reset everything. =item locks locks => 1 Reset only locks. =back =head2 retry_job my $bool = $backend->retry_job($job_id, $retries); my $bool = $backend->retry_job($job_id, $retries, {delay => 10}); Transition job back to C state, already C jobs may also be retried to change options. These options are currently available: =over 2 =item attempts attempts => 25 Number of times performing this job will be attempted. =item delay delay => 10 Delay job for this many seconds (from now). =item expire expire => 300 Job is valid for this many seconds (from now) before it expires. Note that this option is B and might change without warning! =item lax lax => 1 Existing jobs this job depends on may also have transitioned to the C state to allow for it to be processed, defaults to C. Note that this option is B and might change without warning! =item parents parents => [$id1, $id2, $id3] Jobs this job depends on. =item priority priority => 5 Job priority. =item queue queue => 'important' Queue to put job in. =back =head2 stats my $stats = $backend->stats; Get statistics for the job queue. These fields are currently available: =over 2 =item active_jobs active_jobs => 100 Number of jobs in C state. =item active_locks active_locks => 100 Number of active named locks. =item active_workers active_workers => 100 Number of workers that are currently processing a job. =item delayed_jobs delayed_jobs => 100 Number of jobs in C state that are scheduled to run at specific time in the future. =item enqueued_jobs enqueued_jobs => 100000 Rough estimate of how many jobs have ever been enqueued. =item failed_jobs failed_jobs => 100 Number of jobs in C state. =item finished_jobs finished_jobs => 100 Number of jobs in C state. =item inactive_jobs inactive_jobs => 100 Number of jobs in C state. =item inactive_workers inactive_workers => 100 Number of workers that are currently not processing a job. =item uptime uptime => undef Uptime in seconds. Always undefined for SQLite. =back =head2 unlock my $bool = $backend->unlock('foo'); Release a named lock. =head2 unregister_worker $backend->unregister_worker($worker_id); Unregister worker. =head1 BUGS Report any issues on the public bugtracker. =head1 AUTHOR Dan Book =head1 COPYRIGHT AND LICENSE This software is Copyright (c) 2015 by Dan Book. This is free software, licensed under: The Artistic License 2.0 (GPL Compatible) =head1 SEE ALSO L, L =cut __DATA__ @@ minion -- 1 up create table if not exists minion_jobs ( id integer not null primary key autoincrement, args blob not null, created text not null default current_timestamp, delayed text not null, finished text, priority integer not null, result blob, retried text, retries integer not null default 0, started text, state text not null default 'inactive', task text not null, worker integer, queue text not null default 'default' ); create index if not exists minion_jobs_priority_created on minion_jobs (priority desc, created); create index if not exists minion_jobs_state on minion_jobs (state); create table if not exists minion_workers ( id integer not null primary key autoincrement, host text not null, pid integer not null, started text not null default current_timestamp, notified text not null default current_timestamp ); -- 1 down drop table if exists minion_jobs; drop table if exists minion_workers; -- 2 up alter table minion_jobs add column attempts integer not null default 1; -- 3 up create table minion_jobs_NEW ( id integer not null primary key autoincrement, args text not null, created text not null default current_timestamp, delayed text not null, finished text, priority integer not null, result text, retried text, retries integer not null default 0, started text, state text not null default 'inactive', task text not null, worker integer, queue text not null default 'default', attempts integer not null default 1 ); insert into minion_jobs_NEW select * from minion_jobs; drop table minion_jobs; alter table minion_jobs_NEW rename to minion_jobs; -- 4 up alter table minion_jobs add column parents text not null default '[]'; -- 5 up alter table minion_workers add column inbox text not null check(json_valid(inbox) and json_type(inbox) = 'array') default '[]'; -- 6 up drop index if exists minion_jobs_priority_created; drop index if exists minion_jobs_state; create index if not exists minion_jobs_state_priority_id on minion_jobs (state, priority desc, id); -- 7 up alter table minion_workers add column status text not null check(json_valid(status) and json_type(status) = 'object') default '{}'; -- 8 up create table if not exists minion_locks ( id integer not null primary key autoincrement, name text not null, expires text not null ); create index if not exists minion_locks_name_expires on minion_locks (name, expires); alter table minion_jobs add column notes text not null check(json_valid(notes) and json_type(notes) = 'object') default '{}'; -- 8 down drop table if exists minion_locks; -- 9 up alter table minion_jobs add column sequence text; alter table minion_jobs add column next integer; create unique index minion_jobs_next on minion_jobs (next); create index minion_jobs_sequence_next on minion_jobs (sequence, next); -- 10 up create table minion_jobs_NEW ( id integer not null primary key autoincrement, args text not null, created text not null default current_timestamp, delayed text not null, finished text, priority integer not null, result text, retried text, retries integer not null default 0, started text, state text not null default 'inactive', task text not null, worker integer, queue text not null default 'default', attempts integer not null default 1, parents text not null default '[]', notes text not null check(json_valid(notes) and json_type(notes) = 'object') default '{}', expires text, lax boolean not null default 0 ); insert into minion_jobs_NEW (id,args,created,delayed,finished,priority,result,retried,retries, started,state,task,worker,queue,attempts,parents,notes) select id,args,created,delayed,finished,priority,result,retried,retries, started,state,task,worker,queue,attempts,parents,notes from minion_jobs; drop table minion_jobs; alter table minion_jobs_NEW rename to minion_jobs; create index if not exists minion_jobs_state_priority_id on minion_jobs (state, priority desc, id); create index minion_jobs_expires on minion_jobs (expires);