1Minion(3) User Contributed Perl Documentation Minion(3)
2
3
4
6 Minion - Job queue
7
9 use Minion;
10
11 # Connect to backend
12 my $minion = Minion->new(Pg => 'postgresql://postgres@/test');
13
14 # Add tasks
15 $minion->add_task(something_slow => sub {
16 my ($job, @args) = @_;
17 sleep 5;
18 say 'This is a background worker process.';
19 });
20
21 # Enqueue jobs
22 $minion->enqueue(something_slow => ['foo', 'bar']);
23 $minion->enqueue(something_slow => [1, 2, 3] => {priority => 5});
24
25 # Perform jobs for testing
26 $minion->enqueue(something_slow => ['foo', 'bar']);
27 $minion->perform_jobs;
28
29 # Start a worker to perform up to 12 jobs concurrently
30 my $worker = $minion->worker;
31 $worker->status->{jobs} = 12;
32 $worker->run;
33
35 Minion is a high performance job queue for the Perl programming
36 language, with support for multiple named queues, priorities, delayed
37 jobs, job dependencies, job progress, job results, retries with
38 backoff, rate limiting, unique jobs, expiring jobs, statistics,
39 distributed workers, parallel processing, autoscaling, remote control,
40 Mojolicious <https://mojolicious.org> admin ui, resource leak
41 protection and multiple backends (such as PostgreSQL
42 <https://www.postgresql.org>).
43
44 Job queues allow you to process time and/or computationally intensive
45 tasks in background processes, outside of the request/response
46 lifecycle of web applications. Among those tasks you'll commonly find
47 image resizing, spam filtering, HTTP downloads, building tarballs,
48 warming caches and basically everything else you can imagine that's not
49 super fast.
50
52 You can use Minion as a standalone job queue or integrate it into
53 Mojolicious applications with the plugin Mojolicious::Plugin::Minion.
54
55 use Mojolicious::Lite;
56
57 plugin Minion => {Pg => 'postgresql://sri:s3cret@localhost/test'};
58
59 # Slow task
60 app->minion->add_task(poke_mojo => sub {
61 my $job = shift;
62 $job->app->ua->get('mojolicious.org');
63 $job->app->log->debug('We have poked mojolicious.org for a visitor');
64 });
65
66 # Perform job in a background worker process
67 get '/' => sub {
68 my $c = shift;
69 $c->minion->enqueue('poke_mojo');
70 $c->render(text => 'We will poke mojolicious.org for you soon.');
71 };
72
73 app->start;
74
75 Background worker processes are usually started with the command
76 Minion::Command::minion::worker, which becomes automatically available
77 when an application loads Mojolicious::Plugin::Minion.
78
79 $ ./myapp.pl minion worker
80
81 The worker process will fork a new process for every job that is being
82 processed. This allows for resources such as memory to be returned to
83 the operating system once a job is finished. Perl fork is very fast, so
84 don't worry about the overhead.
85
86 Minion::Worker
87 |- Minion::Job [1]
88 |- Minion::Job [2]
89 +- ...
90
91 By default up to four jobs will be processed in parallel, but that can
92 be changed with configuration options or on demand with signals.
93
94 $ ./myapp.pl minion worker -j 12
95
96 Jobs can be managed right from the command line with
97 Minion::Command::minion::job.
98
99 $ ./myapp.pl minion job
100
101 You can also add an admin ui to your application by loading the plugin
102 Mojolicious::Plugin::Minion::Admin. Just make sure to secure access
103 before making your application publically accessible.
104
105 # Make admin ui available under "/minion"
106 plugin 'Minion::Admin';
107
108 To manage background worker processes with systemd, you can use a unit
109 configuration file like this.
110
111 [Unit]
112 Description=My Mojolicious application workers
113 After=postgresql.service
114
115 [Service]
116 Type=simple
117 ExecStart=/home/sri/myapp/myapp.pl minion worker -m production
118 KillMode=process
119
120 [Install]
121 WantedBy=multi-user.target
122
123 Every job can fail or succeed, but not get lost, the system is
124 eventually consistent and will preserve job results for as long as you
125 like, depending on "remove_after". While individual workers can fail in
126 the middle of processing a job, the system will detect this and ensure
127 that no job is left in an uncertain state, depending on
128 "missing_after".
129
131 And as your application grows, you can move tasks into application
132 specific plugins.
133
134 package MyApp::Task::PokeMojo;
135 use Mojo::Base 'Mojolicious::Plugin';
136
137 sub register {
138 my ($self, $app) = @_;
139 $app->minion->add_task(poke_mojo => sub {
140 my $job = shift;
141 $job->app->ua->get('mojolicious.org');
142 $job->app->log->debug('We have poked mojolicious.org for a visitor');
143 });
144 }
145
146 1;
147
148 Which are loaded like any other plugin from your application.
149
150 # Mojolicious
151 $app->plugin('MyApp::Task::PokeMojo');
152
153 # Mojolicious::Lite
154 plugin 'MyApp::Task::PokeMojo';
155
157 For even more flexibility you can also move tasks into dedicated
158 classes. Allowing the use of Perl features such as inheritance and
159 roles. But be aware that support for task classes is still EXPERIMENTAL
160 and might change without warning!
161
162 package MyApp::Task::PokeMojo;
163 use Mojo::Base 'Minion::Job';
164
165 sub run {
166 my $self = shift;
167 $self->app->ua->get('mojolicious.org');
168 $self->app->log->debug('We have poked mojolicious.org for a visitor');
169 }
170
171 1;
172
173 Task classes are registered just like any other task with "add_task"
174 and you can even register the same class with multiple names.
175
176 $minion->add_task(poke_mojo => 'MyApp::Task::PokeMojo');
177
179 This distribution also contains a great example application you can use
180 for inspiration. The link checker
181 <https://github.com/mojolicious/minion/tree/master/examples/linkcheck>
182 will show you how to integrate background jobs into well-structured
183 Mojolicious applications.
184
186 Minion inherits all events from Mojo::EventEmitter and can emit the
187 following new ones.
188
189 enqueue
190 $minion->on(enqueue => sub {
191 my ($minion, $id) = @_;
192 ...
193 });
194
195 Emitted after a job has been enqueued, in the process that enqueued it.
196
197 $minion->on(enqueue => sub {
198 my ($minion, $id) = @_;
199 say "Job $id has been enqueued.";
200 });
201
202 worker
203 $minion->on(worker => sub {
204 my ($minion, $worker) = @_;
205 ...
206 });
207
208 Emitted in the worker process after it has been created.
209
210 $minion->on(worker => sub {
211 my ($minion, $worker) = @_;
212 say "Worker $$ started.";
213 });
214
216 Minion implements the following attributes.
217
218 app
219 my $app = $minion->app;
220 $minion = $minion->app(MyApp->new);
221
222 Application for job queue, defaults to a Mojo::HelloWorld object. Note
223 that this attribute is weakened.
224
225 backend
226 my $backend = $minion->backend;
227 $minion = $minion->backend(Minion::Backend::Pg->new);
228
229 Backend, usually a Minion::Backend::Pg object.
230
231 backoff
232 my $cb = $minion->backoff;
233 $minion = $minion->backoff(sub {...});
234
235 A callback used to calculate the delay for automatically retried jobs,
236 defaults to "(retries ** 4) + 15" (15, 16, 31, 96, 271, 640...), which
237 means that roughly 25 attempts can be made in 21 days.
238
239 $minion->backoff(sub {
240 my $retries = shift;
241 return ($retries ** 4) + 15 + int(rand 30);
242 });
243
244 missing_after
245 my $after = $minion->missing_after;
246 $minion = $minion->missing_after(172800);
247
248 Amount of time in seconds after which workers without a heartbeat will
249 be considered missing and removed from the registry by "repair",
250 defaults to 1800 (30 minutes).
251
252 remove_after
253 my $after = $minion->remove_after;
254 $minion = $minion->remove_after(86400);
255
256 Amount of time in seconds after which jobs that have reached the state
257 "finished" and have no unresolved dependencies will be removed
258 automatically by "repair", defaults to 172800 (2 days). It is not
259 recommended to set this value below 2 days.
260
261 stuck_after
262 my $after = $minion->stuck_after;
263 $minion = $minion->stuck_after(86400);
264
265 Amount of time in seconds after which jobs that have not been processed
266 will be considered stuck by "repair" and transition to the "failed"
267 state, defaults to 172800 (2 days).
268
269 tasks
270 my $tasks = $minion->tasks;
271 $minion = $minion->tasks({foo => sub {...}});
272
273 Registered tasks.
274
276 Minion inherits all methods from Mojo::EventEmitter and implements the
277 following new ones.
278
279 add_task
280 $minion = $minion->add_task(foo => sub {...});
281 $minion = $minion->add_task(foo => 'MyApp::Task::Foo');
282
283 Register a task, which can be a closure or a custom Minion::Job
284 subclass. Note that support for custom task classes is EXPERIMENTAL and
285 might change without warning!
286
287 # Job with result
288 $minion->add_task(add => sub {
289 my ($job, $first, $second) = @_;
290 $job->finish($first + $second);
291 });
292 my $id = $minion->enqueue(add => [1, 1]);
293 my $result = $minion->job($id)->info->{result};
294
295 broadcast
296 my $bool = $minion->broadcast('some_command');
297 my $bool = $minion->broadcast('some_command', [@args]);
298 my $bool = $minion->broadcast('some_command', [@args], [$id1, $id2, $id3]);
299
300 Broadcast remote control command to one or more workers.
301
302 # Broadcast "stop" command to all workers to kill job 10025
303 $minion->broadcast('stop', [10025]);
304
305 # Broadcast "kill" command to all workers to interrupt job 10026
306 $minion->broadcast('kill', ['INT', 10026]);
307
308 # Broadcast "jobs" command to pause worker 23
309 $minion->broadcast('jobs', [0], [23]);
310
311 class_for_task
312 my $class = $minion->class_for_task('foo');
313
314 Return job class for task. Note that this method is EXPERIMENTAL and
315 might change without warning!
316
317 enqueue
318 my $id = $minion->enqueue('foo');
319 my $id = $minion->enqueue(foo => [@args]);
320 my $id = $minion->enqueue(foo => [@args] => {priority => 1});
321
322 Enqueue a new job with "inactive" state. Arguments get serialized by
323 the "backend" (often with Mojo::JSON), so you shouldn't send objects
324 and be careful with binary data, nested data structures with hash and
325 array references are fine though.
326
327 These options are currently available:
328
329 attempts
330 attempts => 25
331
332 Number of times performing this job will be attempted, with a delay
333 based on "backoff" after the first attempt, defaults to 1.
334
335 delay
336 delay => 10
337
338 Delay job for this many seconds (from now), defaults to 0.
339
340 expire
341 expire => 300
342
343 Job is valid for this many seconds (from now) before it expires. Note
344 that this option is EXPERIMENTAL and might change without warning!
345
346 lax
347 lax => 1
348
349 Existing jobs this job depends on may also have transitioned to the
350 "failed" state to allow for it to be processed, defaults to "false".
351 Note that this option is EXPERIMENTAL and might change without
352 warning!
353
354 notes
355 notes => {foo => 'bar', baz => [1, 2, 3]}
356
357 Hash reference with arbitrary metadata for this job that gets
358 serialized by the "backend" (often with Mojo::JSON), so you shouldn't
359 send objects and be careful with binary data, nested data structures
360 with hash and array references are fine though.
361
362 parents
363 parents => [$id1, $id2, $id3]
364
365 One or more existing jobs this job depends on, and that need to have
366 transitioned to the state "finished" before it can be processed.
367
368 priority
369 priority => 5
370
371 Job priority, defaults to 0. Jobs with a higher priority get
372 performed first.
373
374 queue
375 queue => 'important'
376
377 Queue to put job in, defaults to "default".
378
379 foreground
380 my $bool = $minion->foreground($id);
381
382 Retry job in "minion_foreground" queue, then perform it right away with
383 a temporary worker in this process, very useful for debugging.
384
385 guard
386 my $guard = $minion->guard('foo', 3600);
387 my $guard = $minion->guard('foo', 3600, {limit => 20});
388
389 Same as "lock", but returns a scope guard object that automatically
390 releases the lock as soon as the object is destroyed, or "undef" if
391 aquiring the lock failed.
392
393 # Only one job should run at a time (unique job)
394 $minion->add_task(do_unique_stuff => sub {
395 my ($job, @args) = @_;
396 return $job->finish('Previous job is still active')
397 unless my $guard = $minion->guard('fragile_backend_service', 7200);
398 ...
399 });
400
401 # Only five jobs should run at a time and we try again later if necessary
402 $minion->add_task(do_concurrent_stuff => sub {
403 my ($job, @args) = @_;
404 return $job->retry({delay => 30})
405 unless my $guard = $minion->guard('some_web_service', 60, {limit => 5});
406 ...
407 });
408
409 history
410 my $history = $minion->history;
411
412 Get history information for job queue.
413
414 These fields are currently available:
415
416 daily
417 daily => [{epoch => 12345, finished_jobs => 95, failed_jobs => 2}, ...]
418
419 Hourly counts for processed jobs from the past day.
420
421 is_locked
422 my $bool = $minion->is_locked('foo');
423
424 Check if a lock with that name is currently active.
425
426 job
427 my $job = $minion->job($id);
428
429 Get Minion::Job object without making any changes to the actual job or
430 return "undef" if job does not exist.
431
432 # Check job state
433 my $state = $minion->job($id)->info->{state};
434
435 # Get job metadata
436 my $progress = $minion->$job($id)->info->{notes}{progress};
437
438 # Get job result
439 my $result = $minion->job($id)->info->{result};
440
441 jobs
442 my $jobs = $minion->jobs;
443 my $jobs = $minion->jobs({states => ['inactive']});
444
445 Return Minion::Iterator object to safely iterate through job
446 information.
447
448 # Iterate through jobs for two tasks
449 my $jobs = $minion->jobs({tasks => ['foo', 'bar']});
450 while (my $info = $jobs->next) {
451 say "$info->{id}: $info->{state}";
452 }
453
454 # Remove all failed jobs from a named queue
455 my $jobs = $minion->jobs({states => ['failed'], queues => ['unimportant']});
456 while (my $info = $jobs->next) {
457 $minion->job($info->{id})->remove;
458 }
459
460 # Count failed jobs for a task
461 say $minion->jobs({states => ['failed'], tasks => ['foo']})->total;
462
463 These options are currently available:
464
465 ids
466 ids => ['23', '24']
467
468 List only jobs with these ids.
469
470 notes
471 notes => ['foo', 'bar']
472
473 List only jobs with one of these notes.
474
475 queues
476 queues => ['important', 'unimportant']
477
478 List only jobs in these queues.
479
480 states
481 states => ['inactive', 'active']
482
483 List only jobs in these states.
484
485 tasks
486 tasks => ['foo', 'bar']
487
488 List only jobs for these tasks.
489
490 These fields are currently available:
491
492 args
493 args => ['foo', 'bar']
494
495 Job arguments.
496
497 attempts
498 attempts => 25
499
500 Number of times performing this job will be attempted.
501
502 children
503 children => ['10026', '10027', '10028']
504
505 Jobs depending on this job.
506
507 created
508 created => 784111777
509
510 Epoch time job was created.
511
512 delayed
513 delayed => 784111777
514
515 Epoch time job was delayed to.
516
517 expires
518 expires => 784111777
519
520 Epoch time job is valid until before it expires.
521
522 finished
523 finished => 784111777
524
525 Epoch time job was finished.
526
527 id
528 id => 10025
529
530 Job id.
531
532 lax
533 lax => 0
534
535 Existing jobs this job depends on may also have failed to allow for
536 it to be processed.
537
538 notes
539 notes => {foo => 'bar', baz => [1, 2, 3]}
540
541 Hash reference with arbitrary metadata for this job.
542
543 parents
544 parents => ['10023', '10024', '10025']
545
546 Jobs this job depends on.
547
548 priority
549 priority => 3
550
551 Job priority.
552
553 queue
554 queue => 'important'
555
556 Queue name.
557
558 result
559 result => 'All went well!'
560
561 Job result.
562
563 retried
564 retried => 784111777
565
566 Epoch time job has been retried.
567
568 retries
569 retries => 3
570
571 Number of times job has been retried.
572
573 started
574 started => 784111777
575
576 Epoch time job was started.
577
578 state
579 state => 'inactive'
580
581 Current job state, usually "active", "failed", "finished" or
582 "inactive".
583
584 task
585 task => 'foo'
586
587 Task name.
588
589 time
590 time => 78411177
591
592 Server time.
593
594 worker
595 worker => '154'
596
597 Id of worker that is processing the job.
598
599 lock
600 my $bool = $minion->lock('foo', 3600);
601 my $bool = $minion->lock('foo', 3600, {limit => 20});
602
603 Try to acquire a named lock that will expire automatically after the
604 given amount of time in seconds. You can release the lock manually with
605 "unlock" to limit concurrency, or let it expire for rate limiting. For
606 convenience you can also use "guard" to release the lock automatically,
607 even if the job failed.
608
609 # Only one job should run at a time (unique job)
610 $minion->add_task(do_unique_stuff => sub {
611 my ($job, @args) = @_;
612 return $job->finish('Previous job is still active')
613 unless $minion->lock('fragile_backend_service', 7200);
614 ...
615 $minion->unlock('fragile_backend_service');
616 });
617
618 # Only five jobs should run at a time and we wait for our turn
619 $minion->add_task(do_concurrent_stuff => sub {
620 my ($job, @args) = @_;
621 sleep 1 until $minion->lock('some_web_service', 60, {limit => 5});
622 ...
623 $minion->unlock('some_web_service');
624 });
625
626 # Only a hundred jobs should run per hour and we try again later if necessary
627 $minion->add_task(do_rate_limited_stuff => sub {
628 my ($job, @args) = @_;
629 return $job->retry({delay => 3600})
630 unless $minion->lock('another_web_service', 3600, {limit => 100});
631 ...
632 });
633
634 An expiration time of 0 can be used to check if a named lock could have
635 been acquired without creating one.
636
637 # Check if the lock "foo" could have been acquired
638 say 'Lock could have been acquired' unless $minion->lock('foo', 0);
639
640 Or to simply check if a named lock already exists you can also use
641 "is_locked".
642
643 These options are currently available:
644
645 limit
646 limit => 20
647
648 Number of shared locks with the same name that can be active at the
649 same time, defaults to 1.
650
651 new
652 my $minion = Minion->new(Pg => 'postgresql://postgres@/test');
653 my $minion = Minion->new(Pg => Mojo::Pg->new);
654
655 Construct a new Minion object.
656
657 perform_jobs
658 $minion->perform_jobs;
659 $minion->perform_jobs({queues => ['important']});
660
661 Perform all jobs with a temporary worker, very useful for testing.
662
663 # Longer version
664 my $worker = $minion->worker;
665 while (my $job = $worker->register->dequeue(0)) { $job->perform }
666 $worker->unregister;
667
668 These options are currently available:
669
670 queues
671 queues => ['important']
672
673 One or more queues to dequeue jobs from, defaults to "default".
674
675 repair
676 $minion = $minion->repair;
677
678 Repair worker registry and job queue if necessary.
679
680 reset
681 $minion = $minion->reset({all => 1});
682
683 Reset job queue.
684
685 These options are currently available:
686
687 all
688 all => 1
689
690 Reset everything.
691
692 locks
693 locks => 1
694
695 Reset only locks.
696
697 result_p
698 my $promise = $minion->result_p($id);
699 my $promise = $minion->result_p($id, {interval => 5});
700
701 Return a Mojo::Promise object for the result of a job. The state
702 "finished" will result in the promise being "fullfilled", and the state
703 "failed" in the promise being "rejected". This operation can be
704 cancelled by resolving the promise manually at any time.
705
706 # Enqueue job and receive the result at some point in the future
707 my $id = $minion->enqueue('foo');
708 $minion->result_p($id)->then(sub {
709 my $info = shift;
710 my $result = ref $info ? $info->{result} : 'Job already removed';
711 say "Finished: $result";
712 })->catch(sub {
713 my $info = shift;
714 say "Failed: $info->{result}";
715 })->wait;
716
717 These options are currently available:
718
719 interval
720 interval => 5
721
722 Polling interval in seconds for checking if the state of the job has
723 changed, defaults to 3.
724
725 stats
726 my $stats = $minion->stats;
727
728 Get statistics for the job queue.
729
730 # Check idle workers
731 my $idle = $minion->stats->{inactive_workers};
732
733 These fields are currently available:
734
735 active_jobs
736 active_jobs => 100
737
738 Number of jobs in "active" state.
739
740 active_locks
741 active_locks => 100
742
743 Number of active named locks.
744
745 active_workers
746 active_workers => 100
747
748 Number of workers that are currently processing a job.
749
750 delayed_jobs
751 delayed_jobs => 100
752
753 Number of jobs in "inactive" state that are scheduled to run at
754 specific time in the future or have unresolved dependencies.
755
756 enqueued_jobs
757 enqueued_jobs => 100000
758
759 Rough estimate of how many jobs have ever been enqueued.
760
761 failed_jobs
762 failed_jobs => 100
763
764 Number of jobs in "failed" state.
765
766 finished_jobs
767 finished_jobs => 100
768
769 Number of jobs in "finished" state.
770
771 inactive_jobs
772 inactive_jobs => 100
773
774 Number of jobs in "inactive" state.
775
776 inactive_workers
777 inactive_workers => 100
778
779 Number of workers that are currently not processing a job.
780
781 uptime
782 uptime => 1000
783
784 Uptime in seconds.
785
786 unlock
787 my $bool = $minion->unlock('foo');
788
789 Release a named lock that has been previously acquired with "lock".
790
791 worker
792 my $worker = $minion->worker;
793
794 Build Minion::Worker object. Note that this method should only be used
795 to implement custom workers.
796
797 # Use the standard worker with all its features
798 my $worker = $minion->worker;
799 $worker->status->{jobs} = 12;
800 $worker->status->{queues} = ['important'];
801 $worker->run;
802
803 # Perform one job manually in a separate process
804 my $worker = $minion->repair->worker->register;
805 my $job = $worker->dequeue(5);
806 $job->perform;
807 $worker->unregister;
808
809 # Perform one job manually in this process
810 my $worker = $minion->repair->worker->register;
811 my $job = $worker->dequeue(5);
812 if (my $err = $job->execute) { $job->fail($err) }
813 else { $job->finish }
814 $worker->unregister;
815
816 # Build a custom worker performing multiple jobs at the same time
817 my %jobs;
818 my $worker = $minion->repair->worker->register;
819 do {
820 for my $id (keys %jobs) {
821 delete $jobs{$id} if $jobs{$id}->is_finished;
822 }
823 if (keys %jobs >= 4) { sleep 5 }
824 else {
825 my $job = $worker->dequeue(5);
826 $jobs{$job->id} = $job->start if $job;
827 }
828 } while keys %jobs;
829 $worker->unregister;
830
831 workers
832 my $workers = $minion->workers;
833 my $workers = $minion->workers({ids => [2, 3]});
834
835 Return Minion::Iterator object to safely iterate through worker
836 information.
837
838 # Iterate through workers
839 my $workers = $minion->workers;
840 while (my $info = $workers->next) {
841 say "$info->{id}: $info->{host}";
842 }
843
844 These options are currently available:
845
846 ids
847 ids => ['23', '24']
848
849 List only workers with these ids.
850
851 These fields are currently available:
852
853 id
854 id => 22
855
856 Worker id.
857
858 host
859 host => 'localhost'
860
861 Worker host.
862
863 jobs
864 jobs => ['10023', '10024', '10025', '10029']
865
866 Ids of jobs the worker is currently processing.
867
868 notified
869 notified => 784111777
870
871 Epoch time worker sent the last heartbeat.
872
873 pid
874 pid => 12345
875
876 Process id of worker.
877
878 started
879 started => 784111777
880
881 Epoch time worker was started.
882
883 status
884 status => {queues => ['default', 'important']}
885
886 Hash reference with whatever status information the worker would like
887 to share.
888
890 This is the class hierarchy of the Minion distribution.
891
892 · Minion
893
894 · Minion::Backend
895
896 · Minion::Backend::Pg
897
898 · Minion::Command::minion
899
900 · Minion::Command::minion::job
901
902 · Minion::Command::minion::worker
903
904 · Minion::Job
905
906 · Minion::Worker
907
908 · Mojolicious::Plugin::Minion
909
910 · Mojolicious::Plugin::Minion::Admin
911
913 The Minion distribution includes a few files with different licenses
914 that have been bundled for internal use.
915
916 Minion Artwork
917 Copyright (C) 2017, Sebastian Riedel.
918
919 Licensed under the CC-SA License, Version 4.0
920 <http://creativecommons.org/licenses/by-sa/4.0>.
921
922 Bootstrap
923 Copyright (C) 2011-2018 The Bootstrap Authors.
924
925 Licensed under the MIT License,
926 <http://creativecommons.org/licenses/MIT>.
927
928 D3.js
929 Copyright (C) 2010-2016, Michael Bostock.
930
931 Licensed under the 3-Clause BSD License,
932 <https://opensource.org/licenses/BSD-3-Clause>.
933
934 epoch.js
935 Copyright (C) 2014 Fastly, Inc.
936
937 Licensed under the MIT License,
938 <http://creativecommons.org/licenses/MIT>.
939
940 Font Awesome
941 Copyright (C) Dave Gandy.
942
943 Licensed under the MIT License,
944 <http://creativecommons.org/licenses/MIT>, and the SIL OFL 1.1,
945 <http://scripts.sil.org/OFL>.
946
947 moment.js
948 Copyright (C) JS Foundation and other contributors.
949
950 Licensed under the MIT License,
951 <http://creativecommons.org/licenses/MIT>.
952
953 popper.js
954 Copyright (C) Federico Zivolo 2017.
955
956 Licensed under the MIT License,
957 <http://creativecommons.org/licenses/MIT>.
958
960 Sebastian Riedel, "sri@cpan.org".
961
963 In alphabetical order:
964
965 Andrey Khozov
966
967 Andrii Nikitin
968
969 Brian Medley
970
971 Franz Skale
972
973 Hubert "depesz" Lubaczewski
974
975 Joel Berger
976
977 Paul Williams
978
979 Stefan Adams
980
982 Copyright (C) 2014-2020, Sebastian Riedel and others.
983
984 This program is free software, you can redistribute it and/or modify it
985 under the terms of the Artistic License version 2.0.
986
988 <https://github.com/mojolicious/minion>, <https://minion.pm>,
989 Mojolicious::Guides, <https://mojolicious.org>.
990
991
992
993perl v5.32.0 2020-08-02 Minion(3)