######################################################################
# Pool class
######################################################################

package Perlbal::Pool;
use strict;
use warnings;

use Perlbal::BackendHTTP;

# how often to reload the nodefile
use constant NODEFILE_RELOAD_FREQ => 3;

# balance methods we support
use constant BM_SENDSTATS => 1;
use constant BM_ROUNDROBIN => 2;
use constant BM_RANDOM => 3;

use fields (
            'name',            # string; name of this pool
            'use_count',       # int; number of services using us
            'nodes',           # arrayref; [ip, port] values (port defaults to 80)
            'node_count',      # int; number of nodes
            'node_used',       # hashref; { ip:port => use count }
            'balance_method',  # int; BM_ constant from above

            # used in sendstats mode
            'sendstats.listen',        # what IP/port the stats listener runs on
            'sendstats.listen.socket', # Perlbal::StatsListener object

            # used in nodefile mode
            'nodefile',           # string; filename to read nodes from
            'nodefile.lastmod',   # unix time nodefile was last modified
            'nodefile.lastcheck', # unix time nodefile was last stated
            'nodefile.checking',  # boolean; if true AIO is stating the file for us
            );

sub new {
    my Perlbal::Pool $self = shift;
    $self = fields::new($self) unless ref $self;

    my ($name) = @_;

    $self->{name} = $name;
    $self->{use_count} = 0;

    $self->{nodes} = [];
    $self->{node_count} = 0;
    $self->{node_used} = {};

    $self->{nodefile} = undef;
    $self->{balance_method} = BM_RANDOM;

    return $self;
}

sub set {
    my Perlbal::Pool $self = shift;
    
    my ($key, $val, $out, $verbose) = @_;
    my $err = sub { $out->("ERROR: $_[0]");   return 0;       };
    my $ok  = sub { $out->("OK") if $verbose; return 1;       };
    my $set = sub { $self->{$key} = $val;     return $ok->(); };

    if ($key eq 'nodefile') {
        # allow to unset it, which stops us from checking it further,
        # but doesn't clear our current list of nodes
        if ($val =~ /^(?:none|undef|null|""|'')$/) {
            $self->{'nodefile'} = undef;
            $self->{'nodefile.lastmod'} = 0;
            $self->{'nodefile.checking'} = 0;
            $self->{'nodefile.lastcheck'} = 0;
            return $ok->();
        }

        # enforce that it exists from here on out
        return $err->("File not found")
            unless -e $val;

        # force a reload
        $self->{'nodefile'} = $val;
        $self->{'nodefile.lastmod'} = 0;
        $self->{'nodefile.checking'} = 0;
        $self->load_nodefile;
        $self->{'nodefile.lastcheck'} = time;
        return $ok->();
    }

    if ($key eq "balance_method") {
        $val = {
            'sendstats' => BM_SENDSTATS,
            'random' => BM_RANDOM,
        }->{$val};
        return $err->("Unknown balance method")
            unless $val;
        return $set->();
    }
    
    if ($key =~ /^sendstats\./) {
        return $err->("Can only set sendstats listening address on service with balancing method 'sendstats'")
            unless $self->{balance_method} == BM_SENDSTATS;
        if ($key eq "sendstats.listen") {
            return $err->("Invalid host:port")
                unless $val =~ m!^\d+\.\d+\.\d+\.\d+:\d+$!;
            
            if (my $pbs = $self->{"sendstats.listen.socket"}) {
                $pbs->close;
            }
            
            unless ($self->{"sendstats.listen.socket"} =
                    Perlbal::StatsListener->new($val, $self)) {
                return $err->("Error creating stats listener: $Perlbal::last_error");
            }
            
            $self->populate_sendstats_hosts;
        }
        return $set->();
    }

}

sub populate_sendstats_hosts {
    my Perlbal::Pool $self = shift;

    # tell the sendstats listener about the new list of valid
    # IPs to listen from
    if ($self->{balance_method} == BM_SENDSTATS) {
        my $ss = $self->{'sendstats.listen.socket'};
        $ss->set_hosts(map { $_->[0] } @{$self->{nodes}}) if $ss;
    }
}

# returns string of balance method
sub balance_method {
    my Perlbal::Pool $self = $_[0];
    my $methods = {
        &BM_SENDSTATS => "sendstats",
        &BM_ROUNDROBIN => "round_robin",
        &BM_RANDOM => "random",
    };
    return $methods->{$self->{balance_method}} || $self->{balance_method};
}

sub load_nodefile {
    my Perlbal::Pool $self = shift;
    return 0 unless $self->{'nodefile'};

    if ($Perlbal::OPTMOD_LINUX_AIO) {
        return $self->_load_nodefile_async;
    } else {
        return $self->_load_nodefile_sync;
    }
}

