File Coverage

blib/lib/Mojolicious/Plugin/Cron.pm
Criterion Covered Total %
statement 64 64 100.0
branch 16 20 80.0
condition 14 19 73.6
subroutine 12 12 100.0
pod 1 1 100.0
total 107 116 92.2


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Cron;
2 3     3   105659 use Mojo::Base 'Mojolicious::Plugin';
  3         202963  
  3         28  
3 3     3   1904 use File::Spec;
  3         7  
  3         131  
4 3     3   18 use Fcntl ':flock';
  3         6  
  3         458  
5 3     3   544 use Mojo::File 'path';
  3         34085  
  3         210  
6 3     3   732 use Mojo::IOLoop;
  3         146797  
  3         31  
7 3     3   688 use Algorithm::Cron;
  3         5245  
  3         131  
8              
9 3     3   24 use Carp 'croak';
  3         6  
  3         285  
10              
11             our $VERSION = "0.031";
12 3     3   23 use constant CRON_DIR => 'mojo_cron_';
  3         9  
  3         3122  
13             my $crondir;
14              
15             sub register {
16 2     2 1 94 my ($self, $app, $cronhashes) = @_;
17 2 50       11 croak "No schedules found" unless ref $cronhashes eq 'HASH';
18              
19             # for *nix systems, getpwuid takes precedence
20             # for win systems or wherever getpwuid is not implemented,
21             # eval returns undef so getlogin takes precedence
22             $crondir
23             = path($app->config->{cron}{dir} // File::Spec->tmpdir)
24 2   33     20 ->child(CRON_DIR . (eval { scalar getpwuid($<) } || getlogin || 'nobody'),
      50        
25             $app->mode);
26             Mojo::IOLoop->next_tick(sub {
27 2 100   2   13182 if (ref((values %$cronhashes)[0]) eq 'CODE') {
28              
29             # special case, plugin => 'mm hh dd ...' => sub {}
30 1         25 $self->_cron($app->moniker,
31             {crontab => (keys %$cronhashes)[0], code => (values %$cronhashes)[0]});
32             }
33             else {
34 1         9 $self->_cron($_, $cronhashes->{$_}) for keys %$cronhashes;
35             }
36 2         214 });
37             }
38              
39             sub _cron {
40 7     7   421 my ($self, $sckey, $cronhash) = @_;
41 7         21 my $code = delete $cronhash->{code};
42 7   100     35 my $all_proc = delete $cronhash->{all_proc} // '';
43             my $test_key
44 7         59 = delete $cronhash->{__test_key}; # __test_key is for test case only
45 7   66     26 $sckey = $test_key // $sckey;
46              
47 7   100     29 $cronhash->{base} //= 'local';
48              
49 7 50       20 ref $cronhash->{crontab} eq ''
50             or croak "crontab parameter for schedule $sckey not a string";
51 7 50       21 ref $code eq 'CODE' or croak "code parameter for schedule $sckey is not CODE";
52              
53 7         55 my $cron = Algorithm::Cron->new(%$cronhash);
54 7         1356 my $time = time;
55              
56             # $all_proc, $code, $cron, $sckey and $time will be part of the $task clojure
57 7         28 my $task;
58             $task = sub {
59 28     28   102 $time = $cron->next_time($time);
60 28 100       8074 if (!$all_proc) {
61             }
62             Mojo::IOLoop->timer(
63             ($time - time) => sub {
64 21         43299 my $fire;
65 21 100       50 if ($all_proc) {
66 2         6 $fire = 1;
67             }
68             else {
69 19         96 my $dat = $crondir->child("$sckey.time");
70 19         469 my $sem = $crondir->child("$sckey.time.lock");
71 19         332 $crondir->make_path; # ensure path exists
72 19 50       1393 my $handle_sem = $sem->open('>')
73             or croak "Cannot open semaphore file $!";
74 19         2728 flock($handle_sem, LOCK_EX);
75 19 100 66     125 my $rtime = $1
      100        
76             if (-e $dat && $dat->slurp // '') =~ /(\d+)/; # do some untainting
77 19   100     1900 $rtime //= '0';
78 19 100       68 if ($rtime != $time) {
79 18         72 $dat->spurt($time);
80 18         3260 $fire = 1;
81             }
82 19         46 undef $dat;
83 19         231 undef $sem; # unlock
84             }
85 21 100       138 $code->() if $fire;
86 21         1013 $task->();
87             }
88 28         78 );
89 7         47 };
90 7         18 $task->();
91             }
92              
93             1;
94              
95             =encoding utf8
96              
97             =head1 NAME
98              
99             Mojolicious::Plugin::Cron - a Cron-like helper for Mojolicious and Mojolicious::Lite projects
100              
101             =head1 SYNOPSIS
102              
103             # Execute some job every 5 minutes, from 9 to 5 (4:55 actually)
104              
105             # Mojolicious::Lite
106              
107             plugin Cron => ( '*/5 9-16 * * *' => sub {
108             # do someting non-blocking but useful
109             });
110              
111             # Mojolicious
112              
113             $self->plugin(Cron => '*/5 9-16 * * *' => sub {
114             # same here
115             });
116              
117             # More than one schedule, or more options requires extended syntax
118              
119             plugin Cron => (
120             sched1 => {
121             base => 'utc', # not needed for local time
122             crontab => '*/10 15 * * *', # at every 10th minute past hour 15 (3:00 pm to 3:50 pm)
123             code => sub {
124             # job 1 here
125             }
126             },
127             sched2 => {
128             crontab => '*/15 15 * * *', # at every 15th minute past hour 15 (3:00 pm to 3:45 pm)
129             code => sub {
130             # job 2 here
131             }
132             });
133              
134             =head1 DESCRIPTION
135              
136             L is a L plugin that allows to schedule tasks
137             directly from inside a Mojolicious application.
138              
139             The plugin mimics *nix "crontab" format to schedule tasks (see L) .
140              
141             As an extension to regular cron, seconds are supported in the form of a sixth space
142             separated field (For more information on cron syntax please see L).
143              
144             The plugin can help in development and testing phases, as it is very easy to configure and
145             doesn't require a schedule utility with proper permissions at operating system level.
146              
147             For testing, it may be helpful to use Test::Mock::Time ability to "fast-forward"
148             time calling all the timers in the interval. This way, you can actually test events programmed
149             far away in the future.
150              
151             For deployment phase, it will help avoiding the installation steps normally asociated with
152             scheduling periodic tasks.
153              
154             =head1 BASICS
155              
156             When using preforked servers (as applications running with hypnotoad), some coordination
157             is needed so jobs are not executed several times.
158              
159             L uses standard Fcntl functions for that coordination, to assure
160             a platform-independent behavior.
161              
162             Please take a look in the examples section, for a simple Mojo Application that you can
163             run on hypnotoad, try hot restarts, adding / removing workers, etc, and
164             check that scheduled jobs execute without interruptions or duplications.
165              
166             =head1 EXTENDEND SYNTAX HASH
167              
168             When using extended syntax, you can define more than one crontab line, and have access
169             to more options
170              
171             plugin Cron => {key1 => {crontab line 1}, key2 => {crontab line 2}, ...};
172              
173             =head2 Keys
174              
175             Keys are the names that identify each crontab line. They are used to form a locking
176             semaphore file to avoid multiple processes starting the same job.
177              
178             You can use the same name in different Mojolicious applications that will run
179             at the same time. This will ensure that not more that one instance of the cron job
180             will take place at a specific scheduled time.
181              
182             =head2 Crontab lines
183              
184             Each crontab line consists of a hash with the following keys:
185              
186             =over 8
187            
188             =item base => STRING
189            
190             Gives the time base used for scheduling. Either C or C (default C).
191            
192             =item crontab => STRING
193            
194             Gives the crontab schedule in 5 or 6 space-separated fields.
195            
196             =item sec => STRING, min => STRING, ... mon => STRING
197            
198             Optional. Gives the schedule in a set of individual fields, if the C
199             field is not specified.
200              
201             For more information on base, crontab and other time related keys,
202             please refer to L Constructor Attributes.
203              
204             =item code => sub {...}
205              
206             Mandatory. Is the code that will be executed whenever the crontab rule fires.
207             Note that this code *MUST* be non-blocking. For tasks that are naturally
208             blocking, the recommended solution would be to enqueue tasks in a job
209             queue (like the L queue, that will play nicelly with any Mojo project).
210              
211             =back
212              
213             =head1 METHODS
214              
215             L inherits all methods from
216             L and implements the following new ones.
217              
218             =head2 register
219              
220             $plugin->register(Mojolicious->new, {Cron => '* * * * *' => sub {}});
221              
222             Register plugin in L application.
223              
224             =head1 WINDOWS INSTALLATION
225              
226             To install in windows environments, you need to force-install module
227             Test::Mock::Time, or installation tests will fail.
228              
229             =head1 AUTHOR
230              
231             Daniel Mantovani, C
232              
233             =head1 COPYRIGHT AND LICENCE
234              
235             Copyright 2018, Daniel Mantovani.
236              
237             This library is free software; you may redistribute it and/or modify it under
238             the terms of the Artistic License version 2.0.
239              
240             =head1 SEE ALSO
241              
242             L, L, L, L
243              
244             =cut