File Coverage

blib/lib/Cron/Sequencer.pm
Criterion Covered Total %
statement 15 42 35.7
branch 3 16 18.7
condition 0 3 0.0
subroutine 4 6 66.6
pod 2 2 100.0
total 24 69 34.7


line stmt bran cond sub pod time code
1             #!perl
2              
3 1     1   662 use v5.20.0;
  1         3  
4 1     1   4 use warnings;
  1         2  
  1         46  
5              
6              
7             our $VERSION = '0.05';
8              
9             use Carp qw(croak confess);
10 1     1   5  
  1         2  
  1         500  
11             require Cron::Sequencer::Parser;
12              
13             # scalar -> filename
14             # ref to scalar -> contents
15             # hashref -> fancy
16              
17             my ($class, @args) = @_;
18             confess('new() called as an instance method')
19 30     30 1 33414 if ref $class;
20 30 100       323  
21             my @self;
22             for my $arg (@args) {
23 29         33 $arg = Cron::Sequencer::Parser->new($arg)
24 29         42 unless UNIVERSAL::isa($arg, 'Cron::Sequencer::Parser');
25 29 50       126 push @self, $arg->entries();
26             }
27 15         36  
28             return bless \@self, $class;
29             }
30 15         51  
31             # The intent is to avoid repeatedly calling ->next_time() on every event on
32             # every loop, which would make next() have O(n) performance, and looping a range
33             # O(n**2)
34              
35             my $self = shift;
36              
37             my $when = $self->[0]{next};
38 0     0     my @found;
39              
40 0           for my $entry (@$self) {
41 0           if ($entry->{next} < $when) {
42             # If this one is earlier, discard everything we found so far
43 0           $when = $entry->{next};
44 0 0         @found = $entry;
    0          
45             } elsif ($entry->{next} == $when) {
46 0           # If it's a tie, add it to the list of found
47 0           push @found, $entry;
48             }
49             }
50 0            
51             my @retval;
52              
53             for my $entry (@found) {
54 0           my %published = (
55             time => $when, %$entry{qw(file lineno when command)},
56 0           );
57             # Be careful not to set these if they are not present in the input hash
58 0           # (If the key is present the value is always defined, so it doesn't
59             # actually matter if we do an exists test or a defined test. It's sort
60             # of annoying that the hash key-value slice syntax always provides a
61             # list pair ($key, undef) for missing keys, but the current behaviour is
62             # also useful in other cases, and we only have one syntax available)
63             for my $key (qw(env unset)) {
64             $published{$key} = $entry->{$key}
65             if exists $entry->{$key};
66 0           }
67             push @retval, \%published;
68 0 0          
69             # We've "consumed" this firing, so update the cached value
70 0           $entry->{next} = $entry->{whenever}->next_time($when);
71             }
72              
73 0           return @retval;
74             }
75              
76 0           my ($self, $start, $end) = @_;
77              
78             croak('sequence($epoch_seconds, $epoch_seconds)')
79             if $start !~ /\A[1-9][0-9]*\z/ || $end !~ /\A[1-9][0-9]*\z/;
80 0     0 1    
81             return
82 0 0 0       unless @$self;
83              
84             # As we have to call ->next_time(), which returns the next time *after* the
85             # epoch time we pass it.
86 0 0         --$start;
87              
88             for my $entry (@$self) {
89             # Cache the time (in epoch seconds) for the next firing for this entry
90 0           $entry->{next} = $entry->{whenever}->next_time($start);
91             }
92 0            
93             my @results;
94 0           while(my @group = $self->_next()) {
95             last
96             if $group[0]->{time} >= $end;
97 0            
98 0           push @results, \@group;
99             }
100 0 0          
101             return @results;
102 0           }
103              
104             =head1 NAME
105 0            
106             Cron::Sequencer
107              
108             =head1 SYNOPSIS
109              
110             my $crontab = Cron::Sequencer->new("/path/to/crontab");
111             print encode_json([$crontab->sequence($start, $end)]);
112              
113             =head1 DESCRIPTION
114              
115             This class can take one or more crontabs and show the sequence of commands
116             that they would run for the time interval requested.
117              
118             =head1 METHODS
119              
120             =head2 new
121              
122             C<new> takes a list of arguments each representing a crontab file, passes each
123             in turn to C<< Cron::Sequence::Parser->new >>, and then combines the parsed
124             files into a single set of crontab events.
125              
126             See L<Cron::Sequencer::Parser/new> for the various formats to specify a crontab
127             file or its contents.
128              
129             =head2 sequence I<from> I<to>
130              
131             Generates the sequence of commands that the crontab(s) would run for the
132             specific time interval. I<from> and I<to> are in epoch seconds, I<from> is
133             inclusive, I<end> exclusive.
134              
135             Hence for this input:
136              
137             30 12 * * * lunch!
138             30 12 * * 5 POETS!
139              
140             Calling C<< $crontab->sequence(45000, 131400) >> generates this output:
141              
142             [
143             [
144             {
145             command => "lunch!",
146             env => undef,
147             file => "reminder",
148             lineno => 1,
149             time => 45000,
150             unset => undef,
151             when => "30 12 * * *",
152             },
153             ],
154             ]
155              
156             where the event(s) at C<131400> are not reported, because the end is
157             exclusive. Whereas C<< $crontab->sequence(45000, 131401) >> shows:
158              
159             [
160             [
161             {
162             command => "lunch!",
163             env => undef,
164             file => "reminder",
165             lineno => 1,
166             time => 45000,
167             unset => undef,
168             when => "30 12 * * *",
169             },
170             ],
171             [
172             {
173             command => "lunch!",
174             env => undef,
175             file => "reminder",
176             lineno => 1,
177             time => 131400,
178             unset => undef,
179             when => "30 12 * * *",
180             },
181             {
182             command => "POETS!",
183             env => undef,
184             file => "reminder",
185             lineno => 2,
186             time => 131400,
187             unset => undef,
188             when => "30 12 * * 5",
189             },
190             ],
191             ]
192              
193             The output is structured as a list of lists, with events that fire at the
194             same time grouped as lists. This makes it easier to find cases where different
195             crontab lines trigger at the same time.
196              
197             =head1 SEE ALSO
198              
199             This module uses L<Algorithm::Cron> to implement the cron scheduling, but has
200             its own crontab file parser. There are many other modules on CPAN:
201              
202             =over 4
203              
204             =item L<Config::Crontab>
205              
206             Parses, edits and outputs crontab files
207              
208             =item L<Config::Generator::Crontab>
209              
210             Outputs crontab files
211              
212             =item L<DateTime::Cron::Simple>
213              
214             Parse a cron entry and check against current time
215              
216             =item L<DateTime::Event::Cron>
217              
218             Generate recurrence sets from crontab lines and files
219              
220             =item L<Mojar::Cron>
221              
222             Cron-style datetime patterns and algorithm (for Mojolicious)
223              
224             =item L<Parse::Crontab>
225              
226             Parses crontab files
227              
228             =item L<Pegex::Crontab>
229              
230             A Pegex crontab Parser
231              
232             =item L<QBit::Cron>
233              
234             "Class for working with Cron" (for qbit)
235              
236             =item L<Set::Crontab>
237              
238             Expands crontab integer lists
239              
240             =item L<Schedule::Cron>
241              
242             cron-like scheduler for Perl subroutines
243              
244             =item L<Schedule::Cron::Events>
245              
246             take a line from a crontab and find out when events will occur
247              
248             =item L<Time::Crontab>
249              
250             Parser for crontab time specifications
251              
252             =back
253              
254             These modules fall into roughly three groups
255              
256             =over 4
257              
258             =item *
259              
260             Abstract C<crontab> file parsing and manipulation
261              
262             =item *
263              
264             Parsing individule command time specification strings
265              
266             =item *
267              
268             Scheduling events in real time
269              
270             =back
271              
272             None of the "schedulers" are easy to adapt to show events (rather than running
273             them) and to do so for arbitrary time intervals. The parsers effectively provide
274             an "abstract syntax tree" for the crontab, but by design don't handle
275             "compiling" this into a sequence of "this command, with these environment
276             variable definitions in scope". The parser/compiler in this module is 70 lines
277             of code, including comments, and handles various corner cases and quirks of the
278             vixie crontab C parser code. Interfacing to one of AST parser modules and
279             implementing a "compiler" on it would likely be more code than this.
280              
281             =head1 LICENSE
282              
283             This library is free software; you can redistribute it and/or modify it under
284             the same terms as Perl itself. If you would like to contribute documentation,
285             features, bug fixes, or anything else then please raise an issue / pull request:
286              
287             https://github.com/Humanstate/cron-sequencer
288              
289             =head1 AUTHOR
290              
291             Nicholas Clark - C<nick@ccl4.org>
292              
293             =cut
294              
295             1;