# -*-perl-*-

# $Id: OffLine.plm,v 1.10 2007/02/28 21:20:48 thomas Exp $

# $Log: OffLine.plm,v $
# Revision 1.10  2007/02/28 21:20:48  thomas
# Multiple simultaneous writer enhancement
#
# Revision 1.9  2004/02/24 15:02:03  thomas
# commit before 0.3.12 fork
#
# Revision 1.8  2003/06/09 14:45:11  thomas
# schema version of database
#
# Revision 1.7  2003/06/04 17:30:53  thomas
# *** empty log message ***
#
# Revision 1.6  2002/09/10 20:42:57  thomas
# sushi-0.3.1
#
# Revision 1.5  2002/08/20 21:59:56  thomas
# installed 0.3.0
#
# Revision 1.4  2002/08/14 17:13:07  thomas
# 0.2.9 release
#
# Revision 1.3  2002/07/23 16:02:28  thomas
# version 0.2.5
#
# Revision 1.2  2002/07/10 14:01:28  thomas
# sushi-0.2.4
#
# Revision 1.1  2002/07/08 15:56:33  thomas
# 0.2.3 release
#

package Sushi::OffLine;

use strict;
use Time::localtime;
use Time::Local;
use Data::Dumper;

use base qw(Sushi::Log);

###############################################################################
# Create a new OffLine object.  The actual object class is based on
# the type specified here.

sub new {
  my $class = shift;
  my $args = { @_ };
  my $parms = {};

  # Transfer over interesting pieces from $args
  foreach ('storage_id', 'storage_info', 'type', 'debug', 'mountpoint') {
    defined ($args->{$_}) and $parms->{$_} = $args->{$_};
  }

  my $self = {
	      debug      => $::debug,
	      %$parms
	     };

  bless $self, "Sushi::OffLine::$self->{type}";

  if (!$self->prepare) {
    return undef;
  }

  return $self;
}

###############################################################################
# Return a group name from $args.  If not set, use last month.
# Do it in one place for consistency.

sub setinfo {
  my $self = shift;
  my $args = shift;

  my ($begintime, $endtime);
  my ($month, $year);

  # Save set name
  if (!defined $args->{saveset_name}) {
    ($month, $year) = (localtime->mon()+1, localtime->year()+1900);
    if ($month == 1) {
      ($month, $year) = (12, $year-1);
    } else {
      $month--;
    }
    $args->{saveset_name} = sprintf "%04d.%02d", $year, $month;
    print "Using $args->{saveset_name}\n";
  }

  # Force the month and year from the saveset name, just in case.
  if ($args->{saveset_name} =~ /^(\d\d\d\d).(\d\d)$/) {
    ($month, $year) = ($2, $1);
  } else {
    $::log->error ("Can't parse saveset name ($args->{saveset_name}).\n");
    return ($args->{saveset_name}, $begintime, $endtime);
  }

  # begin and end dates
  $begintime = timelocal (0, 0, 0, 1, $month-1, $year);

  if ($month==12) {
    $endtime = timelocal (0, 0, 0, 1, 0, $year+1)-1;
  } else {
    $endtime = timelocal (0, 0, 0, 1, $month, $year)-1;
  }

  return ($args->{saveset_name}, $begintime, $endtime);
}

###############################################################################
# Print summary information

sub scan {
  my $self = shift;
  my $args = shift;
  my $opts = shift;

  my $epochs = $self->getEpochList($args);

  # Get some stats
  my $idx;
  my %totals = (
                comp_size => 0,
                raw_size  => 0,
                runtime   => 0,
		archived  => 0
               );
  if (defined $args->{display_after}) {
    Sushi::GenDB::displayHeader (
				 { print_headings => 1 },
				 $::table_defs->{view_results_offline}
				);
  }
  foreach $idx (sort keys %$epochs) {
    if ((defined ($args->{display_after}) &&
	 ($epochs->{$idx}->{timestamp} > $args->{display_after})) ||
	(defined ($opts->{not_written}) &&
	 !defined ($epochs->{$idx}->{storage_id}))) {
      # print "Epoched $_ on " . ctime($epochs->{$idx}->{timestamp}) . ".\n";
      Sushi::GenDB::displayRow (
				{},
				$::table_defs->{view_results_offline},
				$epochs->{$idx}
			       );
    }

    foreach my $attr (keys %totals) {
      $totals{$attr} += $epochs->{$idx}->{$attr};
    }
    defined ($epochs->{$idx}->{storage_id}) and $totals{archived}++;
  }
  my $atoms = scalar keys %$epochs;


  print "\n\n$atoms atoms epoched\n" .
    "$atoms atoms to archive, $totals{archived} already archived\n" .
    "Total run time $totals{runtime} (" .
    int (100*$totals{runtime}/$atoms)/100 . " secs/atom)\n" .
    "Total raw data " . int($totals{raw_size}/(1024*10.24))/100 . " MB (" .
    int($totals{raw_size}/$atoms/10000)/100 . " MB/atom, " .
    int($totals{raw_size}/($totals{runtime}*1024*10.24))/100 . " MB/sec)\n" .
    "Total compressed data " .
    int($totals{comp_size}/10000)/100 . " MB (" .
    int($totals{comp_size}/$atoms/10000)/100 . " MB/atom, " .
    int($totals{comp_size}/($totals{runtime}*1024*10.24))/100 . " MB/sec)\n",
    "Average compression ratio " .
    int(100*$totals{raw_size}/$totals{comp_size})/100 . "\n";
}

