329 lines
8.9 KiB
Perl
329 lines
8.9 KiB
Perl
|
######################################################################
|
||
|
# 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:
|