sub _parse_nodefile {
    my Perlbal::Pool $self = shift;
    my $dataref = shift;

    my @nodes = split(/\r?\n/, $$dataref);

    # prepare for adding nodes
    $self->{nodes} = [];
    $self->{node_used} = {};

    foreach (@nodes) {
        s/\#.*//;
        if (/(\d+\.\d+\.\d+\.\d+)(?::(\d+))?/) {
            my ($ip, $port) = ($1, $2);
            $self->{node_used}->{"$ip:$port"} ||= 0; # set to 0 if not set
            push @{$self->{nodes}}, [ $ip, $port || 80 ];
        }
    }
    
    # setup things using new data
    $self->{node_count} = scalar @{$self->{nodes}};
    $self->populate_sendstats_hosts;
}

sub _load_nodefile_sync {
    my Perlbal::Pool $self = shift;

    my $mod = (stat($self->{nodefile}))[9];
    return if $mod == $self->{'nodefile.lastmod'};
    $self->{'nodefile.lastmod'} = $mod;

    open NODEFILE, $self->{nodefile} or return;
    my $nodes;
    { local $/ = undef; $nodes = <NODEFILE>; }
    close NODEFILE;
    $self->_parse_nodefile(\$nodes);
}

sub _load_nodefile_async {
    my Perlbal::Pool $self = shift;

    return if $self->{'nodefile.checking'};
    $self->{'nodefile.checking'} = 1;

    Perlbal::AIO::aio_stat($self->{nodefile}, sub {
        $self->{'nodefile.checking'} = 0;

        # this might have gotten unset while we were out statting the file, which
        # means that the user has instructed us not to use a node file, and may
        # have changed the nodes in the pool, so we should do nothing and return
        return unless $self->{'nodefile'};

        # ignore if the file doesn't exist
        return unless -e _;

        my $mod = (stat(_))[9];
        return if $mod == $self->{'nodefile.lastmod'};
        $self->{'nodefile.lastmod'} = $mod;

        # construct a filehandle (we only have a descriptor here)
        open NODEFILE, $self->{nodefile}
            or return;
        my $nodes;
        { local $/ = undef; $nodes = <NODEFILE>; }
        close NODEFILE;

        $self->_parse_nodefile(\$nodes);
        return;
    });
    
    return 1;
}

sub add {
    my Perlbal::Pool $self = shift;
    my ($ip, $port) = @_;

    $self->remove($ip, $port); # no dupes

    $self->{node_used}->{"$ip:$port"} = 0;
    push @{$self->{nodes}}, [ $ip, $port ];
    $self->{node_count} = scalar(@{$self->{nodes}});
}

sub remove {
    my Perlbal::Pool $self = shift;
    my ($ip, $port) = @_;

    delete $self->{node_used}->{"$ip:$port"};
    @{$self->{nodes}} = grep { "$_->[0]:$_->[1]" ne "$ip:$port" } @{$self->{nodes}};
    $self->{node_count} = scalar(@{$self->{nodes}});
}

sub get_backend_endpoint {
    my Perlbal::Pool $self = $_[0];

    my @endpoint;  # (IP,port)
    
    # re-load nodefile if necessary
    if ($self->{nodefile}) {
        my $now = time;
        if ($now > $self->{'nodefile.lastcheck'} + NODEFILE_RELOAD_FREQ) {
            $self->{'nodefile.lastcheck'} = $now;
            $self->load_nodefile;
        }
    }

    if ($self->{balance_method} == BM_SENDSTATS) {
        my $ss = $self->{'sendstats.listen.socket'};
        if ($ss && (@endpoint = $ss->get_endpoint)) {
            return @endpoint;
        }
    }

    # no nodes?
    return () unless $self->{node_count};

    # pick one randomly
    return @{$self->{nodes}[int(rand($self->{node_count}))]};
}

sub backend_should_live {
    my Perlbal::Pool $self = $_[0];
    my Perlbal::BackendHTTP $be = $_[1];

    # a backend stays alive if we still have users.  eventually this whole
    # function might do more and actually take into account the individual
    # backend, but for now, this suits us.
    return 1 if $self->{use_count};
    return 0;
}

sub node_count {
    my Perlbal::Pool $self = $_[0];
    return $self->{node_count};
}

sub nodes {
    my Perlbal::Pool $self = $_[0];
    return $self->{nodes};
}

sub node_used {
    my Perlbal::Pool $self = $_[0];
    return $self->{node_used}->{$_[1]};
}

sub mark_node_used {
    my Perlbal::Pool $self = $_[0];
    $self->{node_used}->{$_[1]}++;
}

sub increment_use_count {
    my Perlbal::Pool $self = $_[0];
    $self->{use_count}++;
}

sub decrement_use_count {
    my Perlbal::Pool $self = $_[0];
    $self->{use_count}--;
}

sub name {
    my Perlbal::Pool $self = $_[0];
    return $self->{name};
}

1;

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