#!/usr/bin/perl

=head1 NAME

pfqueue - pf queue handler service

=head1 SYNOPSIS

pfqueue [options]

 Options:
   -d      Daemonize
   -h      Help
   -v      Verbose

=cut

use warnings;
use strict;
use Getopt::Std;
use File::Basename qw(basename);
use POSIX qw(:signal_h pause :sys_wait_h);
use Pod::Usage;
use Fcntl qw(:flock);
use Scalar::Util qw(blessed);

#pf::log must always be initilized first
BEGIN {
    # log4perl init
    use constant INSTALL_DIR => '/usr/local/pf';
    use lib INSTALL_DIR . "/lib";
    use pf::log(service => 'pfqueue');
}

use pf::file_paths;
use pf::util;
use pf::services::util;
use pf::db;
use Config::IniFiles;
use pf::SwitchFactory;
use pf::pfqueue::consumer::redis;
use pf::client;
pf::client::setClient("pf::api::can_fork");
use threads;
use pf::factory::task;

tie our %CONFIG ,'Config::IniFiles' => (-file => "$install_dir/conf/pfqueue.conf");

our $config = tied %CONFIG;

# initialization
# --------------
# assign process name (see #1464)
our $PROGRAM_NAME = $0 = basename($0);
our @REGISTERED_TASKS;
our $IS_CHILD = 0;
our %CHILDREN;
our @TASKS_RUN;
our $ALARM_RECV = 0;


my $logger = Log::Log4perl->get_logger($PROGRAM_NAME);

$SIG{INT}  = \&normal_sighandler;
$SIG{HUP}  = \&normal_sighandler;
$SIG{TERM} = \&normal_sighandler;
$SIG{CHLD} = \&child_sighandler;

$SIG{PIPE} = 'IGNORE';

my %args;
getopts('dhvr', \%args);

pod2usage(-verbose => 1) if ($args{h});

my $daemonize = $args{d};
my $verbose   = $args{v};
my $restart   = $args{r};

my $pidfile = "${var_dir}/run/pfqueue.pid";

our $HAS_LOCK = 0;
open(my $fh, ">>$pidfile");
flock($fh, LOCK_EX | LOCK_NB) or die "cannot lock $pidfile another pfqueue is running\n";
$HAS_LOCK = 1;

our $running = 1;

# standard signals and daemonize
daemonize($PROGRAM_NAME) if ($daemonize);
our $PARENT_PID = $$;
Log::Log4perl::MDC->put( 'tid', $$ );

pf::SwitchFactory::preLoadModules();

register_tasks();
wait_for_it();
cleanup();
$config = undef;

END {
    if (!$args{h} && $HAS_LOCK && !$IS_CHILD) {
        deletepid();
        $logger->info("stopping pfqueue");
    }
}

exit(0);

=head1 SUBROUTINES

=head2 register_tasks

Register all tasks

=cut

sub register_tasks {
    foreach my $section ($config->GroupMembers('queue')) {
        my $name = $section;
        next unless $name =~ s/^queue +//;
        my $worker = $config->val($section, 'workers', 10);
        foreach (1 .. $worker) {
            register_task("Queue:$name",\&process_queue, {  queue_name => "Queue:$name"});
        }
        my $has_delayed_queue = isenabled($config->val($section, 'has_delayed_queue', 0));
        if ($has_delayed_queue) {
            my $delayed_queue = "Delayed:$name";
            my $submit_queue = "Queue:$name";
            my $parameters = {
                'delay_queue' => $delayed_queue,
                'submit_queue' => $submit_queue,
                'batch' => $config->val($section, 'delayed_queue_batch', 100),
                'batch_sleep' => $config->val($section, 'delayed_queue_sleep', 100) * 1000,
            };
            my $delayed_queue_workers = $config->val($section, 'delayed_queue_workers', 1);
            for(1 .. $delayed_queue_workers) {
                register_task($delayed_queue, \&process_delayed_jobs, $parameters );
            }
        }
    }
}

=head2 cleanup

cleans up after children

=cut

sub cleanup {
    kill_and_wait_for_children('INT',  30);
    kill_and_wait_for_children('USR1', 10);
    signal_children('KILL');
}

=head2 kill_and_wait_for_children

signal children and waits for them to exit process

=cut

sub kill_and_wait_for_children {
    my ($signal, $waittime) = @_;
    return unless keys %CHILDREN;
    my $start = time();
    signal_children($signal);
    while (((keys %CHILDREN) != 0) ) {
        my $slept = sleep $waittime;
        $waittime -= $slept;
        $logger->trace("($signal) left to sleep : $waittime " . join(" ",keys %CHILDREN));
        last if $waittime <= 0;
    }
    my $diff = time - $start;
    $logger->trace("Time waiting for $diff $waittime");
}

