Schedule::Cron(3pm)
๐ NAME ๐
Schedule::Cron โ no wait, the manual calls it Cron โ it's a cron-like scheduler for Perl subroutines. Bestie, it's Schedule::Cron(3pm) and it's about to be your new obsession. โจ
โจ SYNOPSIS โจ
use Schedule::Cron;
# Subroutines to be called
sub dispatcher {
print "ID: ",shift,"\n";
print "Args: ","@_","\n";
}
sub check_links {
# do something...
}
# Create new object with default dispatcher
my $cron = new Schedule::Cron(\&dispatcher);
# Load a crontab file
$cron->load_crontab("/var/spool/cron/perl");
# Add dynamically crontab entries
$cron->add_entry("3 4 * * *",ROTATE => "apache","sendmail");
$cron->add_entry("0 11 * * Mon-Fri",\&check_links);
# Run scheduler
$cron->run(detach=>1);
๐ธ DESCRIPTION ๐ธ
Okay so here's the whole vibe: this module gives you a simple but complete cron-like scheduler. Like, you can use it to periodically execute Perl subroutines. The dates and the parameters for the subroutines you wanna call get specified with a format known as a crontab entry (peep "METHODS", add_entry() and crontab(5) for that).
The philosophy behind Schedule::Cron is chef's kiss honestly: it calls subroutines periodically from within one single Perl program instead of letting cron trigger several (possibly different) Perl scripts. Everything under one roof, as it should be. ๐
And on top of that, Schedule::Cron gives you a mechanism to create crontab entries dynamically, which ngl isn't that easy with cron.
Schedule::Cron knows about all the extensions โ well, at least all the extensions the author is aware of, i.e. those of the so called "Vixie" cron โ for crontab entries, like ranges including 'steps', specifying month and days of the week by name, or lists and ranges coexisting in the same field. It even supports a little bit more than that (like lists and ranges with symbolic names). Iconic behavior fr fr. ๐
๐ METHODS ๐
$cron = new Schedule::Cron($dispatcher,[extra args])
Creates a new Cron object, periodt. $dispatcher is a reference to a subroutine, and that's the one that gets called by default. $dispatcher gets invoked with the arguments parameter you put in the crontab entry, but only if no other subroutine is specified. That argument can go two ways:
- a single argument containing the argument parameter literally as a string (this is the default behavior), or
- a list of arguments, if you're using the
evaloption described down below.
The date specifications have to come either from a crontab-like file or get added explicitly with add_entry() ("add_entry").
extra_args can be a hash or a hash reference for extra arguments. These are the parameters it recognizes:
file => <crontab>โ load the crontab entries from<crontab>.eval => 1โ eval the argument parameter in a crontab entry before calling the subroutine (instead of literally calling the dispatcher with the argument parameter as a string).nofork => 1โ don't fork when starting the scheduler. Instead, the jobs are executed within the current process. So inside your executed jobs you have full access to the global variables of your script, and hence you might influence other jobs running at a different time. Heads up: this behavior is fundamentally different from the 'fork' mode, where each job gets its own process and therefore its own copy of the process space, independent of every other job and of the main process. That's just the nature of theforksystem call, bestie. ๐ซถnostatus => 1โ do not update status in$0. Set this if you don't wantpsto reveal the internals of your application, including job argument lists. Default is0(update status).skip => 1โ skip any pending jobs whose time has passed. This option is only useful in combination withnofork, where a job might block the execution of the following jobs for quite some time. By default, any pending job is executed even if its scheduled execution time has already passed. With this option set to true, all pending jobs which would have been started in the meantime are skipped.catch => 1โ catch any exception raised by a job. This is especially useful in combination with thenoforkoption, so you avoid stopping the main process when a job raises an exception (dies). ๐โก๏ธ๐after_job => \&after_subโ call a subroutine after a job has been run. The first argument is the return value of the dispatched job, and the remaining arguments are the arguments with which the dispatched job has been called.
Example:
my $cron = new Schedule::Cron(..., after_job => sub {
my ($ret,@args) = @_;
print "Return value: ",$ret," - job arguments: (",join ":",@args,")\n";
});
log => \&log_subโ install a logging subroutine. The subroutine you give gets called for several events during the lifetime of a job. It's called with two arguments: a log level of0(info),1(warning) or2(error), depending on how important the message is, and the message itself.
For example, you could use Log4perl (<http://log4perl.sf.net>) for logging purposes, like in this code snippet:
use Log::Log4perl;
use Log::Log4perl::Level;
my $log_method = sub {
my ($level,$msg) = @_;
my $DBG_MAP = { 0 => $INFO, 1 => $WARN, 2 => $ERROR };
my $logger = Log::Log4perl->get_logger("My::Package");
$logger->log($DBG_MAP->{$level},$msg);
}
my $cron = new Schedule::Cron(.... , log => $log_method);
loglevel => <-1,0,1,2>โ restricts logging to the specified severity level or below. Use0to have all messages generated,1for only warnings and errors, and2for errors only. Default is0(all messages). A loglevel of-1(debug) will include job argument lists (also in$0) in the job start message logged with a level of 0 or above. You may have security concerns with this, so unless you're debugging, use0or higher. A value larger than2will disable logging completely.
And like, even though you can filter inside your own log routine, generating the messages can be expensive โ for example if you pass arguments pointing to large hashes. Specifying a loglevel avoids formatting data that your routine would just discard anyway. Efficiency queen. ๐
processprefix => <name>โCron::Schedulesets the process' name (i.e.$0) to contain some informative messages, like when the next job executes or with which arguments a job is called. By default, the prefix for these labels isSchedule::Cron. With this option you can set it to something different. You can e.g. use$0to include the original process name. You can inhibit this with thenostatusoption, and prevent the argument display by settingloglevelto zero or higher.processname => <name>โ set the process name (i.e.$0) to a literal string. Using this setting overridesprocessprefixandnostatus.sleep => \&hookโ if specified,&hookwill be called instead ofsleep(), with the time to sleep in seconds as the first argument and the Schedule::Cron object as the second. This hook is the moment where you can useselect()instead of sleep, so you can handle IO โ like job requests coming in from a network connection. Main character energy for your event loop honestly. ๐
e.g.
$cron->run( { sleep => \&sleep_hook, nofork => 1 } );
sub sleep_hook {
my ($time, $cron) = @_;
my ($rin, $win, $ein) = ('','','');
my ($rout, $wout, $eout);
vec($rin, fileno(STDIN), 1) = 1;
my ($nfound, $ttg) = select($rout=$rin, $wout=$win, $eout=$ein, $time);
if ($nfound) {
handle_io($rout, $wout, $eout);
}
return;
}
$cron->load_crontab($file)
$cron->load_crontab(file=>$file,[eval=>1])
Loads and parses the crontab file $file. The entries found in this file get added to the current time table with $cron->add_entry. ๐
The format of the file is cron commands made of lines with at least 5 columns, where the first 5 columns specify the date. The rest of the line (i.e. columns 6 and greater) contains the argument with which the dispatcher subroutine will be called. By default the dispatcher gets called with one single string argument containing the rest of the line literally. Alternatively, if you call this method with the optional argument eval=>1 (you must then use the second format shown above), the rest of the line will be evaled before being used as the argument for the dispatcher.
For the format of the first 5 columns, please see add_entry.
Blank lines and lines starting with a # get ignored. ๐ซ
There's no way to specify another subroutine within the crontab file โ all calls get made to the dispatcher that was provided at construction time. No exceptions, bestie.
If you want to start fresh, you should call $cron->clean_timetable() before.
Example of a crontab file:
# The following line runs on every Monday at 2:34 am
34 2 * * Mon "make_stats"
# The next line should be best read in with an eval=>1 argument
* * 1 1 * { NEW_YEAR => '1',HEADACHE => 'on' }
$cron->add_entry($timespec,[arguments])
Adds a new entry to the list of scheduled cron jobs. โจ
Time and Date specification
$timespec is the specification of the scheduled time in crontab format (crontab(5)), which contains five mandatory time and date fields plus an optional 6th column. $timespec can be either a plain string containing a whitespace separated time and date specification, or a reference to an array containing the five elements for the date fields.
The time and date fields are (taken mostly from crontab(5), "Vixie" cron):
minuteโ0-59hourโ0-23day of monthโ1-31monthโ1-12(or as names)day of weekโ0-7(0 or 7 is Sunday, or as names)secondsโ0-59(optional)
A field may be an asterisk (*), which always stands for ``first-last''.
Ranges? allowed ๐ A range is just two numbers vibing together separated by a hyphen, and the whole thing is inclusive โ no off-by-one nonsense here. So <pink>8-11</pink> in the `hours` field means run at hours 8, 9, 10 and 11. periodt.
Lists? also allowed ๐
A list is a set of numbers (or ranges) separated by commas. Examples that live in my head rent free: `1,2,5,9`, `0-4,8-12`.
Step values can hang out with ranges too. Pop a `/<number>` after a range and it skips by that value all the way through the range. So `0-23/2` in the hours field = every other hour. (The old V7 standard way to say the exact same thing is `0,2,4,6,8,10,12,14,16,18,20,22` โ iconic but unnecessarily long, ngl.) Steps are also completely fine after an asterisk, so if you're trying to say "every two hours", just use `*/2`. so easy. so slay. ๐ฆ
Names work for the `month` and `day of week` fields too ๐ธ just use the first three letters of the day or month (case doesn't matter, she's not picky bestie).
Note bestie: the day a command runs can be specified by two fields โ day of month AND day of week. If BOTH are restricted (i.e. they aren't`*`), the command runs when either field matches the current time. So`30 4 1,15 * 5`runs at 4:30 am on the 1st and 15th of each month, plus every Friday. two moods, both valid ๐
Examples:
"8 0 * * *" ==> 8 minutes after midnight, every day
"5 11 * * Sat,Sun" ==> at 11:05 on each Saturday and Sunday
"0-59/5 * * * *" ==> every five minutes
"42 12 3 Feb Sat" ==> at 12:42 on 3rd of February and on
each Saturday in February
"32 11 * * * 0-30/2" ==> 11:32:00, 11:32:02, ... 11:32:30 every
day
And yeah, ranges or lists of names are allowed too โจ
Oh and there's an optional sixth column for specifying the seconds within the minute ๐ If you don't include it, it's implicitly set to `"0"`. sneaky, but fair.
๐ Command specification ๐
The subroutine that actually fires when the `$timespec` matches? You can hand it over in a few different ways, because options are self-care ๐ซถ
First up: if the optional `arguments` are missing entirely, the default dispatching subroutine you set at construction time gets called โ with no arguments. simple. clean.
If the second parameter you pass is a reference to a subroutine, that subroutine gets used instead of the dispatcher. main character energy ๐
Any additional parameters you tack on get handed down as arguments to the subroutine that runs. You can also pass a reference to an array instead of a whole list of parameters โ whatever feels right โจ
You can ALSO use a named parameter list, handed over as a hashref. Here's what she recognizes:
`subroutine`,`sub`โ Reference to subroutine to be executed`arguments`,`args`โ Reference to array containing arguments to be used when calling the subroutine`eval`โ If true, use the evaled string provided with the`"arguments"`parameter. The evaluation happens immediately (not when the subroutine is going to be called). heads up on that timing bestie โฐ
Examples:
$cron->add_entry("* * * * *");
$cron->add_entry("* * * * *","doit");
$cron->add_entry("* * * * *",\&dispatch,"first",2,"third");
$cron->add_entry("* * * * *",{'subroutine' => \&dispatch,
'arguments' => [ "first",2,"third" ]});
$cron->add_entry("* * * * *",{'subroutine' => \&dispatch,
'arguments' => '[ "first",2,"third" ]',
'eval' => 1});
๐ `@entries = $cron->list_entries()`
Hands you a whole list of cron entries. Each entry is a hash reference shaped like this:
$entry = {
time => $timespec,
dispatch => $dispatcher,
args => $args_ref
}
So `$timespec` is the time you specified in crontab format, exactly as you gave it to `add_entry`. `$dispatcher` is a reference to the dispatcher for that entry. And `$args_ref` is a reference to an array holding the extra arguments (it can totes be an empty array reference). For the full tea on those arguments, go read the docs for `add_entry` ๐
The order index of each entry is usable inside `update_entry`, `get_entry` and `delete_entry`. BUT โ ๏ธ be aware: once you delete an entry you have to re-fetch the list, because the order will have changed. she shifts, she reindexes, that's just how it is.
Heads up: these entries come back by value โ they were pulled from the internal list via a deep copy. Which means you're free to modify them all you want, but it will not touch the originals. If you actually need to change a real crontab entry, use `update_entry` instead. no cap.
๐ `$entry = $cron->get_entry($idx)`
Grabs a single entry. `$entry` is either a hashref with the possible keys `time`, `dispatch` and `args` (peep `list_entries()` if you forgot), or `undef` if there's no entry sitting at that `$idx`. sometimes the answer is just nothing, and that's valid ๐
๐ `$cron->delete_entry($idx)`
Deletes the entry at index `$idx`. Returns the deleted entry if it works, `undef` otherwise. bye bestie ๐
โจ `$cron->update_entry($idx,$entry)`
Updates the entry at index `$idx`. `$entry` is a hash ref as described in `list_entries()`, and it must contain at least a value for `$entry->{time}`. If no `$entry->{dispatcher}` is given, then the default dispatcher is used. This method returns the old entry on success, `undef` otherwise. glow up complete ๐
๐ `$cron->run([options])`
This is the one. This method starts the scheduler. ๐ฌ
Called with no options at all, this method will never return โ it just executes the scheduled subroutine calls as needed, forever, like a loyal bestie.
Alternatively you can detach the main scheduler loop from the current process (aka daemon mode ๐ง). In that case, the pid of the forked scheduler process gets returned to you.
The `options` parameter decides the running mode of `Schedule::Cron`. It can be either a plain list (which gets interpreted as a hash) or a reference to a hash. Here's what she recognizes:
`detach`โ If set to a true value, the scheduler process is detached from the current process (UNIX only bestie, don't try it elsewhere).`pid_file`โ If running in daemon mode, name the optional file where the process id of the scheduler process should be written. By default, no PID File will be created.`nofork`,`skip`,`catch`,`log`,`loglevel`,`nostatus`,`sleep`โ See`new()`for a description of these configuration parameters, which can be provided here too. Note that the options given here override those of the constructor. the run-time options ate and left no crumbs ๐ฅ
Examples:
# Start scheduler, detach from current process and
# write the PID of the forked scheduler to the
# specified file
$cron->run(detach=>1,pid_file=>"/var/run/scheduler.pid");
# Start scheduler and wait forever.
$cron->run();
๐ธ `$cron->clean_timetable()`
Remove all scheduled entries. total reset, fresh start, new era ๐
๐ `$cron->check_entry($id)`
Checks whether the given ID is already registered in the timetable. An ID is the first argument in the argument parameter of a crontab entry ๐
Returns (one of) the index in the timetable โ and yes, it can be 0, too โ if the ID could be found, or `undef` otherwise.
Example:
$cron->add_entry("* * * * *","ROTATE");
.
.
defined($cron->check_entry("ROTATE")) || die "No ROTATE entry !"
๐ฆ `$cron->get_next_execution_time($cron_entry,[$ref_time])`
OK so bestie, this one is mostly an internal method, but it might be useful all on its own ๐
The whole point of this method is to calculate the next execution time from a specified crontab entry.
Parameters:
`$cron_entry`โ The crontab entry as specified in`add_entry``$ref_time`โ The reference time for which the next time should be searched that matches`$cron_entry`. By default, it takes the current time.
This method returns the number of epoch-seconds of the next matched date for `$cron_entry` โณ
And since the author suspects that this calculation of the next execution time might fail in some circumstances (bugs are lurking everywhere ;-) โ see, even the manpage is being real with you โ an additional interactive method `bug()` is provided for checking crontab entries against your expected output. Refer to the top-level README for additional usage information for this method. go read it, it's giving thorough ๐
๐
`$cron->set_timeshift($ts)`
Modifies the global time shift for the entire timetable. The timeshift gets subbed from localtime to calculate the next execution time for all scheduled jobs. one number, and every job moves โจ
The `ts` parameter must be in seconds. Default value is `0`. Negative values are allowed if you want to shift time into the past (yes she can do that, reading the vibes backwards).
Returns the actual timeshift in seconds.
Example:
$cron->set_timeshift(120);
Will delay all jobs 2 minutes in the future. tiny push, big energy ๐ฎ
๐ธ DST ISSUES (the daylight saving drama) ๐ธ
okay bestie, buckle up, because daylight saving time is genuinely the villain of this whole story and we need to talk about it ๐ญ
DST happens like clockwork, twice a year. First switch: one hour just vanishes into thin air. So if a job triggers in that skipped hour, it gets fired in the next hour instead. Example, because examples ate: when the DST switch jumps from 2:00 to 3:00, a job scheduled for 2:43 will run at 3:43. That's the forward switch, and it's predictable, she behaves ๐ซถ
Now the backwards switch later in the year? the behavior is undefined. Not me using the scariest word in computing. Two possible things can happen:
- for jobs triggered in short intervals, where the next execution time would also fire in the extra hour, the job could be executed again or skipped in that extra hour.
As it stands right now: running Schedule::Cron in MET would skip the extra job, while in PST8PDT it would execute a second time. ๐
Why? It's all about how Time::ParseDate calculates epoch times for dates specified like:
02:50:00 2009/10/25
Like, should it return the seconds since 1970 for this time happening first, or for this time in the extra hour? Ambiguous! Iconic in the worst way. And as it turns out, Time::ParseDate returns the epoch time of the first occurrence for PST8PDT, and for MET it returns the second occurrence. Unfortunately there is no way to specify which entry Time::ParseDate should pick (until now, anyway).
And honestly? After all, this is obviously not Time::ParseDate's fault, because a simple date specification within the DST back-switch period is genuinely ambiguous. But it would be nice if the parsing behavior of Time::ParseDate were consistent across time zones (a ticket has been raised for fixing this). Then Schedule::Cron's behavior within a DST backward switch would be consistent as well. ๐
Since changing the internal algorithm which has worked now for over ten years would be too risky, and there's no simple solution in sight right now, it is likely that this undefined behavior will exist for some time. Maybe some hero is coming along and will fix this, but this is probably not me ;-)
Sorry for that. ๐
๐ AUTHORS
- Roland Huร
<roland@consol.de> - Currently maintained by Nicholas Hubbard
<nicholashubbard@posteo.net>
๐ซถ CONTRIBUTORS
- Alexandr Ciornii
<alexchorny@gmail.com> - Andrew Danforth
- Andy Ford
- Bray Jones
- Clinton Gormley
- Eric Wilhelm
- Frank Mayer
- Jamie McCarthy
- Loic Paillotin
- Nicholas Hubbard
<nicholashubbard@posteo.net> - Peter Vary
- Philippe Verdret
๐ COPYRIGHT AND LICENSE
Copyright (c) 1999-2013 Roland Huร.
Copyright (c) 2022-2023 Nicholas Hubbard.
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
and that's the whole page bestie!! ๐ this was Schedule::Cron(3pm) as shipped with perl v5.40.1, dated 2026-07-31. go forth and schedule slayfully, no cap ๐ฆโจ
๐ฆ where this page comes from
- ubuntu stonking: libschedule-cron-perl 1.07-2 ยท Artistic-1.0-Perl OR GPL-1.0-or-later
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms