Commit 67a0e58e authored by Leigh Stoller's avatar Leigh Stoller

Add automated cancellation of reservations that are not used:

* If unused at six hours, schedule for cancel in three hours and send
  email.

* If reservation becomes used within those three hours, rescind the
  cancellation.

* Add an override bit so that cancel/uncancel on the command line
  supercedes (so explicit cancel or rescinding a cancel, means do not
  make any more automated checks for unused).

* Rework cancel to be more library friendly.
parent df988724
......@@ -216,6 +216,9 @@ sub Lookup($$;$$$$)
$self->{'APPROVED'} = $record->{'a'};
$self->{'APPROVER'} = $record->{'approver'};
$self->{'UUID'} = $record->{'uuid'};
$self->{'NOTIFIED'} = $record->{'notified'};
$self->{'NOTIFIED_UNUSED'} = $record->{'notified_unused'};
$self->{'OVERRIDE_UNUSED'} = $record->{'override_unused'};
# For compat with history entries.
$self->{'DELETED'} = undef;
# Local temp datastore
......@@ -243,6 +246,9 @@ sub admin_notes($) { return $_[0]->{"ADMIN_NOTES"}; }
sub approved($) { return $_[0]->{"APPROVED"}; }
sub approver($) { return $_[0]->{"APPROVER"}; }
sub uuid($) { return $_[0]->{"UUID"}; }
sub notified($) { return $_[0]->{"NOTIFIED"}; }
sub notified_unused($) { return $_[0]->{"NOTIFIED_UNUSED"}; }
sub override_unused($) { return $_[0]->{"OVERRIDE_UNUSED"}; }
# For compat with history entries.
sub deleted($) { return $_[0]->{"DELETED"}; }
# Get/Set some temporary extra data.
......@@ -318,13 +324,6 @@ sub SetAdminNotes($$)
$self->{'ADMIN_NOTES'} = $notes;
}
sub SetCancel($$)
{
my ($self, $cancel) = @_;
$self->{'CANCEL'} = $cancel;
}
# Mark the reservation as approved. This DOES NOT update the database
# state: to do so requires an admission control check! See BeginTransaction(),
# IsFeasible(), Book(), etc.
......@@ -433,7 +432,6 @@ sub Book($;$)
my $admin_notes = DBQuoteSpecial( $self->admin_notes() );
my $approved = $self->approved();
my $approver = $self->approver();
my $cancel = $self->cancel();
my $base_query = "SET pid='$pid', " .
"pid_idx='$pid_idx', " .
......@@ -449,8 +447,6 @@ sub Book($;$)
", admin_notes=$admin_notes" : "" ) .
( defined( $approved ) ?
", approved=FROM_UNIXTIME($approved)" : "" ) .
( defined( $cancel ) ?
", cancel=FROM_UNIXTIME($cancel)" : ", cancel=null" ) .
( defined( $approver ) ? ", approver='$approver'" : "" );
my $query_result =
......@@ -1278,6 +1274,9 @@ sub LookupHistorical($$) {
$self->{'APPROVED'} = undef;
$self->{'APPROVER'} = undef;
$self->{'UUID'} = $record->{'uuid'};
$self->{'NOTIFIED'} = undef;
$self->{'NOTIFIED_UNUSED'} = undef;
$self->{'OVERRIDE_UNUSED'} = 0;
bless($self, $class);
# Local temp datastore
$self->{'DATA'} = {};
......@@ -1860,5 +1859,65 @@ sub ReservableTypes($)
return @result;
}
#
# Mark the reservation for cancellation because it has not been used.
# We also mark the notified_unused flag so that we do not interefere
# with command line cancellation (and cancel cancellation).
#
# We are wrapped in a transaction by the caller.
#
sub MarkUnused($$)
{
my ($self, $when) = @_;
my $idx = $self->idx();
if ($when) {
DBQueryWarn("update future_reservations set ".
" cancel=FROM_UNIXTIME($when), notified_unused=now() ".
"where idx='$idx'")
or return -1;
}
else {
DBQueryWarn("update future_reservations set ".
" cancel=null, notified_unused=null ".
"where idx='$idx'")
or return -1;
}
$self->{'CANCEL'} = $when;
$self->{'NOTIFIED_UNUSED'} = $when;
return 0
}
sub ClearUnused($) { Reservation::MarkUnused($_[0], undef); }
#
# Similiar operation for cancel, although in this case we also clear
# notified_unused flag so that the unused reservation checking code
# does not interfere with a command line cancel/abortcancel.
#
sub MarkCancel($$)
{
my ($self, $when) = @_;
my $idx = $self->idx();
if ($when) {
DBQueryWarn("update future_reservations set ".
" cancel=FROM_UNIXTIME($when), ".
" notified_unused=null, override_unused=1 ".
"where idx='$idx'")
or return -1;
}
else {
DBQueryWarn("update future_reservations set ".
" cancel=null, notified_unused=null, override_unused=1 ".
"where idx='$idx'")
or return -1;
}
$self->{'CANCEL'} = $when;
$self->{'NOTIFIED_UNUSED'} = undef;
$self->{'OVERRIDE_UNUSED'} = 1;
return 0;
}
sub ClearCancel($) { Reservation::MarkCancel($_[0], undef); }
# _Always_ make sure that this 1 is at the end of the file...
1;
......@@ -2233,6 +2233,7 @@ CREATE TABLE `future_reservations` (
`approver` varchar(8) DEFAULT NULL,
`notified` datetime DEFAULT NULL,
`notified_unused` datetime DEFAULT NULL,
`override_unused` tinyint(1) NOT NULL default '0',
`uuid` varchar(40) NOT NULL default '',
PRIMARY KEY (`idx`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
......
use strict;
use libdb;
use emdb;
sub DoUpdate($$$)
{
my ($dbhandle, $dbname, $version) = @_;
if (!DBSlotExists("future_reservations", "override_unused")) {
DBQueryFatal("alter table future_reservations add ".
" `override_unused` tinyint(1) NOT NULL default '0' ".
" after notified_unused");
}
return 0;
}
# Local Variables:
# mode:perl
# End:
#!/usr/bin/perl -w
#
# Copyright (c) 2017 University of Utah and the Flux Group.
# Copyright (c) 2017-2018 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
......@@ -52,6 +52,9 @@ use emdb;
use libtestbed;
use emutil;
use Reservation;
use ResUtil;
use User;
use Project;
#
# Turn off line buffering on output
......@@ -194,4 +197,119 @@ while( my ($idx, $pid, $count, $type, $start, $end, $email, $cancel) =
$TBOPS);
}
#
# Handle reservations that are unused within the first six hours. The
# idea is that we start check after four hours. If idle we mark it for
# cancel in two hours, and then if they start using it, we cancel the
# cancelation. We stop checking after a while, in case we missed it
# cause the testbed was down; do not want to keep checking cause its
# expensive.
#
$query_result = DBQueryFatal("SELECT r.idx,".
" TIMESTAMPDIFF(MINUTE, r.start, now())/60 ".
" from future_reservations as r ".
"where r.approved IS NOT NULL AND " .
" r.start <= NOW() AND ".
" r.override_unused=0 AND ".
" (TIMESTAMPDIFF(MINUTE, r.start, now()) > ".
" (6 * 60)) AND ".
" (TIMESTAMPDIFF(MINUTE, r.start, now()) < ".
" (10 * 60))");
while (my ($idx, $hours) = $query_result->fetchrow_array()) {
my $res = Reservation->Lookup($idx);
next
if (!defined($res));
my $project = Project->Lookup($res->pid());
next
if (!defined($project));
eval {
my @records = CreateTimeline($project, $res);
ReservationUtilization($res, @records);
};
if ($@) {
print STDERR $@;
next;
}
my $utilization = $res->data("utilization");
print STDERR "$res - utilization:$utilization\n"
if ($debug);
if ($utilization == 0.0) {
# Skip if cancel already set; do not interfere with a command
# line operation that set the cancel.
next
if ($res->cancel());
# Mark for cancelation.
print "Marking unused reservation for cancellation.\n"
if ($debug);
next
if ($impotent);
my $cancel = time() + (3 * 3600);
while (1) {
if (!defined(Reservation->
BeginTransaction(Reservation->GetVersion()))) {
sleep(1);
next;
}
$res->MarkUnused($cancel);
Reservation->EndTransaction();
last;
}
my $user = User->Lookup($res->uid());
if (!defined($user)) {
print STDERR "Could not lookup user " . $res->uid() ."\n";
next;
}
my $email = $user->email();
my $count = $res->nodes();
my $pid = $res->pid();
my $type = $res->type();
my $start = TBDateStringUTC($res->start());
my $end = TBDateStringUTC($res->end());
$cancel = TBDateStringUTC($cancel);
SENDMAIL($email, "$SITE reservation",
"Your reservation for $count $type nodes in project $pid,\n" .
"starting at $start and ending at $end,\n".
"has not been used since it started.\n".
"\n".
"We have marked your reservation for cancellation at $cancel.\n".
"\n".
"If you use your reservation before then, we will rescind\n".
"the cancellation.\n",
$TBOPS);
}
elsif ($res->notified_unused()) {
#
# We only cancel our own cancellation. If the command line tool
# aborts the cancel (or even sets the cancel), we view that as an
# override on our cancel (the notified_unused flag is cleared on
# that path, and override_unused is set).
#
print "Clearing unused reservation cancellation.\n"
if ($debug);
next
if ($impotent);
while (1) {
if (!defined(Reservation->
BeginTransaction(Reservation->GetVersion()))) {
sleep(1);
next;
}
$res->ClearUnused();
Reservation->EndTransaction();
last;
}
}
}
exit( 0 );
......@@ -464,14 +464,17 @@ if ($cancel || $abortcancel) {
exit( 1 );
}
$res->SetCancel(($abortcancel ? undef : $cancel));
while (1) {
if (!defined(Reservation->BeginTransaction(Reservation->GetVersion()))) {
sleep(1);
next;
}
$res->Book($res->idx());
if ($abortcancel) {
$res->ClearCancel();
}
else {
$res->MarkCancel($cancel);
}
Reservation->EndTransaction();
last;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment