ljr/wcmtools/perlbal/lib/Perlbal/StatsListener.pm

220 lines
6.6 KiB
Perl
Executable File

######################################################################
# UDP listener for Apache free/busy stats
######################################################################
package Perlbal::StatsListener;
use strict;
use warnings;
use base "Perlbal::Socket";
use fields ('service', # Perlbal::Service,
'pos', # index in ring. this index has an empty value in it
# entries before it are good
'message_ring', # arrayref of UDP messages, unparsed
'from_ring', # arrayref of from addresses
'hostinfo', # hashref of ip (4 bytes) -> [ $free, $active ] (or undef)
'total_free', # int scalar: free listeners
'need_parse', # hashref: ip -> pos
'use_count', # hashref: ip -> times_used (ip can also be '' for empty case)
'use_total', # int scalar: count of uses we've had total
'dead', # int; if 1 then we're dead (don't give out any more info)
);
use constant RING_SIZE => 30;
sub new {
my $class = shift;
my ($hostport, $service) = @_;
my $sock = IO::Socket::INET->new(
LocalAddr => $hostport,
Proto => 'udp',
ReuseAddr => 1,
Blocking => 0,
);
return Perlbal::error("Error creating listening socket: $!")
unless $sock;
$sock->sockopt(Socket::SO_BROADCAST, 1);
$sock->blocking(0);
my $self = fields::new($class);
$self->SUPER::new($sock); # init base fields
$self->{dead} = 0;
$self->{service} = $service;
$self->reset_state;
bless $self, ref $class || $class;
$self->watch_read(1);
return $self;
}
sub reset_state {
my Perlbal::StatsListener $self = shift;
$self->{pos} = 0;
$self->{message_ring} = [];
$self->{from_ring} = [];
$self->{total_free} = 0;
$self->{need_parse} = {};
$self->{hostinfo} = {};
$self->{use_count} = {};
}
sub event_read {
my Perlbal::StatsListener $self = shift;
my $sock = $self->{sock};
my ($port, $iaddr);
while (my $from = $sock->recv($self->{message_ring}[$self->{pos}], 1024)) {
# set the from just to the 4 byte IP address
($port, $from) = Socket::sockaddr_in($from);
$self->{from_ring}[$self->{pos}] = $from;
# new message from host $from, so clear its cached data
if (exists $self->{hostinfo}{$from}) {
if (my $hi = $self->{hostinfo}{$from}) {
$self->{total_free} -= $hi->[0];
}
$self->{hostinfo}{$from} = undef;
$self->{need_parse}{$from} = $self->{pos};
}
$self->{pos} = 0 if ++$self->{pos} == RING_SIZE;
}
}
sub get_endpoint {
my Perlbal::StatsListener $self = shift;
return () if $self->{dead};
# catch up on our parsing
while (my ($from, $pos) = each %{$self->{need_parse}}) {
# make sure this position still corresponds to that host
next unless $from eq $self->{from_ring}[$pos];
next unless $self->{message_ring}[$pos] =~
m!^bcast_ver=1\nfree=(\d+)\nactive=(\d+)\n$!;
$self->{hostinfo}{$from} = [ $1, $2 ];
$self->{total_free} += $1;
}
$self->{need_parse} = {};
# mode 1 (normal) is on advertised free, mode 2 is when nothing's
# free, so we make a weighted random guess on past performance
my $mode = 1;
my $upper_bound = $self->{total_free};
unless ($upper_bound) {
$mode = 2;
$upper_bound = $self->{use_total};
}
# pick what position we'll return
my $winner = rand($upper_bound);
# find the winner
my $count = 0;
# two passes, since the inner while is doing 'each'
# which we intrerupt when we find the winner. so later,
# coming back into this, the each doesn't necessarily
# start in the beginning so we have to let it loop around
foreach my $pass (1..2) {
while (my ($from, $hi) = each %{$self->{hostinfo}}) {
if ($mode == 1) {
# must have data
next unless $hi;
$count += $hi->[0];
} elsif ($mode == 2) {
# increment count by uses this one's received for weighting
$count += $self->{use_count}{$from};
}
if ($count >= $winner) {
my $ip = Socket::inet_ntoa($from);
if ($mode == 1) {
$hi->[0]--;
$self->{total_free}--;
$self->{use_total}++;
$self->{use_count}{$from}++;
}
return ($ip, 80);
}
}
}
# guess we couldn't find anything
$self->{use_count}{'winner_too_high'}++;
return ();
}
sub set_hosts {
my Perlbal::StatsListener $self = shift;
my @hosts = @_;
# clear the known hosts
$self->reset_state;
# make each provided host known, but undef (meaning
# its ring data hasn't been parsed)
foreach my $dq (@hosts) {
# converted dotted quad to packed format
my $pd = Socket::inet_aton($dq);
$self->{hostinfo}{$pd} = undef;
}
}
sub debug_dump {
my Perlbal::StatsListener $self = shift;
my $out = shift;
no warnings;
$out->("Stats listener dump:");
$out->(" pos = $self->{pos}");
$out->(" message_ring = ");
for (my $i=0; $i<RING_SIZE; $i++) {
my $ip = eval { Socket::inet_ntoa($self->{'from_ring'}[$i]); };
$out->(" \#$i: [$ip] " . $self->{'message_ring'}[$i]);
}
my $count_free = 0;
foreach my $host (sort keys %{$self->{hostinfo}}) {
my $ip = eval { Socket::inet_ntoa($host); };
my $hi = $self->{hostinfo}{$host};
my $need_parse = $self->{need_parse}{$host};
my $uses = $self->{use_count}{$host};
if ($hi) {
$count_free += $hi->[0];
$out->(" host $ip = $uses: [ $hi->[0] free, $hi->[1] act ] needparse=$need_parse");
} else {
$out->(" host $ip = $uses: needparse=$need_parse");
}
}
$out->(" total free: $self->{total_free} (calculated: $count_free)");
$out->("Uses with no total: $self->{use_count}{'no_free'}, winner too high: $self->{use_count}{'winner_too_high'}");
}
sub event_err { }
sub event_hup { }
sub die_gracefully {
# okay, let's actually die now
my $self = shift;
$self->{dead} = 1;
$self->close;
}
1;
# Local Variables:
# mode: perl
# c-basic-indent: 4
# indent-tabs-mode: nil
# End: