views:

271

answers:

4

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 honors 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 bein 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 http://search.cpan.org/~roland/Schedule-Cron-0.99/lib/Schedule/Cron.pm

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.

+1  A: 

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.

malonso
I like that. Doesn't even seem that kludgey to me. One might even run the cron scripts hourly and then let them decide whether it's the right time or not (if hourly fits the schedule).
innaM
I was thinking about creating a DateTime object having the details of the time in PT and then using one of the Perl scheduler modules to carry out the execution of the task. I am wondering, however, how to make sure this does not lead to issues when DST rules change/gets disabled. This is an decision I need help on.
PoorLuzer
Why does it need to be in PT? Can you just have it us UTC?
malonso
+2  A: 

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.

Sinan Ünür
@Sinan: This works except that the seconds from the epoch will change twice a year, as his script must run at the same Pacific Time with respect to Daylight Savings Time. In the fall, he will need to add an hour, and subtract it again in the spring.
Adam Bellaire
EXCELLENT understanding Adam! This is an issue I am worried about. Writing code is not the issue - the problem is that it would become a custom snippet instead of something that is part a standrd CPAN module. What I am wondering is - is there already no module/standardized way of getting this done without writing custom code ( I would need to write documentation, test cases and test data to make sure my custom code is correct as this would certainly make into production code ) ?
PoorLuzer
A: 

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 with DateTime->now or extract an epoch time from your object and compare that with the results of time.

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.

daotoad
+1  A: 

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)
Greg Bacon
EXCELLENT work!
PoorLuzer