###############################################################################
# This is the function that writes out monthly media

sub monthlysave {
  my $self = shift;
  my $args = shift;
  my $opts = shift;

  my ($media, $epoch, $row, $available, $epochkey, $mediakey);
  my ($m, $status, $return_code, $commonName);
  my ($foundspace, $numfull, $numbusy, $pool);

  my ($setname) ; ($setname) = Sushi::OffLine->setinfo ($args);

  # I.  Make sure all the storage devices exist and calculate
  # available space.
  $::log->log (2, "Checking free space on media...\n");
  $available = 0;
  foreach $mediakey (@{$args->{media}}) {
    $pool = Sushi::GenDB::fetch (
				 {storage_id => $mediakey},
				 {},
				 $::table_defs->{offline_pool},
				);
    if (ref($pool) eq 'ARRAY' && @$pool == 1) {
      $media->{$mediakey} = $pool->[0];
    } else {
      $::log->error ("Failed to find offline pool entry for $mediakey.  Skipping.\n");
      next;
    }

    $m = $media->{$mediakey};

    # Ready the device
    $m->{handle} = new Sushi::OffLine (
				       storage_id => $m->{storage_id},
				       type       => $m->{media_type}
				      );
    if (!defined $m->{handle}) {
      $::log->error ("Failed to prepare storage device $mediakey. \n");
      # Don't use this device
      delete $media->{$mediakey};
      next;
    }
    $::log->log (2, "Opened offline device $mediakey.\n");

    # Find out how much of the device is used.
    $m->{used_bytes} = 0;
    foreach $row (@{Sushi::GenDB::fetch (
					 {
					  storage_id => $mediakey,
					  disposition => 'Available',
					 },
					 {},
					 $::table_defs->{view_results_offline}
					)}) {
      $m->{used_bytes} += $row->{comp_size};
    }
    if ($m->{media_size} > $m->{used_bytes}) {
      $available += $m->{media_size} - $m->{used_bytes};
    }
  }

  # Ia.  Make sure I have at least one device available
  if (!keys %$media) {
    $::log->error ("No usable media provided.  Exitting.\n");
    return -1;
  }

  # II.  Get epoch list and print stats
  $::log->log (2, "Building epoch list...\n");
  my $epochs = $self->getEpochList ($args);

  my %stats = (
	       total_bytes => 0,
	       done_bytes => 0,
	       total_atoms => 0,
	       done_atoms => 0
	      );
  foreach my $idx (keys %$epochs) {
    $stats{atoms}++;
    $stats{comp_size} += $epochs->{$idx}->{comp_size};
    if (defined $epochs->{$idx}->{storage_id}) {
      $stats{done_atoms}++;
      $stats{done_comp_size} += $epochs->{$idx}->{comp_size};
      delete $epochs->{$idx};
    }
  }

  print  "For save set $setname:\n",
	 "$stats{atoms} atoms in set, " . ($stats{atoms} - $stats{done_atoms}) . " still to do.\n",
	 "$stats{comp_size} bytes in set, " . ($stats{comp_size} - $stats{done_comp_size}) . " still to write.\n",
	 "$available bytes available on media\n";
  if ( $stats{comp_size} - $stats{done_comp_size} > $available ) {
    $::log->error ("Epoch size > available space; I'll write as much as I can.\n");
  }


  # III.  Write epochs until we're done.
  while (1) {

    # See if anything just finished up
    foreach $mediakey (keys %$media) {
      $m = $media->{$mediakey};
      foreach $epoch ($m->{handle}->get_active) {
	($status, $return_code) = $m->{handle}->get_status ($epoch);
	next if $status==0; # Still Running
	# Mark the result
	$self->check_record_finished_epoch (
					    $m,
					    $epoch,
					    $status,
					    $return_code,
					    $setname,
					    );
	# And unmount the source volume
	if (!Sushi::AfsBits::unMountVolume (($epoch->{afs_volume}, $epoch->{mount_reason}))) {
	  $self->error ("Unmount of result volume $epoch->{afs_volume} failed.  Continuing...\n");
	}
	delete $epochs->{"$epoch->{type}:$epoch->{name}"};
      } # each $epoch
    } # each $mediakey


    # Try to start some atoms
    $numfull = 0;
    $numbusy = 0;
    foreach $mediakey (keys %$media) {
      $m = $media->{$mediakey};
      # Check for full disk or too many currently active atoms
      if ($m->{full}) {
	$numfull++;
	next;
      }
      if (!$m->{handle}->can_start_more_epochs) {
	$numbusy++;
	next;
      }

      # OK.  Find epoch(s) to start here...
      # Leave keys randomized to even out space on the media volumes.
      $foundspace=0;
      foreach $epochkey (keys %$epochs) {
	$epoch = $epochs->{$epochkey};
	next if $epoch->{working};
	next if $epoch->{comp_size} > $m->{media_size} - $m->{used_bytes};
	# OK.  We can start this epoch.

	$commonName = Sushi::Atom::commonName ($epoch);
	$foundspace++;
	# Allocate the space now so I don't over-fill the device
	$m->{used_bytes} += $epoch->{comp_size};
	$epoch->{mount_reason} = "Offline: $epoch->{schedule_group}:$epoch->{type}:$epoch->{name} for $m->{storage_id}";
	if (!defined ($epoch->{mount_mtpt} = Sushi::AfsBits::mountVolume ($epoch->{afs_volume}, $epoch->{mount_reason}))) {
	  $self->error ("Mount of result volume $epoch->{afs_volume} failed.  Bummer.\n");
	} elsif (!$m->{handle}->start_copy ($epoch)) {
	  $::log->error ("Failed to start epoch $commonName($epoch->{timestamp}) to $m->{storage_id}.\n");
	  # Don't try this epoch again
	} else {
	  $::log->log (2, "Started epoch $commonName($epoch->{timestamp}) to $m->{storage_id}.\n");
	  $epoch->{working}=1;
	}
	# If this epoch failed, don't try it again
	delete $epochs->{"$epoch->{type}:$epoch->{name}"} unless $epoch->{working};
	last unless $m->{handle}->can_start_more_epochs;
      }
      $m->{full}=1 unless $foundspace;
    } # each $mediakey

    # See if we're done.  When all remaining epochs are marked 'working'
    # the check above will report all media full.  So the %$epochs
    # check here probably isn't necessary.
    if (($numfull == keys %$media) || (keys %$epochs == 0)) {
      last;
    }

    # Don't swamp the CPU with useless checking
    sleep (1);

  } # while 1

  # IV. Wait for running epochs to finish
  foreach $mediakey (keys %$media) {
    $m = $media->{$mediakey};
    foreach $epoch ($m->{handle}->get_active) {
      $commonName = Sushi::Atom::commonName ($epoch);
      $::log->log (2, "Waiting for completion of $commonName($epoch->{timestamp}) on $mediakey.\n");
      ($status, $return_code) = $m->{handle}->get_status ($epoch);
      while (defined ($status) && !$status) {
	# Epoch running
	sleep(5);
	($status, $return_code) = $m->{handle}->get_status ($epoch);
      }
      # OK.  Done.  Mark the result
      $self->check_record_finished_epoch (
					  $m,
					  $epoch,
					  $status,
					  $return_code,
					  $setname,
					 );
      # And unmount the source volume
      if (!Sushi::AfsBits::unMountVolume (($epoch->{afs_volume}, $epoch->{mount_reason}))) {
	$self->error ("Unmount of result volume $epoch->{afs_volume} failed.  Continuing...\n");
      }
      # And we're done with this epoch
      delete $epochs->{"$epoch->{type}:$epoch->{name}"};
    }
    # Unmount the disk
    $m->{handle}->close;
  }

}