=head2 signal_children

sends a signal to all active children

=cut

sub signal_children {
    my ($signal) = @_;
    kill($signal, keys %CHILDREN);
}

=head2 normal_sighandler

the signal handler to shutdown the service

=cut

sub normal_sighandler {
    $running = 0;
}

=head2 run_tasks

run all run_tasks

=cut

sub run_tasks {
    db_disconnect();
    my $mask = POSIX::SigSet->new(POSIX::SIGCHLD());
    sigprocmask(SIG_BLOCK, $mask);
    while (@REGISTERED_TASKS) {
        my $task = shift @REGISTERED_TASKS;
        run_task($task);
    }
    sigprocmask(SIG_UNBLOCK, $mask);
}

=head2 run_task

creates a new child to run a task

=cut

sub run_task {
    my ($task) = @_;
    my $pid = fork();
    if ($pid) {
        $CHILDREN{$pid} = $task;
    } elsif ($pid == 0) {
        eval {
            srand();
            my $mask = POSIX::SigSet->new(POSIX::SIGCHLD());
            Log::Log4perl::MDC->put('tid', $$);
            $SIG{CHLD} = \&child_sighandler2;
            $IS_CHILD = 1;
            sigprocmask(SIG_UNBLOCK, $mask);
            _run_task(@$task);
        };
        $logger->error("Error running the task $@") if $@;
        exit 0;
    } else {
        $logger->error("Fork error $!");
    }
}

=head2 _run_task

the task to is run in a loop until it is finished

=cut

sub _run_task {
    my ($id, $task, $parameters) = @_;
    $0 = "pfqueue - $id";
    my $consumer;
    while ($running) {
        $consumer = get_consumer($id, $parameters) unless $consumer;
        pf::config::cached::ReloadConfigs();
        eval {
            #reload all cached configs before running the task
            $task->($consumer);
        };
        if ($@) {
            $logger->error("Error running task '$id': $@");
            $consumer = undef;
        }

        #Stop running if parent is no longer alive
        unless (is_parent_alive()) {
            $logger->error("Parent is no longer running shutting down");
            $running = 0;
        }
        unless ($consumer) {
            sleep (int(rand(5)) + 1);
        }
    }
    $logger->trace("$$ shutting down");
}

=head2 is_parent_alive

Checks to see if parent is alive

=cut

sub is_parent_alive {
    kill(0, $PARENT_PID);
}

=head2 register_task

registers the task to run

=cut

sub register_task {
    my ($taskId, $function, $parameters) = @_;
    push @REGISTERED_TASKS, [$taskId, $function, $parameters];

}

=head2 process_queue

Process queue

=cut

sub process_queue {
    my ($consumer, @args) = @_;
    $consumer->process_next_job(@args);
}

=head2 process_delayed_jobs

Move the delayed jobs to the work queue

=cut

sub process_delayed_jobs {
    my ($consumer, @args) = @_;
    $consumer->process_delayed_jobs(@args);
}

=head2 wait_for_it

waits for signals

=cut

sub wait_for_it {
    while ($running) {
        $logger->trace("Starting tasks");
        run_tasks();
        $logger->trace("Finish tasks");
        pause;
        $logger->trace("Woke up from a signal");
    }
    $logger->trace("Signaled to stop running");
}

=head2 alarm_sighandler

the alarm signal handler

=cut

sub alarm_sighandler {
    $ALARM_RECV = 1;
}

=head2 child_sighandler

reaps the children

=cut

sub child_sighandler {
    local ($!, $?);
    while (1) {
        my $child = waitpid(-1, WNOHANG);
        last unless $child > 0;
        my $task = delete $CHILDREN{$child};
        register_task(@$task);
    }
}

=head2 child_sighandler2

reaps the children

=cut

sub child_sighandler2 {
    local ($!, $?);
    while (1) {
        my $child = waitpid(-1, WNOHANG);
        last unless $child > 0;
    }
}

=head2 get_consumer($id, $param_hash)

get consumer

=cut

sub get_consumer {
    my ($id, $parameters) = @_;
    my $consumer;
    eval {
        $consumer = pf::pfqueue::consumer::redis->new({ %{$CONFIG{"consumer"}}, %$parameters, redis_name => $id });
    };
    if ($@) {
        $logger->error($@);
    }
    return $consumer;
}

=head1 AUTHOR

Inverse inc. <info@inverse.ca>

Minor parts of this file may have been contributed. See CREDITS.

=head1 COPYRIGHT

Copyright (C) 2005-2015 Inverse inc.

=head1 LICENSE

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
USA.

=cut

