/usr/local/CPAN/Chronic/Schedule/Chronic.pm
#
# Constraint-based, opportunistic scheduler.
# Author: Vipul Ved Prakash <mail@vipul.net>.
# $Id: Chronic.pm,v 1.14 2005/04/26 07:25:34 hackworth Exp $
#
package Schedule::Chronic;
use base qw(Schedule::Chronic::Base Schedule::Chronic::Tab);
use Schedule::Chronic::Timer;
use Schedule::Chronic::Logger;
use Data::Dumper;
sub new {
my ($class, %args) = @_;
my %self = (%args);
$self{safe_sleep} ||= 1; # 1 second
$self{scheduler_wait} = new Schedule::Chronic::Timer ('down');
$self{var} ||= '/var/run';
$self{max_sw} = 10 * 60; # 10 minutes
$self{only_once_tw} = 10 * 365 * 24 * 3600; # 10 years
$self{logger} = new Schedule::Chronic::Logger (type => $args{logtype});
$self{nohup} = 0;
$self{pending_hup} = 0;
unless (exists $self{debug}) {
$self{debug} = 1;
}
return bless \%self, $class;
}
sub load_cns_for_schedule {
my ($self) = @_;
for my $task (@{$$self{_schedule}}) {
unless (exists $task->{_sched_constraints_loaded}) {
$self->load_cns_for_task($task);
}
}
}
sub load_cns_for_task {
my ($self, $task) = @_;
my $constraints = $task->{constraints};
my $n_objects = 0;
my $prep_args = sub {
my $topass = Dumper shift;
$topass =~ s/\$VAR1 = \[//;
$topass =~ s/];\s*//g;
return $topass;
};
for my $constraint (keys %$constraints) {
# Load the module corresponding to the constraint from
# disk. Die with a FATAL error if the module is not
# loaded. This behaviour should be configurable through
# a data member.
my $module = "Schedule::Chronic::Constraint::$constraint";
eval "require $module; use $module";
if ($@) {
my $error = join'', $@;
if ($error =~ m"Can't locate (\S+)") {
$self->fatal("Cant' locate constraint module ``$1''");
}
}
# Call the constructor and then the init() method to
# pass the constraint object a copy of schedule, task
# and thresholds/parameters supplied by the user. Save
# the constraint object under the constraint key.
my $constructor = "$module->new()";
$task->{constraints}->{$constraint}->{_object} = eval $constructor or die $!;
my $object = $task->{constraints}->{$constraint}->{_object};
my $init = $object->init (
$$self{_schedule}, $task, $$self{logger},
@{$task->{constraints}->{$constraint}->{thresholds}}
);
unless ($init) {
$self->fatal("init() failed for $module")
}
$n_objects++;
}
# All's good.
$self->debug("$n_objects constraint objects created.");
$task->{_sched_constraints_loaded} = 1;
# print Dumper $self;
return 1;
}
sub schedule {
my $self = shift;
my $schedule = $$self{_schedule};
my $scheduler_wait = $$self{scheduler_wait};
# A subroutine to compute a scheduler wait, which is the the
# smallest of all task waits. We call this routine after
# we've run through all tasks at least once. This function
# is closed under schedule() so it has access to variables
# local to schedule.
my $recompute_scheduler_wait = sub {
unless (scalar @{$schedule}) {
# Oops, there are no tasks. We'll set wait to
# maximum and hope that tasks show up the next time
# this function is called.
$self->debug("no tasks to schedule.");
$scheduler_wait->set($$self{max_sw});
$self->debug("scheduler_wait: set to " . $self->time_rd($$self{max_sw}));
return;
}
my $sw = $schedule->[0]->{_task_wait}->get();
for my $task (@$schedule) {
if ($$task{_task_wait}->get() < $sw) {
$sw = $$task{_task_wait}->get();;
}
}
$sw = $self->{max_sw} if $sw > $self->{max_sw};
if ($sw > 0) {
$scheduler_wait->set($sw);
$self->debug("scheduler_wait: set to " . $self->time_rd($sw));
}
};
$self->debug("entering scheduler loop...");
while (1) {
# Check to see if scheduler_wait is positive. If so,
# go to sleep because all task waits are larger than
# scheduler_wait.
if ($scheduler_wait->get() > 0) {
$self->debug("nothing to schedule for " .
$self->time_rd($scheduler_wait->get()) . ", sleeping...");
sleep($scheduler_wait->get());
}
# Walk over all tasks, checks constraints and execute tasks when
# all constraints are met. This is section should end in
TASK:
for my $task (@$schedule) {
# print Dumper $task;
# A task has four components. A set of constraints, a
# command to run when these constraints are met, the
# last_ran time and a task wait timer which is the
# maximum wait time returned by a constraint.
my $constraints = $$task{constraints};
my $task_wait = $$task{_task_wait};
my $command = $$task{command};
my $last_ran = $$task{last_ran};
my $uid = $$task{_uid};
my $only_once = $$task{only_once};
if ($last_ran > 0 and $only_once == 1) {
# This task was supposed to run ``only_once'' and it has
# been run once before, so we will skip it.
$task_wait->set($$self{only_once_tw});
next TASK;
}
$self->debug("* $command");
if ($task_wait->get() > 0) {
# Constraints have indicated that they will not be met for
# at least sched_wait seconds.
$self->debug(" task_wait: " . $self->time_rd($task_wait->get()));
next TASK;
};
my $all_cns_met = 1;
for my $constraint (keys %$constraints) {
# A constraint has two declarative components and a few
# derived components. The declarative components are the
# name of the constraint and the thresholds that
# parameterize the constraint. The derived components
# include the corresponding constraint object and other
# transient data structures used by the scheduler.
my $cobject = $task->{constraints}->{$constraint}->{_object};
# Now call met() and wait()
my ($met) = $cobject->met();
my ($wait) = $cobject->wait();
if (not $met) {
# The constraint wasn't met. We'll set all_cns_met to
# 0 and compare constraint wait with task_wait to see
# if we need to readjust task_wait.
$self->debug(" ($constraint) unmet");
$all_cns_met = 0;
if ($wait != 0 && $wait > $task_wait->get()) {
# Task wait is largest of all constraint waits.
$self->debug(" ($constraint) won't be met for " . $self->time_rd($wait));
$task_wait->set($wait);
}
} else {
# The constraint has been met. Add a log notification.
# We don't need to do anything. If all constraints are
# met, all_cns_set will remain set to 1.
$self->debug(" ($constraint) met");
}
} # for - iterate over constraints
if ($all_cns_met) {
# All constraints met: the task is ready to run.
# Set nohup to 1. Tells the SIGNAL handler that
# this is not a good time for a HUP. If we
# receive a HUP during system(), the handler
# will record this in $self->{pending_hup} so we
# can replay the signal after system() is done.
$self->{nohup} = 1;
my $now = time();
$$task{_previous_run} = $now - $$task{last_ran};
$$task{last_ran} = $now;
my $rv = system($$task{command});
$$task{_last_rv} = $rv;
# Write the chrontab with updated last_ran value for the
# task only if the task is not an ``only_once'' task.
$self->write_chrontab($$task{_chrontab});
# Notify the email address.
if ($$task{notify}) {
$self->notify($task, time() - $$task{last_ran});
}
$self->{nohup} = 0;
if ($self->{pending_hup}) {
# If there got a HUP during system();
# replay it now.
$self->debug("replaying HUP signal sent earlier");
kill(1, $$self{pid});
}
}
} # for - iterate over tasks
# Compute the schedular wait before going through the next
# cycle. Scheduler wait is set only if the largest
# task_wait is > 0.
&$recompute_scheduler_wait();
# We'll do a one second sleep here so we don't cycle out
# of control when there's a mismatch between task_wait's
# and the scheduler_wait.
sleep ($self->{safe_sleep});
} # while - scheduler loop
}
sub getpid {
my ($self) = @_;
$self->{pid} = $$;
}
sub notify {
my ($self, $task, $time) = @_;
# Sometimes /usr/lib won't be in path, so we look there first before
# calling which()
my $success = $$task{_last_rv} == 0 ? 1 : 0;
my $sendmail_path = '/usr/lib/sendmail';
unless (-e $sendmail_path) {
$sendmail_path = $self->which('sendmail');
}
unless ($sendmail_path) {
$self->debug("``sendmail'' not found, can't notify");
return;
}
$self->debug(" sending notification to $$task{notify}");
my $template;
# Headers
$template .= "From: chronic\@localhost\n"; # FIX. username@host
$template .= "To: $$task{notify}\n";
$template .= "Subject: [Chronic] Success: $$task{command}\n\n" if $success;
$template .= "Subject: [Chronic] Failure: $$task{command}\n\n" unless $success;
# Body
$template .= "Task executed successfully.\n\n" if $success;
$template .= "\nTask failed.\n\n" unless $success;
$template .= sprintf("%20s: %s\n", "Task", $$task{command});
$template .= sprintf("%20s: %s\n", "Executed at", scalar localtime());
$template .= sprintf("%20s: %s\n", "Run time", $self->time_rd($time) . ".");
$template .= sprintf("%20s: %s\n", "Return Value", $$task{_last_rv});
$template .= sprintf("%20s: %s\n", "UID", $$task{_uid});
$template .= sprintf("%20s: %s\n", "Previous run", $self->time_rd($$task{_previous_run}) . " ago.")
if exists $$task{_previous_run} and $$task{only_once} == 0;
$template .= "\nThis was an ``only_once'' task. It won't be rescheduled.\n" if $$task{only_once};
$template .= "\nVirtually yours,\nChronic\n";
open(SENDMAIL, "| $sendmail_path $$task{notify}");
print SENDMAIL $template;
print SENDMAIL ".\n";
close SENDMAIL;
return $self;
}
sub time_rd {
my ($self, $seconds) = @_;
if ($seconds > 3600) {
my $hours = $seconds / 3600;
if ($hours > 24) {
return sprintf("%.2f days", $hours/24);
} else {
return sprintf("%.2f hours", $hours);
}
} elsif ($seconds > 60) {
return sprintf("%.1f minutes", $seconds/60);
}
return "$seconds seconds";
}
1;