###############################################################################

sub check_record_finished_epoch {
  my $self = shift;
  my $media = shift;
  my $epoch = shift;
  my $status = shift;
  my $return_code = shift;
  my $setname = shift;

  my $commonName = Sushi::Atom::commonName ($epoch);
  if ($status == 1) {
    # The atom finished (somehow)
    if ($return_code) {
      $::log->error ("Failed to copy atom $commonName($epoch->{timestamp}) to $media->{pool}->{storage_id}.\n");
    } else {
      # Create the offline record
      my $info = $media->{handle}->getInfo ($epoch);
      if (!Sushi::GenDB::add (
			      {
			       atom_id => $epoch->{id},
			       result_timestamp => $epoch->{timestamp},
			       saveset_name => $setname,
			       write_time => time,
			       storage_id => $media->{storage_id},
			       storage_info => $info,
			       disposition => 'Available'
			      },
			      {},
			      $::table_defs->{offline}
			     )) {
	$::log->error ("Database offline record create failed for $commonName($epoch->{timestamp})!\n");
      } else {
	$::log->log (2, "Database offline record create succeeded for $commonName($epoch->{timestamp})!\n");
      }
      $::sudbh->commit;
    }
  } else {
    $::log->error ("Unknown copy status for epoch $commonName($epoch->{timestamp}) to $media->{pool}->{storage_id}.\n");
  }
}


