package MogileFS::Worker::Fsck;

use strict;
use base 'MogileFS::Worker';
use fields (
            );
use MogileFS::Util qw(every error);
use List::Util ();
use Time::HiRes ();

use constant SUCCESS => 0;
use constant TEMPORARY => 1;
use constant PERMANENT => 2;
use constant REPLICATE => 3;

sub watchdog_timeout { 30 }

sub work {
    my $self = shift;

    my %retvals;
    my ($run_count, $total_time, $total_done) = (0, 0, 0);

    my $my_host = MogileFS::Config->hostname;

    every(5.0, sub {
        my $sleep_set = shift;
        $self->parent_ping;

        # see if we're even enabled for this host.
        my $fhost = MogileFS::Config->server_setting('fsck_host') || "";
        return unless $fhost eq $my_host;

        warn "[fsck] Running...\n";

        # checking doesn't go well if the monitor job hasn't actively started
        # marking things as being available
        unless ($self->monitor_has_run) {
            # only warn on runs after the first.  gives the monitor job some time to work
            # before we throw a message.
            error("waiting for monitor job to complete a cycle before beginning checking")
                if $run_count++ > 0;
            return;
        }

        my $sto = Mgd::get_store();

        my $highest_fid = MogileFS::Config->server_setting('fsck_highest_fid_checked') || 0;
        my @fids_to_check = $sto->get_fidids_above($highest_fid, 1000);
        unless (@fids_to_check) {
            warn "[fsck] no fids to check...\n";
            return;
        }

        warn "[fsck] fids=$fids_to_check[0] ~ $fids_to_check[-1]\n";
        my $new_max;
        foreach my $fidid (@fids_to_check) {
            $self->still_alive;
            $new_max = $fidid;
        }

        warn "[fsck] new_max = $new_max\n";
        MogileFS::Config->set_server_setting('fsck_highest_fid_checked', $new_max) if $new_max;

        $sleep_set->(0); # don't sleep in next round.
    });
}

# this sub actually does the checking of a fid.  we put it in its own sub so we can
# return from it using the unlock coderef.  always returns a number:
#
#   0 - file is just fine, drop it from the list
#   1 - temporary failure, check again later
#   2 - permanent failure, this file shouldn't get tried again
#   3 - needs replication, we found something not quite right
#
sub check_fid {
    my ($dbh, $fid, $level) = @_;

    # unlocker sub to be used
    my $lockname = "mgfs:fid:$fid:check";
    my $retunlock = sub {
        my $rv = shift()+0;

        # 0 means success, else some sort of failure
        if ($rv) {
            my $msg = shift() || "no error text";
            my $rvtype = {
                1 => 'temporary failure',
                2 => 'permanent failure',
                3 => 'needs replication',
            }->{$rv} || 'unknown error';
            error("check_fid($fid, $level) = $rvtype: $msg");
        }

        $dbh->do("SELECT RELEASE_LOCK(?)", undef, $lockname);
        return $rv;
    };

    # try to get the lock
    my $lock = $dbh->selectrow_array("SELECT GET_LOCK(?, 1)", undef, $lockname);
    return $retunlock->(TEMPORARY, "failed getting lock $lockname") unless $lock;

    # all checks require us to get the file paths
    my $devids = $dbh->selectcol_arrayref('SELECT devid FROM file_on WHERE fid = ?', undef, $fid);
    return $retunlock->(PERMANENT, 'no sources found') unless $devids && @$devids;

    # if it's a simple location check, we're done
    return $retunlock->(SUCCESS) if $level eq 'locations';

    # get the file size from the database, we're going to need it.  note that this could be
    # a 0 size, so we have to watch for defined.
    my $db_size = $dbh->selectrow_array('SELECT length FROM file WHERE fid = ?', undef, $fid);
    return $retunlock->(TEMPORARY, "database does not contain file size") unless defined $db_size;

    # iterate and do HEAD requests to determine some basic information about the file
    my %devs;
    foreach my $devid (@$devids) {
        # setup and do the request.  these failures are total failures in that we expect
        # them to work again later, as it's probably transient and will persist no matter
        # how many paths we try.
        my $dfid = MogileFS::DevFID->new($devid, $fid);
        my $path = $dfid->get_url
            or return $retunlock->(TEMPORARY, 'failure to create HTTP path to file');
        my $ua = LWP::UserAgent->new(timeout => 3)
            or return $retunlock->(TEMPORARY, 'failed to create LWP::UserAgent object');
        my $resp = $ua->head($path);

        # at this point we're going to assume that any error is based on the device alone
        # so we want to store the status and not return
        if ($resp->is_success) {
            # great, check the size against what's in the database
            if ($resp->header('Content-Length') == $db_size) {
                $devs{$devid} = SUCCESS;
            } else {
                $devs{$devid} = PERMANENT;
            }

        } else {
            # easy one, the request failed for some reason, 500 would tend to imply that the
            # mogstored is having issues so we should try again later, whereas a 404 is a
            # total and permanent failure
            error("check_fid($fid, $level): " . $resp->code . " on device $devid");
            if ($resp->code == 404) {
                $devs{$devid} = PERMANENT;
            } else {
                $devs{$devid} = TEMPORARY;
            }
        }
    }

    # at this point, we need to take actions.  if we discovered some PERMANENT failures in
    # a device scan, then we need to take care of those now by removing them.  but DO NOT
    # remove them if that would leave us with no mappings!  ONLY if there is at least one
    # SUCCESS mapping.
    # FIXME: implement

    # if they wanted a quick scan, let's stop here and throw a result based on the contents
    # of the %devs hash.  basically, if any of the devices had issues, then at this point we
    # want to throw a flag saying "please replicate this".  if not, then we tell them that
    # we're successful on this fid.
    if ($level eq 'quick') {
        foreach my $code (values %devs) {
            return $retunlock->(REPLICATE, "permanent failure on one or more devices")
                if $code != SUCCESS;
        }
        return $retunlock->(SUCCESS);
    }

    # full mode not here yet
    return $retunlock->(TEMPORARY, "sorry, $level mode is not implemented yet");
}

1;

# Local Variables:
# mode: perl
# c-basic-indent: 4
# indent-tabs-mode: nil
# End:
