Scheduling scripts at a different timezone
There are some programs/scripts that need to be run at specific times in a timezone different from the system timezone.
A la crontab in Perl, but one that ho开发者_如何学编程nors a timezone and DST rules in a region different from that in which the system is configured.
Here is the use case : I will create an excel sheet with the time in PT in column B and the corresponding program/Perl script to run in column C.
Nothing specific about this information being in a Excel sheet - could be plain text file/"crontab" entry too.
A Perl script will read in the data from the excel sheet and run/spawn those scripts at the correct time.
The thing to keep at mind is that the Perl script should run correctly regardless of what timezone the system that it is running on is.
Regardless of whether the script is running on a Box in NY or IL or CA, it should spawn the scripts at the time mentioned in the file entries as per the Pacific Standard Time with DST at mind.
It is very important, as I said before, of it being aware, "automagically" ( without me doing any explicit programmming ) of the latest DST rules for the PT region.
What would you suggest?
Maybe I can visit some website that shows current time in that region and scan the time value from it, and run the scripts when it's the correct time?
Any such Perl screen scraper friendly site?
Or maybe I can use some smart Perl module, like Schedule::Cron
For the record, a large number of good suggestions came by at http://www.perlmonks.org/index.pl?node_id=772934, however, they, in typical at/cron fashion, work as per the system configured timezone.
In general, if you care about timezones, represent times internally in some universal format and convert times for display purposes only.
Applying this to your problem, write a crontab whose times are expressed in GMT. On each worker machine, convert to local time and install the crontab.
Front matter:
#! /usr/bin/perl
use warnings;
use strict;
use feature qw/ switch /;
use Time::Local qw/ timegm /;
For the conversions this program supports, use today's date and substitute the time from the current cronjob. Return the adjusted hour and day-of-week offset:
sub gmtoday {
my($gmmin,$gmhr,$gmmday,$gmmon,$gmwday) = @_;
my @gmtime = gmtime $^T;
my(undef,undef,$hour,$mday,$mon,$year,$wday) = @gmtime;
my @args = (
0, # sec
$gmmin eq "*" ? "0" : $gmmin,
$gmhr,
$mday,
$mon,
$year,
);
my($lhour,$lwday) = (localtime timegm @args)[2,6];
($lhour, $lwday - $wday);
}
Take the five-field time specification from the current cronjob and convert it from GMT to local time. Note that a fully general implementation would support 32 (i.e., 2 ** 5) cases.
sub localcron {
my($gmmin,$gmhr,$gmmday,$gmmon,$gmwday) = @_;
given ("$gmmin,$gmhr,$gmmday,$gmmon,$gmwday") {
# trivial case: no adjustment necessary
when (/^\d+,\*,\*,\*,\*$/) {
return ($gmmin,$gmhr,$gmmday,$gmmon,$gmwday);
}
# hour and maybe minute
when (/^(\d+|\*),\d+,\*,\*,\*$/) {
my($lhour) = gmtoday @_;
return ($gmmin,$lhour,$gmmday,$gmmon,$gmwday);
}
# day of week, hour, and maybe minute
when (/^(\d+|\*),\d+,\*,\*,\d+$/) {
my($lhour,$wdoff) = gmtoday @_;
return ($gmmin,$lhour,$gmmday,$gmmon,$gmwday+$wdoff);
}
default {
warn "$0: unhandled case: $gmmin $gmhr $gmmday $gmmon $gmwday";
return;
}
}
}
Finally, the main loop reads each line from the input and generates the appropriate output. Note that we do not destroy unhandled times: they instead appear in the output as comments.
while (<>) {
if (/^\s*(?:#.*)?$/) {
print;
next;
}
chomp;
my @gmcron = split " ", $_, 6;
my $cmd = pop @gmcron;
my @localcron = localcron @gmcron;
if (@localcron) {
print join(" " => @localcron), "\t", $cmd, "\n"
}
else {
print "# ", $_, "\n";
}
}
For this sorta-crontab
33 * * * * minute only
0 0 * * * minute and hour
0 10 * * 1 minute, hour, and wday (same day)
0 2 * * 1 minute, hour, and wday (cross day)
the output is the following when run in the US Central timezone:
33 * * * * minute only
0 18 * * * minute and hour
0 4 * * 1 minute, hour, and wday (same day)
0 20 * * 0 minute, hour, and wday (cross day)
In the schedule, store the number of seconds from the epoch when each run should occur rather than a date/time string.
Expanding a little:
#!/usr/bin/perl
use strict; use warnings;
use DateTime;
my $dt = DateTime->new(
year => 2010,
month => 3,
day => 14,
hour => 2,
minute => 0,
second => 0,
time_zone => 'America/Chicago',
);
print $dt->epoch, "\n";
gives me
Invalid local time for date in time zone: America/Chicago
because 2:00 am on March 14, 2010 is when the switch occurs. On the other hand, using hour => 3
, I get: 1268553600
. Now, in New York, I use:
C:\Temp> perl -e "print scalar localtime 1268553600" Sun Mar 14 04:00:00 2010
So, the solution seems to be to avoid scheduling these events during non-existent times in your local time zone. This does not require elaborate logic: Just wrap the DateTime
constructor call in an eval
and deal with the exceptional time.
While I certainly think that there are likely "cleaner" solutions, would the following work?
set the cron to run the scripts several hours ahead of the possible range of times you actually want the script to run
handle the timezone detection in the script and have it sleep for the appropriate amount of time
Again, I know this is kinda kludgey but I thought I would put it out there.
Use the DateTime module to calculate times.
So if your setup says to run a script at 2:30 am every day, you will need logic to:
Try to create a
DateTime
object for 2:30am in timezone America\Los_Angeles.If no object add 5 minutes to the time and try again. Give up after 2 hours offset.
Once you have a
DateTime
object, you can do comparisons withDateTime->now
or extract an epoch time from your object and compare that with the results oftime
.
Note that I chose 2:30 am, since that time won't exist at least 1 day a year. That's why you need to have a loop that adds an offset.
精彩评论