###############################################################################
# Get a list of epochs that correspond to the monthly saves for the month
# and year passed in $args.  This is the earliest epoch of the month, or
# the latest epoch before the 1st if there are none in the month.

sub getEpochList {

  my $self = shift;
  my $args = shift;
  my $opts = shift;

  my $epochs = {};
  my $row;

  my ($setname, $begintime, $endtime) = Sushi::OffLine->setinfo ($args);

  # Get the current contents of the save set
  $::log->log (2, "Reading current save set $setname.\n");
  my $set = {};
  foreach $row (@{Sushi::GenDB::fetch (
				       {
					saveset_name => $setname
				       },
				       {},
				       $::table_defs->{view_results_offline}
				      )}) {
    $set->{"$row->{id}:$row->{timestamp}"} = $row;
  }

  $::log->log (2, "Building complete save set $setname.\n");
  # Now, build complete save set
  my $results = {};
  foreach $row (@{Sushi::GenDB::fetch (
				       {
					result => 'Success',
					on_disk => 'Yes',
					level => 0
				       },
				       {},
				       $::table_defs->{view_atom_results}
				      )}) {
    my $idx = "$row->{type}:$row->{name}";

    # Don't use epochs that are after the requested month
    ($row->{timestamp} > $endtime) and next;

    # Fill in information from saveset record if present
    my $saveidx = "$row->{id}:$row->{timestamp}";
    if (defined $set->{$saveidx}) {
      $row->{write_time}   = $set->{$saveidx}->{write_time};
      $row->{storage_id}   = $set->{$saveidx}->{storage_id};
      $row->{storage_info} = $set->{$saveidx}->{storage_info};
    }

    if (!defined $epochs->{$idx}) {
      $epochs->{$idx} = $row;
      next;
    }

    if ($epochs->{$idx}->{timestamp} >= $begintime) {
      if ($row->{timestamp} >= $begintime &&
	  $row->{timestamp} < $epochs->{$idx}->{timestamp}) {
	# Replace an epoch in the month with one earlier in the month
	$epochs->{$idx} = $row;
      }
    } elsif ($row->{timestamp} >= $begintime) {
      # Replace an epoch not in the month with one in the month
      $epochs->{$idx} = $row;
    } elsif ($row->{timestamp} > $epochs->{$idx}->{timestamp}) {
      # Replace an epoch before the month with a later one before the month.
      $epochs->{$idx} = $row;
    }

  } # while

  return $epochs;
}

###############################################################################
# List the atom archive for passed atom

sub listArchive {

  my $self = shift;
  my $args = shift;
  my $opts = shift;

  my $atoms = Sushi::GenDB::display (
				     $args,
				     $opts,
				     $::table_defs->{view_results_offline}
				    );
}

###############################################################################
# Create a restore stream to STDOUT

sub restore {
  my $self = shift;
  my $args = shift;
  my $opts = shift;

  # Find the atom to restore
  my $atom = (@{Sushi::Atom->readAtoms (%$args)})[0];
  if (!defined ($atom)) {
    $::log->error ("Could not find atom!\n");
    return 0;
  }
  my $commonName = $atom->commonName;

  # Find an offline record
  my $offline = (@{Sushi::GenDB::fetch (
				      {
				       atom_id => $atom->{id},
				       disposition => 'Available',
				       %$opts
				      },
				      {},
				      $::table_defs->{offline}
				     )})[0];
  if (!defined $offline) {
    $atom->error ("Could not find offline record for $commonName!\n");
    return 0;
  }

  # Get the media type from offline_pool
  my $pool = (@{Sushi::GenDB::fetch (
				  { storage_id => $offline->{storage_id} },
				  {},
				  $::table_defs->{offline_pool}
				  )})[0];
  if (!defined $pool) {
    $atom->error ("Couldn't find media device $offline->{storage_id} in db!\n");
    return 0;
  }

  print STDERR "Using saveset $offline->{saveset_name} copy dated " . ctime ($offline->{write_time}) . "\n";
  print STDERR "Attempting to load media device $offline->{storage_id}\n";

  my $media = new Sushi::OffLine (
				  type => $pool->{media_type},
				  storage_id => $offline->{storage_id}
				 );
  if (!defined $media) {
    $atom->error ("Failed to load off-line volume $offline->{storage_id}.  Bummer\n");
    return 0;
  }

  my $fn = $media->doRestore ($offline);

  if (!$media->close) {
    $atom->error ("Closing media device $self->{storage_id} failed.\n");
  }

  if (!$fn) {
    $atom->error ("No data files found.  This probably isn't good(tm).\n");
    return 0;
  }
  return 1;
}

###############################################################################
# Delete the offline atoms specified by $args, with options in $opts.

sub del {
  my $class = shift;
  my $args = shift;
  my $opts = shift;

  my ($row, $handle);
  my $pool;
  foreach $row (@{Sushi::GenDB::fetch (
				       {},
				       {},
				       $::table_defs->{offline_pool}
				      )}) {
    $pool->{$row->{storage_id}} = $row;
  }
  my $rows = Sushi::GenDB::fetch (
				  $args,
				  $opts,
				  $::table_defs->{offline}
				 );

  my $numrows = scalar @$rows;

  Sushi::Misc::confirm ("Warning: Continuing this operation will delete $numrows rows\nof offline data.") or return 0;

  foreach $row (@$rows) {
    # Sanity check
    if (!defined $pool->{$row->{storage_id}}) {
      $::log->error ("Storage id $row->{storage_id} not found in offline pool!");
      next;
    }

    if (!defined $opts->{nomedia}) {
      # Delete the data first
      # Mount the media if not mounted
      if (!exists $pool->{$row->{storage_id}}->{handle}) {
	$pool->{$row->{storage_id}}->{handle} = 
	  new Sushi::OffLine (
			      storage_id  => $row->{storage_id},
			      type => $pool->{$row->{storage_id}}->{media_type}
			     );
	defined ($pool->{$row->{storage_id}}->{handle}) ||
	  $::log->error ("Failed to mount media for storage device $row->{storage_id}");
      }
      defined ($pool->{$row->{storage_id}}->{handle}) or next;

      $pool->{$row->{storage_id}}->{handle}->clearmedia ($row);
    }

    my $nrows;
    if ($opts->{delete_reason} eq "Delete") {
      # Delete the DB record
      $nrows = Sushi::GenDB::del
	(
	 {
	  atom_id => $row->{atom_id},
	  result_timestamp => $row->{result_timestamp},
	  saveset_name => $row->{saveset_name}
	 },
	 {},
	 $::table_defs->{offline}
	);
    } else {
      # Update the DB record
      $nrows = Sushi::GenDB::update
	(
	 {
	  atom_id => $row->{atom_id},
	  result_timestamp => $row->{result_timestamp},
	  saveset_name => $row->{saveset_name}
	 },
	 {},
	 {
	  disposition => $opts->{delete_reason}
	 },
	 $::table_defs->{offline}
	);
    }
    if ($nrows != 1) {
      $::log->error ("Update/Delete failed for atom $row->{atom_id}, time " .
		     ctime ($row->{result_timestamp}) .
		     ",\nsave set $row->{saveset_name}: $nrows rows modified (should be 1)\n");
    }
  }

  # _Do_ clean up now, children!!
  my $mediakey;
  foreach $mediakey (keys %$pool) {
    defined ($pool->{$mediakey}->{handle}) and
      $pool->{$mediakey}->{handle}->close;
  }

  return $numrows;
}

###############################################################################

1;

