Perlbal-1.80/ 0000755 0001750 0001750 00000000000 11722625030 013211 5 ustar dormando dormando Perlbal-1.80/conf/ 0000755 0001750 0001750 00000000000 11722625030 014136 5 ustar dormando dormando Perlbal-1.80/conf/ssl.conf 0000644 0001750 0001750 00000002731 11503530123 015604 0 ustar dormando dormando #
# This is an example webserver configuration using virtual hosts.
#
# See doc/config-guide.txt for descriptions of each command (line)
# and configuration syntax.
#
################################3
#
# to use SSL mode, you'll need IO::Socket::SSL 0.97+
#
# You can do SSL either on webserver mode, a reverse_proxy, or a service selector,
# but not if the service selector is vhost-based, because SSL and vhosts aren't
# compatible.
#
# the pound docs recommend this cipher list for a known bug in older
# versions of IE:
#
# ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP:+eNULL
#
# You can make a self-signed key and cert with;
#
# openssl req -x509 -newkey rsa:1024 -keyout server-key.pem -out server-cert.pem -days 365 -nodes
#
CREATE POOL my_apaches
POOL my_apaches ADD 10.0.0.10:8080
POOL my_apaches ADD 10.0.0.11:8080
CREATE SERVICE site
SET listen = 0.0.0.0:443
SET role = reverse_proxy
SET pool = my_apaches
SET persist_client = on
SET persist_backend = on
SET verify_backend = on
SET enable_ssl = on
SET ssl_key_file = certs/server-key.pem
SET ssl_cert_file = certs/server-cert.pem
# optionally set the cipher list. the default is "ALL:!LOW:!EXP"
SET ssl_cipher_list = ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP:+eNULL
ENABLE site
# always good to keep an internal management port open:
CREATE SERVICE mgmt
SET role = management
SET listen = 127.0.0.1:16000
ENABLE mgmt
Perlbal-1.80/conf/virtual-hosts.conf 0000644 0001750 0001750 00000001471 11503530123 017627 0 ustar dormando dormando #
# This is an example webserver configuration using virtual hosts.
#
# See doc/config-guide.txt for descriptions of each command (line)
# and configuration syntax.
#
LOAD vhosts
CREATE SERVICE site
SET role = web_server
SET docroot = /var/www/site.com/
SET dirindexing = 1
ENABLE site
CREATE SERVICE example
SET role = web_server
SET docroot = /var/www/example.com/
SET dirindexing = 0
ENABLE example
CREATE SERVICE vdemo
SET listen = 0.0.0.0:80
SET role = selector
SET plugins = vhosts
SET persist_client = on
VHOST *.site.com = site
VHOST *.example.com = example
ENABLE vdemo
# always good to keep an internal management port open:
CREATE SERVICE mgmt
SET role = management
SET listen = 127.0.0.1:16000
ENABLE mgmt
Perlbal-1.80/conf/echoservice.conf 0000644 0001750 0001750 00000001060 11503530123 017274 0 ustar dormando dormando #
# This is an example configuration using the EchoService plugin.
#
# See doc/config-guide.txt for descriptions of each command (line)
# and configuration syntax.
#
LOAD EchoService
CREATE SERVICE echo
SET listen = 0.0.0.0:7123
SET role = echo
ENABLE echo
CREATE SERVICE echo_delayed
SET listen = 0.0.0.0:7124
SET role = echo
SET echo_delay = 3
ENABLE echo_delayed
# always good to keep an internal management port open:
CREATE SERVICE mgmt
SET role = management
SET listen = 127.0.0.1:16000
ENABLE mgmt
Perlbal-1.80/conf/webserver.conf 0000644 0001750 0001750 00000000745 11503530123 017012 0 ustar dormando dormando #
# This is an example webserver configuration.
#
# See doc/config-guide.txt for descriptions of each command (line)
# and configuration syntax.
#
CREATE SERVICE docs
SET listen = 0.0.0.0:80
SET role = web_server
SET docroot = /usr/share/doc/
SET dirindexing = 1
SET persist_client = on
ENABLE docs
# always good to keep an internal management port open:
CREATE SERVICE mgmt
SET role = management
SET listen = 127.0.0.1:16000
ENABLE mgmt
Perlbal-1.80/conf/not-modified-plugin.conf 0000644 0001750 0001750 00000000673 11503530123 020660 0 ustar dormando dormando LOAD vhosts
LOAD NotModified
# if you have a host that always serves immutable content, this plugin
# will reply 304 if client sent any "If-Modified-Since" header. since
# if they have SOME copy, it's the correct one.
CREATE SERVICE demo
SET listen = 0.0.0.0:8001
SET role = selector
SET plugins = vhosts, notmodified
SET persist_client = true
SET demo.notmodified.host_pattern = ^immutable\.
ENABLE demo
Perlbal-1.80/conf/nodelist.dat 0000644 0001750 0001750 00000000264 11503530123 016446 0 ustar dormando dormando # whitespace and comments allowed
10.1.0.10 # test machine
#10.1.0.10:8083 # test machine
# can include port numbers, but defaults to 80 if not provided:
# 10.1.0.4:80
Perlbal-1.80/conf/load-balancer.conf 0000644 0001750 0001750 00000002354 11503530123 017470 0 ustar dormando dormando #
# This is an example reverse configuration doing load balancing.
#
# See doc/config-guide.txt for descriptions of each command (line)
# and configuration syntax.
#
# this service's nodes are configured via a pool object. if you need
# to change them, telnet on in to the management port and you and/or your
# scripts can change them on the fly
CREATE POOL my_apaches
POOL my_apaches ADD 10.0.0.10:8080
POOL my_apaches ADD 10.0.0.11:8080
POOL my_apaches ADD 10.0.0.12
POOL my_apaches ADD 10.0.0.13:8081
CREATE SERVICE balancer
SET listen = 0.0.0.0:80
SET role = reverse_proxy
SET pool = my_apaches
SET persist_client = on
SET persist_backend = on
SET verify_backend = on
ENABLE balancer
# this service's nodes are read via a file. the file is checked every
# few seconds to see if it changed and automatically picked up. use what
# works for you.
CREATE POOL dynamic
SET nodefile = conf/nodelist.dat
CREATE SERVICE balancer2
SET listen = 0.0.0.0:81
SET role = reverse_proxy
SET pool = dynamic
ENABLE balancer2
# always good to keep an internal management port open:
CREATE SERVICE mgmt
SET role = management
SET listen = 127.0.0.1:16000
ENABLE mgmt
Perlbal-1.80/MANIFEST 0000644 0001750 0001750 00000005377 11722624722 014365 0 ustar dormando dormando CHANGES
conf/echoservice.conf
conf/load-balancer.conf
conf/nodelist.dat
conf/not-modified-plugin.conf
conf/ssl.conf
conf/virtual-hosts.conf
conf/webserver.conf
contrib/perlbal-check
contrib/perlbal-check.yaml
CONTRIBUTING
devtools/gendocs.pl
doc/config-guide.txt
doc/hacking/classes.txt
doc/hacking/hooks.txt
doc/hacking/todo.txt
doc/http-versions.txt
doc/pool-parameters.txt
doc/README
doc/reproxying.txt
doc/service-parameters.txt
lib/Perlbal.pm
lib/Perlbal/AIO.pm
lib/Perlbal/BackendHTTP.pm
lib/Perlbal/Cache.pm
lib/Perlbal/ChunkedUploadState.pm
lib/Perlbal/ClientHTTP.pm
lib/Perlbal/ClientHTTPBase.pm
lib/Perlbal/ClientManage.pm
lib/Perlbal/ClientProxy.pm
lib/Perlbal/CommandContext.pm
lib/Perlbal/FAQ.pod
lib/Perlbal/Fields.pm
lib/Perlbal/HTTPHeaders.pm
lib/Perlbal/ManageCommand.pm
lib/Perlbal/Manual.pod
lib/Perlbal/Manual/Configuration.pod
lib/Perlbal/Manual/Contributing.pod
lib/Perlbal/Manual/Credits.pod
lib/Perlbal/Manual/Debugging.pod
lib/Perlbal/Manual/FailOver.pod
lib/Perlbal/Manual/HighPriority.pod
lib/Perlbal/Manual/Hooks.pod
lib/Perlbal/Manual/Install.pod
lib/Perlbal/Manual/Internals.pod
lib/Perlbal/Manual/LoadBalancer.pod
lib/Perlbal/Manual/Logging.pod
lib/Perlbal/Manual/Management.pod
lib/Perlbal/Manual/Plugins.pod
lib/Perlbal/Manual/ReverseProxy.pod
lib/Perlbal/Manual/Roles.pod
lib/Perlbal/Manual/Selector.pod
lib/Perlbal/Manual/WebServer.pod
lib/Perlbal/Plugin/AccessControl.pm
lib/Perlbal/Plugin/AutoRemoveLeadingDir.pm
lib/Perlbal/Plugin/Cgilike.pm
lib/Perlbal/Plugin/EchoService.pm
lib/Perlbal/Plugin/FlvStreaming.pm
lib/Perlbal/Plugin/Highpri.pm
lib/Perlbal/Plugin/Include.pm
lib/Perlbal/Plugin/LazyCDN.pm
lib/Perlbal/Plugin/MaxContentLength.pm
lib/Perlbal/Plugin/NotModified.pm
lib/Perlbal/Plugin/Palimg.pm
lib/Perlbal/Plugin/Queues.pm
lib/Perlbal/Plugin/Redirect.pm
lib/Perlbal/Plugin/Stats.pm
lib/Perlbal/Plugin/Throttle.pm
lib/Perlbal/Plugin/Vhosts.pm
lib/Perlbal/Plugin/Vpaths.pm
lib/Perlbal/Plugin/XFFExtras.pm
lib/Perlbal/Pool.pm
lib/Perlbal/ReproxyManager.pm
lib/Perlbal/Service.pm
lib/Perlbal/Socket.pm
lib/Perlbal/SocketSSL.pm
lib/Perlbal/TCPListener.pm
lib/Perlbal/Test.pm
lib/Perlbal/Test/WebClient.pm
lib/Perlbal/Test/WebServer.pm
lib/Perlbal/UploadListener.pm
lib/Perlbal/Util.pm
Makefile.PL
MANIFEST
MANIFEST.SKIP
META.yml Module meta-data (added by MakeMaker)
perlbal
README
t/00-use.t
t/10-testharness.t
t/12-headers.t
t/13-server-tokens.t
t/15-webserver.t
t/17-webserver-concat.t
t/20-put.t
t/22-chunked-put.t
t/30-reverseproxy.t
t/31-realworld.t
t/32-pipelining.t
t/32-selector.t
t/35-reproxy.t
t/40-ranges.t
t/45-buffereduploads.t
t/50-plugins.t
t/52-chunked-upload.t
t/60-child-httpd.t
t/75-plugin-include.t
t/76-plugin-redirect.t
t/77-plugin-throttle.t
t/78-plugin-xffextras.t
t/90-accesscontrol.t
t/91-fields.t
t/99-benchmark-bool.t
t/helper/child-httpd.pl
Perlbal-1.80/devtools/ 0000755 0001750 0001750 00000000000 11722625030 015050 5 ustar dormando dormando Perlbal-1.80/devtools/gendocs.pl 0000755 0001750 0001750 00000003660 11713660634 017050 0 ustar dormando dormando #!/usr/bin/env perl
#
use strict;
use FindBin;
use lib "$FindBin::Bin/../lib";
use Perlbal;
my $tunables = Perlbal::Service::autodoc_get_tunables();
my %by_role;
while (my ($k, $tun) = each %$tunables) {
$by_role{$tun->{check_role}}{$k} = $tun;
}
my $docs = $FindBin::Bin . "/../doc";
open (H, ">$docs/service-parameters.html") or die "Can't open $docs/service-parameters.html for writing";
print H <Perlbal Service parametersSet via commands of either forms:
SET <service-name> <param> = <value>
SET <param> = <value>
Note on types: 'bool' values can be set using one of 1, true, yes, on, 0, false, off, or no.
'size' values are in integer bytes, or an integer followed by 'b', 'k', or 'm' (case-insensitive)
for bytes, KiB, or MiB.
Note that you can set defaults for all services you create by using the DEFAULT command:
DEFAULT <param> = <value>
HTML
foreach my $role ("*", "reverse_proxy", "web_server") {
if ($role eq "*") {
print H "For all services:
";
} else {
print H "Only for '$role' services:
";
}
print H "\n";
print H "Param | type | Default | Description |
\n";
foreach my $param (sort keys %{$by_role{$role}}) {
my $tun = $by_role{$role}{$param};
my $def = $tun->{default};
my $type = $tun->{check_type} || "";
undef $type unless $type && $type =~ /^bool|int|size$/;
if ($type eq "bool") {
$def = $def ? "true" : "false";
}
print H "$param | $type | $def | $tun->{des} |
\n";
}
print H "
\n";
}
system("links -dump $docs/service-parameters.html > $docs/service-parameters.txt")
and die "Error: links not installed";
unlink "$docs/service-parameters.html";
Perlbal-1.80/perlbal 0000755 0001750 0001750 00000004536 11503530123 014563 0 ustar dormando dormando #!/usr/bin/perl -w
#
=head1 NAME
Perlbal - Reverse-proxy load balancer and webserver
=head1 DESCRIPTION
For now, see example configuration files in conf/ from the CPAN tarball
http://search.cpan.org/dist/Perlbal/
=head1 AUTHORS
Brad Fitzpatrick,
Mark Smith,
=head1 SEE ALSO
http://www.danga.com/perlbal/
=head1 COPYRIGHT AND LICENSE
Copyright 2004, Danga Interactive, Inc.
Copyright 2005-2007, Six Apart, Ltd.
You can use and redistribute Perlbal under the same terms as Perl itself.
=cut
use strict;
use warnings;
use lib 'lib';
use Perlbal;
my $opt_daemonize;
my $opt_config;
my $opt_help;
my $opt_version;
usage(1) unless
Getopt::Long::GetOptions(
'daemon' => \$opt_daemonize,
'config=s' => \$opt_config,
'help' => \$opt_help,
'version' => \$opt_version,
);
my $default_config = "/etc/perlbal/perlbal.conf";
$opt_config = $default_config if ! $opt_config && -e $default_config;
usage(0) if $opt_help;
sub usage {
my $rv = shift;
print STDERR <WatchedSockets() > 0) {
die "No services or management port configured. Nothing to do. Stopping.\n";
}
if ($opt_daemonize) {
Perlbal::daemonize();
} else {
print "Running.\n";
}
exit 0 if Perlbal::run();
exit 1;
# Local Variables:
# mode: perl
# c-basic-indent: 4
# indent-tabs-mode: nil
# End:
Perlbal-1.80/t/ 0000755 0001750 0001750 00000000000 11722625030 013454 5 ustar dormando dormando Perlbal-1.80/t/91-fields.t 0000644 0001750 0001750 00000002163 11713660634 015351 0 ustar dormando dormando use strict;
use warnings;
use Carp;
use Test::More 0.94 tests => 6; # last test to print
use Hash::Util;
my $warn_mocked =
"mocked by perlbal, this error should not be raised using Perlbal::Fields";
{
no warnings 'redefine';
*Hash::Util::lock_ref_keys = sub { croak $warn_mocked; };
}
SKIP: {
skip "perl need to be greater than 5.009", 1 if ( $] < 5.009 );
subtest 'before using Perlbal::Fields' => sub {
use_ok('Perlbal::CommandContext');
eval { Perlbal::CommandContext->new(); };
like( $@, qr{$warn_mocked}, "use old library" );
};
}
my $class = 'Perlbal::Fields';
use_ok( $class, "can load module $class" );
ok( $class->run(), "run method" );
isa_ok( Perlbal::Test::Fields->new(),
'Perlbal::Test::Fields', "can create object" );
use_ok('Perlbal::CommandContext');
isa_ok( Perlbal::CommandContext->new(),
'Perlbal::CommandContext', "can create object" );
done_testing();
1;
package Perlbal::Test::Fields;
use fields ( 'headers', 'origcase' );
sub new {
my Perlbal::Test::Fields $self = shift;
$self = fields::new($self) unless ref $self;
return $self;
}
1;
Perlbal-1.80/t/40-ranges.t 0000644 0001750 0001750 00000007501 11503530123 015337 0 ustar dormando dormando #!/usr/bin/perl
use strict;
use Perlbal::Test;
use Perlbal::Test::WebServer;
use Perlbal::Test::WebClient;
use Test::More tests => 35;
my ($back_port) = start_webserver();
# setup a simple perlbal that uses the above server
my $webport = new_port();
my $dir = tempdir();
my $deadport = new_port();
my $pb_port = new_port();
my $conf = qq{
CREATE POOL a
CREATE SERVICE test
SET test.role = reverse_proxy
SET test.listen = 127.0.0.1:$pb_port
SET test.persist_client = 1
SET test.persist_backend = 1
SET test.pool = a
SET test.connect_ahead = 0
SET test.enable_reproxy = true
ENABLE test
CREATE SERVICE ws
SET ws.role = web_server
SET ws.listen = 127.0.0.1:$webport
SET ws.docroot = $dir
SET ws.dirindexing = 0
SET ws.persist_client = 1
ENABLE ws
};
my $msock = start_server($conf);
ok($msock, 'perlbal started');
ok(manage("POOL a ADD 127.0.0.1:$back_port"), "backend port added");
my $wc = Perlbal::Test::WebClient->new;
$wc->server("127.0.0.1:$pb_port");
$wc->keepalive(1);
$wc->http_version('1.0');
# see if a single request works
my $resp = $wc->request('status');
ok($resp, 'status response ok');
# make a file on disk, verifying we can get it via disk/URL
my $phrase = "foo bar yo this is my content.\n";
my $file_content = $phrase x 1000;
open(F, ">$dir/foo.txt");
print F $file_content;
close(F);
ok(filecontent("$dir/foo.txt") eq $file_content, "file good via disk");
my $hc = Perlbal::Test::WebClient->new;
$hc->server("127.0.0.1:$webport");
$hc->keepalive(1);
$hc->http_version('1.0');
$resp = $hc->request('foo.txt');
ok($resp && $resp->content eq $file_content, 'file good via network');
# now request some ranges on it.....
foreach my $meth (qw(http rp_file rp_url)) {
my $ua = {
'http' => $hc,
'rp_file' => $wc,
'rp_url' => $wc,
}->{$meth} || die;
my $path = {
'http' => "foo.txt",
'rp_file' => "reproxy_file:$dir/foo.txt",
'rp_url' => "reproxy_url:http://127.0.0.1:$webport/foo.txt",
}->{$meth} || die;
my $resp;
my $range;
my $send = sub {
$range = shift;
$resp = $ua->request({ headers => "Range: $range\r\n"}, $path);
};
my @aios = ("-");
if ($meth eq "rp_file" || $meth eq "http") {
@aios = qw(none ioaio);
}
foreach my $aio (@aios) {
my $setaio = $aio eq "-" ? 1 : manage("SERVER aio_mode = $aio");
SKIP: {
skip "can't do AIO mode $aio", 6 unless $setaio;
$send->("bytes=0-6");
ok($resp && $resp->content eq "foo bar", "$meth/$aio: range $range");
ok($resp->status_line =~ /^206/, "is partial") or diag(dump_res($resp));
$send->("bytes=" . length($phrase) . "-");
ok($resp && $resp->content eq ($phrase x 999), "$meth/$aio: range $range");
ok($resp->status_line =~ /^206/, "is partial") or diag(dump_res($resp));
$send->("bytes=" . length($file_content) . "-");
ok($resp && $resp->status_line =~ /^416/, "$meth/$aio: can't satisify") or diag(dump_res($resp));
$send->("bytes=5-1");
ok($resp && $resp->status_line =~ /^416/, "$meth/$aio: can't satisify") or diag(dump_res($resp));
}
}
}
# try to reproxy to a list of URLs, where the first one is bogus, and last one is good
#ok_reproxy_url_list();
sub ok_reproxy_url_list {
my $resp = $wc->request("reproxy_url_multi:$deadport:$webport:/foo.txt");
ok($resp->content eq $file_content, "reproxy URL w/ dead one first");
}
sub ok_reproxy_file {
my $resp = $wc->request("reproxy_file:$dir/foo.txt");
ok($resp && $resp->content eq $file_content, "reproxy file");
}
sub ok_reproxy_url {
my $resp = $wc->request("reproxy_url:http://127.0.0.1:$webport/foo.txt");
ok($resp->content eq $file_content, "reproxy URL");
}
sub ok_status {
my $resp = $wc->request('status');
ok($resp && $resp->content =~ /\bpid\b/, 'status ok');
}
1;
Perlbal-1.80/t/15-webserver.t 0000644 0001750 0001750 00000005763 11503530123 016076 0 ustar dormando dormando #!/usr/bin/perl
use strict;
use Perlbal::Test;
use Test::More tests => 17;
require HTTP::Request;
require HTTP::Date;
my $port = new_port();
my $dir = tempdir();
my $conf = qq{
SERVER aio_mode = none
CREATE SERVICE test
SET test.role = web_server
SET test.listen = 127.0.0.1:$port
SET test.docroot = $dir
SET test.dirindexing = 0
SET test.persist_client = 1
HEADER test insert X-Good-Day: 1
HEADER test insert X-Bad-Day: 0
ENABLE test
};
my $msock = start_server($conf);
ok($msock, "manage sock");
my $ua = ua();
ok($ua, "ua");
my ($url, $disk_file, $contents);
sub set_path {
my $path = shift;
$url = "http://127.0.0.1:$port$path";
$disk_file = "$dir$path";
}
sub set_contents {
$contents = shift;
}
sub write_file {
open(F, ">$disk_file") or die "Couldn't open $disk_file: $!\n";
print F $contents;
close F;
}
our $last_res;
sub get {
my $url = shift;
my $req = HTTP::Request->new(GET => $url);
my $res = $last_res = $ua->request($req);
return $res->is_success ? $res->content : undef;
}
# write a file to disk
mkdir "$dir/foo";
set_path("/foo/bar.txt");
set_contents("foo bar baz\n" x 1000);
write_file();
ok(filecontent($disk_file) eq $contents, "disk file verify");
# a simple get
ok(get($url) eq $contents, "GET request");
# a get with URL parameters
ok(get("$url?foo=bar") eq $contents, "GET request");
{
my $file_time = (stat($disk_file))[9];
my $req = HTTP::Request->new(GET => $url, [ 'If-Modified-Since' => HTTP::Date::time2str($file_time) ]);
my $res = $ua->request($req);
is($res->code, 304, "Got not modified");
is($res->header("Content-Length"), undef, "Shouldn't get a Content-Length header");
}
set_path("/foo/bar+baz.txt");
set_contents("foo bar baz\n" x 1000);
write_file();
ok(filecontent($disk_file) eq $contents, "disk file verify");
# a simple get
ok(get($url) eq $contents, "GET request with '+' filename");
# 404 path
ok(! get("$url/404.txt"), "missing file");
# verify directory indexing is off
{
my $dirurl = $url;
$dirurl =~ s!/[^/]+?$!/!;
my $diridx = get($dirurl);
like($diridx, qr/Directory listing disabled/, "no dirlist");
manage("SET test.dirindexing = 1");
$diridx = get($dirurl);
like($diridx, qr/bar\.txt/, "see dirlist");
}
# test that index files work
{
my $dirurl = $url;
$dirurl =~ s!/[^/]+?$!/!;
manage("SET test.dirindexing = 0");
my $diridx = get($dirurl);
like($diridx, qr/Directory listing disabled/, "no dirlist");
manage("SET test.index_files = not_here.txt, nor_here.html, bar.txt");
$diridx = get($dirurl);
like($diridx, qr/foo bar baz/, "got the index file");
manage("SET test.index_files = blah.txt");
$diridx = get($dirurl);
like($diridx, qr/Directory listing disabled/, "no dirlist again");
}
# directory traversal should fail
ok(! get("$url/../foo/bar.txt"), "directory traversal");
# files with '..' in the names should succeed
{
set_path("/foo/foo..123.txt");
write_file();
ok(get($url) eq $contents, "File with .. in name");
}
1;
Perlbal-1.80/t/20-put.t 0000644 0001750 0001750 00000007067 11713660634 014713 0 ustar dormando dormando #!/usr/bin/perl
use strict;
use warnings;
use Perlbal::Test;
use Test::More 'no_plan';
use Digest::MD5 qw/md5_base64/;
my $port = new_port();
my $dir = tempdir();
my $conf = qq{
SERVER aio_mode = none
CREATE SERVICE test
SET test.role = web_server
SET test.listen = 127.0.0.1:$port
SET test.docroot = $dir
SET test.dirindexing = 0
SET test.enable_put = 1
SET test.enable_delete = 1
SET test.min_put_directory = 0
SET test.persist_client = 1
ENABLE test
};
my $msock = start_server($conf);
my $ua = ua();
ok($ua);
require HTTP::Request;
my $url = "http://127.0.0.1:$port/foo.txt";
my $disk_file = "$dir/foo.txt";
my $content;
sub put_file {
my %opts = @_;
my $req = HTTP::Request->new(PUT => $url);
$content = "foo bar baz\n" x 1000;
if (exists $opts{content}) {
$content = $opts{content}
}
$req->content($content);
if (my $headers = $opts{headers}) {
$req->header(@$headers);
}
my $res = $ua->request($req);
return $res->is_success;
}
sub delete_file {
my $req = HTTP::Request->new(DELETE => $url);
my $res = $ua->request($req);
return $res->is_success;
}
sub verify_put {
ok(filecontent($disk_file) eq $content, "verified put");
}
sub content_md5 {
# Digest::MD5 doesn't pad base64 digests, so we have to do it ourselves
[ "Content-MD5", md5_base64($_[0]) . '==' ]
}
# successful puts
foreach_aio {
my $aio = shift;
ok(put_file(), "$aio: good put");
verify_put();
unlink $disk_file;
};
# good delete
put_file();
ok( -f $disk_file, "file exists");
ok(delete_file(), "delete file");
ok(! -f $disk_file, "file gone");
ok(! delete_file(), "deleting non-existent file");
# min_put_directory
ok(manage("SET test.min_put_directory = 2"), "min_put = 2");
foreach_aio {
my $mode = shift;
my $dir1 = "mode-$mode";
my $path = "$dir1/dir2/foo.txt";
$url = "http://127.0.0.1:$port/$path";
$disk_file = "$dir/$path";
ok(! put_file(), "aio $mode: bad put");
ok(mkdir("$dir/$dir1"), "mkdir dir1");
ok(mkdir("$dir/$dir1/dir2"), "mkdir dir1/dir2");
ok(put_file(), "aio $mode: good put at dir1/dir2/foo.txt");
verify_put();
ok(put_file(content => "", headers => [ "Content-Length" => 0 ]), "aio $mode: zero byte file put");
verify_put();
ok(unlink($disk_file), "rm file");
ok(rmdir("$dir/$dir1/dir2"), "rm dir2");
ok(rmdir("$dir/$dir1"), "rm dir1");
};
ok(manage("SET test.min_put_directory = 0"));
# let Perlbal autocreate a dir tree
foreach_aio {
my $mode = shift;
my $path = "tree-$mode/a/b/c/d/foo.txt";
$url = "http://127.0.0.1:$port/$path";
$disk_file = "$dir/$path";
ok(put_file(), "$mode: made deep file");
ok(-f $disk_file, "$mode: deep file exists");
};
# permissions
ok(put_file());
ok(manage("SET test.enable_put = 0"));
ok(! put_file(), "put disabled");
ok(manage("SET test.enable_delete = 0"));
ok(! delete_file(), "delete disabled");
ok(manage("SET test.enable_put = 1"));
ok(manage("SET test.enable_md5 = 1"));
ok(put_file(), "put re-enabled");
# Content-MD5 checking
ok(put_file(content => "!", headers => content_md5('!')), "Content-MD5 OK");
verify_put();
ok(! put_file(content => "?", headers => content_md5('!')), "Content-MD5 rejected");
ok(filecontent($disk_file) ne $content, "verified put failure");
{
my @list = (<$disk_file*>);
ok(scalar(@list) == 1 && $list[0] eq $disk_file, "no temporary file leftover");
}
$content = "!";
verify_put();
ok(manage("SET test.enable_md5 = 0"), "disable MD5 verification");
ok(put_file(content => "?", headers => content_md5('!')), "Content-MD5 NOT rejected");
verify_put();
1;
Perlbal-1.80/t/13-server-tokens.t 0000644 0001750 0001750 00000002577 11630514744 016713 0 ustar dormando dormando #!/usr/bin/perl
use strict;
use Perlbal::Test;
use Test::More tests => 13;
require HTTP::Request;
# Build conf files
my $dir = tempdir();
my @confs = (
[ new_port() => sub { my $port = shift; qq{
CREATE SERVICE test
SET role = web_server
SET listen = 127.0.0.1:$port
SET docroot = $dir
SET server_tokens = on
ENABLE test
} } ],
[ new_port() => sub { my $port = shift; qq{
CREATE SERVICE test
SET role = web_server
SET listen = 127.0.0.1:$port
SET docroot = $dir
SET server_tokens = off
ENABLE test
} } ],
);
my $count = 0;
foreach my $pair (@confs) {
my $port = $pair->[0];
my $conf = $pair->[1]->($port);
my $msock = start_server($conf);
ok($msock, "manage sock");
my $ua = ua();
ok($ua, "ua");
my $req = HTTP::Request->new( GET => "http://127.0.0.1:$port/" );
my $res = $ua->request($req);
ok( $res, 'Got result' );
isa_ok( $res, 'HTTP::Response' );
ok( $res->is_success, 'Result is successful' );
if ( $count++ == 0 ) {
# check it's on
ok( $res->header('Server'), 'Server header exists' );
is( $res->header('Server'), 'Perlbal' );
} else {
# check it's off
ok( ! $res->header('Server'), 'Server header missing' );
}
}
Perlbal-1.80/t/78-plugin-xffextras.t 0000644 0001750 0001750 00000002166 11713660634 017421 0 ustar dormando dormando use strict;
use warnings;
use lib 't/lib';
use Perlbal::Test;
use Perlbal::Test::WebClient;
use Perlbal::Test::WebServer;
use Test::More tests => 4;
my $perlbal_address = '127.0.0.1';
my $perlbal_port = new_port();
my $web_port = start_webserver();
ok($web_port, 'webserver started');
my $conf = qq{
LOAD XFFExtras
CREATE POOL a
POOL a ADD 127.0.0.1:$web_port
CREATE SERVICE proxy
SET role = reverse_proxy
SET listen = $perlbal_address:$perlbal_port
SET pool = a
SET plugins = XFFExtras
SET send_backend_port = yes
SET send_backend_proto = yes
ENABLE proxy
};
my $msock = start_server($conf);
ok($msock, 'perlbal started');
my $wc = Perlbal::Test::WebClient->new;
$wc->server("127.0.0.1:$perlbal_port");
$wc->http_version('1.0');
my $resp = $wc->request("reflect_request_headers");
my $content = $resp->content;
like($content, qr/^X-Forwarded-Port: \Q$perlbal_port\E$/mi, "Got an X-Forwarded-Port header that seems reasonable");
like($content, qr/^X-Forwarded-Proto: (?-i:http)$/mi, "Got an X-Forwarded-Proto header that seems reasonable");
Perlbal-1.80/t/99-benchmark-bool.t 0000644 0001750 0001750 00000004647 11712141550 016775 0 ustar dormando dormando use strict;
use warnings;
use Test::More 0.94 tests => 5; # last test to print
for my $class (qw/Perlbal Perlbal::Service/) {
use_ok( $class, "can load module $class" );
}
my $class = 'Perlbal::Service';
subtest 'module checking' => sub {
isa_ok( $class->new(), $class, "can create object from $class" );
};
my @words = generate_words(1000);
subtest 'check sub integrity' => sub {
is_deeply( test_optimized(), test_original(), "sub optimized" );
is_deeply( test_hash(), test_original(), "sub hash" );
};
SKIP: {
skip "need Benchmark module", 1 unless eval "require Benchmark";
subtest 'benchmark bool sub' => sub {
use_ok('Benchmark');
timethese(
shift || 100000,
{
'void' => sub {
map { 1 } @words;
},
'original' => \&test_original,
'optimized' => \&test_optimized,
'hash' => \&test_hash,
}
);
};
}
done_testing();
# helpers
sub test_original {
map { _bool_original($_) } @words;
}
sub test_optimized {
map { _bool_optimized($_) } @words;
}
sub test_hash {
map { Perlbal::Service::_bool($_) } @words;
}
sub _bool_original {
my $val = shift;
return unless defined $val;
return 1 if $val =~ /^1|true|on|yes$/i;
return 0 if $val =~ /^0|false|off|no$/i;
return undef;
}
{
# should use state
my $qr_on;
my $qr_off;
sub _bool_optimized {
my $val = shift;
return unless defined $val;
$qr_on = qr/^1|true|on|yes$/i unless defined $qr_on;
$qr_off = qr/^0|false|off|no$/i unless defined $qr_off;
return 1 if $val =~ $qr_on;
return 0 if $val =~ $qr_off;
return undef;
}
}
sub generate_words {
my $n = shift || 10;
my @words = qw/1 true on yes 0 false off no/;
my $reply = [];
for ( 1 .. $n ) {
my $w = $words[ int rand( scalar @words ) ];
if ( rand(3) > 1 ) {
if ( rand(2) > 1 ) {
$w = uc($w);
}
else {
my $l = length $w;
for my $c ( 1 .. $l ) {
next if ( rand(2) > 1 );
substr( $w, $c - 1, 1 ) = uc( getn_substr( $w, $c ) );
}
}
}
push( @$reply, $w );
}
return $reply;
}
sub getn_substr {
return substr $_[0], $_[1] - 1, 1;
}
Perlbal-1.80/t/17-webserver-concat.t 0000644 0001750 0001750 00000005366 11503530123 017344 0 ustar dormando dormando #!/usr/bin/perl
use strict;
use Perlbal::Test;
use Test::More tests => 20;
require HTTP::Request;
my $port = new_port();
my $dir = tempdir();
my $conf = qq{
SERVER aio_mode = none
CREATE SERVICE test
SET test.role = web_server
SET test.listen = 127.0.0.1:$port
SET test.docroot = $dir
SET test.dirindexing = 0
SET test.persist_client = 1
SET test.enable_concatenate_get = 1
ENABLE test
};
my $http = "http://127.0.0.1:$port";
my $msock = start_server($conf);
ok($msock, "manage sock");
my $ua = ua();
ok($ua, "ua");
sub set_disk {
my ($relpath, $contents) = @_;
open(F, ">$dir$relpath") or die "Couldn't open $dir$relpath: $!\n";
print F $contents;
close F;
}
our $last_res;
sub get {
my $url = shift;
my $req = HTTP::Request->new(GET => $url);
my $res = $last_res = $ua->request($req);
return $res->is_success ? $res->content : undef;
}
# write two files to disk
mkdir "$dir/foo";
mkdir "$dir/foo/bar";
my $chunk1 = "a" x 50 . "\n";
my $chunk2 = "b" x 50 . "\n";
set_disk("/foo/a.txt", $chunk1);
set_disk("/foo/b.txt", $chunk2);
set_disk("/foo/bar/a.txt", $chunk1);
set_disk("/foo/bar/b.txt", $chunk2);
# test trailing slash
is(get("${http}/foo??a.txt,b.txt"), undef, "need trailing slash");
is($last_res->code, 500, "got 500 without trailing slash");
# test bogus directory
is(get("${http}/bogus/??a.txt,b.txt"), undef, "bogus directory");
is($last_res->code, 404, "got 404 for bogus directory");
# test bogus file
is(get("${http}/foo/??a.txt,bogus.txt"), undef, "bogus file");
is($last_res->code, 404, "got 404 for bogus file");
is(get("${http}/foo/??a.txt,b.txt"), "$chunk1$chunk2", "basic concat works");
is(get("${http}/foo/??a.txt,bar/b.txt"), "$chunk1$chunk2", "concat w/ directory");
is(get("${http}/foo/??a.txt,a.txt"), "$chunk1$chunk1", "dup concat");
# test that if-modified-since 304 works and w/o a content-length
{
my $req = HTTP::Request->new(GET => "${http}/foo/??a.txt,bar/b.txt");
my $res = $ua->request($req);
ok($res, "got response again");
my $lastmod = $res->header("Last-Modified");
like($lastmod, qr/\bGMT$/, "and it has a last modified");
$req = HTTP::Request->new(GET => "${http}/foo/??a.txt,bar/b.txt");
$req->header("If-Modified-Since" => $lastmod);
my $ua_keep = LWP::UserAgent->new(keep_alive => 2);
$res = $ua_keep->request($req);
ok($res, "got response again");
is($res->code, 304, "the response is a 304");
like($res->header("Last-Modified"), qr/\bGMT$/, "and it has a last modified");
ok(! $res->header("Content-Length"), "No content-length");
like($res->header("Connection"), qr/\bkeep-alive\b/, "and it's keep-alive");
}
manage("SET test.enable_concatenate_get = 0");
is(get("${http}/foo/??a.txt,a.txt"), undef, "denied");
is($last_res->code, 403, "got 403");
1;
Perlbal-1.80/t/helper/ 0000755 0001750 0001750 00000000000 11722625030 014733 5 ustar dormando dormando Perlbal-1.80/t/helper/child-httpd.pl 0000755 0001750 0001750 00000000552 11503530123 017474 0 ustar dormando dormando #!/usr/bin/perl
use strict;
my $req = <>;
die "Bogus request" unless $req =~ /^GET (\/\S*) HTTP\/1\.\d/;
$| = 1;
my $uri = $1;
while (<>) {
last unless /\S/;
}
my $response = "You wanted [$uri] and I am pid=$$\n";
#warn "Response from pid $$: [$response]\n";
my $len = length $response;
print "HTTP/1.0 200 OK\r\nContent-Length: $len\r\n\r\n$response";
Perlbal-1.80/t/45-buffereduploads.t 0000644 0001750 0001750 00000013667 11503530123 017251 0 ustar dormando dormando #!/usr/bin/perl
use strict;
use Perlbal::Test;
use Perlbal::Test::WebServer;
use Perlbal::Test::WebClient;
use IO::Socket::INET;
use Test::More 'no_plan';
# setup webserver
my $web_port = start_webserver();
ok($web_port, 'webserver started');
# setup perlbal
my $port = new_port();
my $dir = tempdir();
my $conf = qq{
SERVER aio_mode = none
CREATE POOL a
POOL a ADD 127.0.0.1:$web_port
CREATE SERVICE test
SET role = reverse_proxy
SET pool = a
SET connect_ahead = 0
SET listen = 127.0.0.1:$port
SET persist_client = 1
SET buffer_uploads_path = $dir
SET buffer_uploads = 1
ENABLE test
};
$ENV{PERLBAL_DEBUG_BUFFERED_UPLOADS} = 1;
my $msock = start_server($conf);
ok($msock, 'perlbal started');
ok(! buffer_file_exists(), "no files in buffer directory");
# setup data
my $data = 'x' x 1_000_000;
my ($curpos, $clen) = (0, 0);
my $req;
# disable all of it
buffer_rules();
request("buffer_off", 500_000,
"finish",
"no-reason",
"empty");
# try writing 400k of a 500k file, and set the buffer size to be "anything
# larger than 400k"
buffer_rules(size => 400_000);
request("buffer_on_size", 500_000,
400_000,
"sleep:0.5",
"exists",
"finish",
"reason:size",
"empty");
# write a file below the limit
request("no_buffer_on_size", 350_000,
300_000,
"sleep:0.5",
"empty",
"finish",
"no-reason",
"empty");
# abort a file in the middle
request("clean_on_early_close", 500_000,
400_000,
"sleep:0.5",
"exists",
"close",
"sleep:0.5", # have to let the pb get scheduled to do cleanup
"empty",
);
# rate tests
# need to write at least 250k (default size threshold)
buffer_rules(rate => 300_000);
request("buffer_on_rate", 1_000_000,
50_000,
"sleep:2",
"empty",
300_000,
"sleep:2",
300_000,
"exists",
"finish",
"reason:rate",
"empty");
request("no_buffer_on_rate", 500_000,
"finish",
"no-reason",
"empty");
# time tests
buffer_rules(time => 3);
request("buffer_on_time", 800_000,
"sleep:2",
300_000,
"sleep:0.5",
"exists",
"finish",
"reason:time",
"empty");
request("no_buffer_on_time", 800_000,
700_000,
"sleep:0.2",
"empty",
"finish",
"no-reason");
sub buf_reason {
my $resp = shift;
return "" unless $resp && $resp->content =~ /^buffered = (\S+)$/m;
return $1;
}
sub buffer_rules {
my %opts = @_;
my $size = delete $opts{size};
my $rate = delete $opts{rate};
my $time = delete $opts{time};
die "bogus opts" if %opts;
# if they don't provide a value, set it to 0, which means threshold ignored
set_threshold('size', $size || 0);
set_threshold('rate', $rate || 0);
set_threshold('time', $time || 0);
}
sub set_threshold {
my ($which, $what) = @_;
manage("SET test.buffer_upload_threshold_$which = $what");
}
sub buffer_file_exists {
opendir DIR, $dir
or die "can't open dir\n";
foreach (readdir(DIR)) {
next if /^\./;
return 1;
}
return 0;
}
# cmds can be:
# write: writes bytes
# sleep: sleeps seconds, may be fractional
# finish (sends any final writes and/or reads response)
# close close socket
# sub {} coderef to run. gets passed response object
# no-reason response has no reason
# reason: did buffering for either "size", "rate", or "time"
# empty No files in temp buffer location
# exists Yes, a temporary file exists
sub request {
my $testname = shift;
my $len = shift || 0;
my @cmds = @_;
my $curpos = 0;
my $remain = $len;
my $hdr = "POST /status HTTP/1.0\r\nContent-length: $len\r\n\r\n";
my $sock = IO::Socket::INET->new( PeerAddr => "127.0.0.1:$port" )
or return undef;
my $rv = syswrite($sock, $hdr);
die unless $rv == length($hdr);
my $res = undef; # no response yet
foreach my $cmd (@cmds) {
my $writelen;
if ($cmd =~ /^write:([\d_]+)/) {
$writelen = $1;
$writelen =~ s/_//g;
} elsif ($cmd =~ /^(\d+)/) {
$writelen = $1;
} elsif ($cmd eq "finish") {
$writelen = $remain;
}
if ($cmd =~ /^sleep:([\d\.]+)/) {
select undef, undef, undef, $1;
next;
}
if ($cmd eq "close") {
close($sock);
next;
}
if ($cmd eq "no-reason") {
ok(! buf_reason($res), "$testname: no buffer reason");
next;
}
if ($cmd =~ /^reason:(\w+)$/) {
my $reason = $1;
is(buf_reason($res), $reason, "$testname: did buffer for $reason");
next;
}
if ($cmd eq "exists") {
ok(buffer_file_exists(), "$testname: buffer file exists");
next;
}
if ($cmd eq "empty") {
ok(! buffer_file_exists(), "$testname: no file");
next;
}
if ($writelen) {
die "Too long" if $writelen > $remain;
my $buf = "x" x $writelen;
my $rv = syswrite($sock, $buf);
die "wrote $rv ($!), not $len" unless $rv == $writelen;
$remain -= $rv;
next unless $cmd eq "finish";
}
if ($cmd eq "finish") {
$res = resp_from_sock($sock);
my $clen = $res ? $res->header('Content-Length') : 0;
ok($res && length($res->content) == $clen, "$testname: good response");
next;
}
if (ref $cmd eq "CODE") {
$cmd->($res, $testname);
next;
}
die "Invalid command: $cmd\n";
}
}
sub pid_of_resp {
my $resp = shift;
return 0 unless $resp && $resp->content =~ /^pid = (\d+)$/m;
return $1;
}
1;
Perlbal-1.80/t/76-plugin-redirect.t 0000644 0001750 0001750 00000002113 11503530123 017160 0 ustar dormando dormando #!/usr/bin/perl
use strict;
use Perlbal::Test;
use Perlbal::Test::WebServer;
use Perlbal::Test::WebClient;
use Test::More 'no_plan';
my $port = new_port();
my $conf = qq{
LOAD Redirect
LOAD Vhosts
CREATE SERVICE ss
SET role = selector
SET listen = 127.0.0.1:$port
SET persist_client = 1
SET plugins = Vhosts
VHOST example.com = test
ENABLE ss
CREATE SERVICE test
SET role = web_server
SET persist_client = 1
SET plugins = Redirect
REDIRECT HOST example.com example.net
ENABLE test
};
my $msock = start_server($conf);
ok($msock, 'perlbal started');
# make first web client
my $wc = Perlbal::Test::WebClient->new;
$wc->server("127.0.0.1:$port");
$wc->keepalive(1);
$wc->http_version('1.0');
my $resp = $wc->request({ host => "example.com", }, "foo/bar.txt"); # Test lib prepends '/' for me.
ok($resp, "Got a response");
is($resp->code, 301, "Redirect has proper code");
like($resp->header("Location"), qr{^http://example.net/foo/bar.txt$}, "Correct redirect response");
like($resp->header("Connection"), qr/Keep-Alive/i, "... and keep-alives are on");
1;
Perlbal-1.80/t/00-use.t 0000644 0001750 0001750 00000000121 11503530123 014637 0 ustar dormando dormando #!/usr/bin/perl -w
use strict;
use Test::More tests => 1;
use Perlbal;
ok(1);
Perlbal-1.80/t/52-chunked-upload.t 0000644 0001750 0001750 00000012321 11503530123 016762 0 ustar dormando dormando #!/usr/bin/perl
use strict;
use Perlbal::Test;
use Perlbal::Test::WebServer;
use Perlbal::Test::WebClient;
use IO::Socket::INET;
use Test::More 'no_plan';
# setup webserver
my $web_port = start_webserver();
ok($web_port, 'webserver started');
# setup perlbal
my $port = new_port();
my $dir = tempdir();
my $conf = qq{
SERVER aio_mode = none
CREATE POOL a
POOL a ADD 127.0.0.1:$web_port
CREATE SERVICE test
SET role = reverse_proxy
SET pool = a
SET connect_ahead = 0
SET listen = 127.0.0.1:$port
SET persist_client = 1
SET buffer_uploads_path = $dir
SET buffer_uploads = 1
ENABLE test
};
my $msock = start_server($conf);
ok($msock, 'perlbal started');
ok(! buffer_file_exists(), "no files in buffer directory");
# setup data
my $data = 'x' x 1_000_000;
my ($curpos, $clen) = (0, 0);
my $req;
# disable all of it
request("buffer_off", 500_000,
"write:500",
"write:5",
"write:5",
"write:5",
"sleep:0.25",
"exists",
"write:100000",
"write:60000",
"write:1000",
"finish",
sub {
my ($res) = @_;
my $cont = $res->content;
like($cont, qr/length = 500000/, "backend got right content-length");
},
"empty");
sub buffer_file_exists {
opendir DIR, $dir
or die "can't open dir\n";
foreach (readdir(DIR)) {
next if /^\./;
return 1;
}
return 0;
}
# cmds can be:
# write: writes bytes
# sleep: sleeps seconds, may be fractional
# finish (sends any final writes and/or reads response)
# close close socket
# sub {} coderef to run. gets passed response object
# no-reason response has no reason
# reason: did buffering for either "size", "rate", or "time"
# empty No files in temp buffer location
# exists Yes, a temporary file exists
sub request {
my $testname = shift;
my $len = shift || 0;
my @cmds = @_;
my $curpos = 0;
my $remain = $len;
my $hdr = "POST /status HTTP/1.0\r\nTransfer-Encoding: chunked\r\nExpect: 100-continue\r\n\r\n";
my $sock = IO::Socket::INET->new( PeerAddr => "127.0.0.1:$port" )
or return undef;
my $rv = syswrite($sock, $hdr);
die unless $rv == length($hdr);
# wanting HTTP/1.1 100 Continue\r\n...\r\n lines
{
my $contline = <$sock>;
die "didn't get 100 Continue line, got: $contline"
unless $contline =~ m!^HTTP/1.1 100!;
my $gotempty = 0;
while (defined(my $line = <$sock>)) {
if ($line eq "\r\n") {
$gotempty = 1;
last;
}
}
die "didn't get empty line after 100 Continue" unless $gotempty;
}
my $res = undef; # no response yet
foreach my $cmd (@cmds) {
my $writelen;
if ($cmd =~ /^write:([\d_]+)/) {
$writelen = $1;
$writelen =~ s/_//g;
} elsif ($cmd =~ /^(\d+)/) {
$writelen = $1;
} elsif ($cmd eq "finish") {
$writelen = $remain;
}
if ($cmd =~ /^sleep:([\d\.]+)/) {
select undef, undef, undef, $1;
next;
}
if ($cmd eq "close") {
close($sock);
next;
}
if ($cmd eq "exists") {
ok(buffer_file_exists(), "$testname: buffer file exists");
next;
}
if ($cmd eq "empty") {
ok(! buffer_file_exists(), "$testname: no file");
next;
}
if ($writelen) {
die "Too long" if $writelen > $remain;
my $buf = "x" x $writelen;
$buf = sprintf("%x\r\n", $writelen) . $buf . "\r\n";
$remain -= $writelen;
if ($remain == 0) {
# one \r\n for chunk ending, one for chunked-body ending,
# after (our empty) trailer...
$buf .= "0\r\n\r\n";
}
my $bufsize = length($buf);
my $off = 0;
while ($off < $bufsize) {
my $rv = syswrite($sock, $buf, $bufsize-$off, $off);
die "Error writing: $!" unless defined $rv;
die "Got rv=0 from syswrite" unless $rv;
$off += $rv;
}
next unless $cmd eq "finish";
}
if ($cmd eq "finish") {
$res = resp_from_sock($sock);
my $clen = $res ? $res->header('Content-Length') : 0;
ok($res && length($res->content) == $clen, "$testname: good response");
next;
}
if (ref $cmd eq "CODE") {
$cmd->($res, $testname);
next;
}
die "Invalid command: $cmd\n";
}
}
# Try a 0 length chunked request, as it used to crash server
{
my $hdr = "POST /status HTTP/1.0\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n";
my $sock = IO::Socket::INET->new( PeerAddr => "127.0.0.1:$port" )
or return undef;
my $rv = syswrite($sock, $hdr);
die unless $rv == length($hdr);
# Give it time to crash
select undef, undef, undef, 1.0;
my $sock2 = IO::Socket::INET->new( PeerAddr => "127.0.0.1:$port" );
ok ($sock2, 'Server still alive');
}
1;
Perlbal-1.80/t/30-reverseproxy.t 0000644 0001750 0001750 00000011373 11503530123 016636 0 ustar dormando dormando #!/usr/bin/perl
use strict;
use Perlbal::Test;
use Perlbal::Test::WebServer;
use Perlbal::Test::WebClient;
use Test::More tests => 28;
# option setup
my $start_servers = 3; # web servers to start
# setup a few web servers that we can work with
my @web_ports = map { start_webserver() } 1..$start_servers;
@web_ports = grep { $_ > 0 } map { $_ += 0 } @web_ports;
ok(scalar(@web_ports) == $start_servers, 'web servers started');
# setup a simple perlbal that uses the above server
my $pb_port = new_port();
my $conf = qq{
CREATE POOL a
CREATE SERVICE test
SET test.role = reverse_proxy
SET test.listen = 127.0.0.1:$pb_port
SET test.persist_client = 1
SET test.persist_backend = 1
SET test.pool = a
SET test.connect_ahead = 0
ENABLE test
};
my $msock = start_server($conf);
ok($msock, 'perlbal started');
add_all();
# make first web client
my $wc = Perlbal::Test::WebClient->new;
$wc->server("127.0.0.1:$pb_port");
$wc->keepalive(0);
$wc->http_version('1.0');
ok($wc, 'web client object created');
# see if a single request works
my $resp = $wc->request('status');
ok($resp, 'status response ok');
my $pid = pid_of_resp($resp);
ok($pid, 'web server functioning');
is($wc->reqdone, 0, "didn't persist to perlbal");
# verify 1 count
is(req_count(), 1, 'stats show 1 request');
# persistent is on, so let's do some more and see if they're counting up
$wc->keepalive(1);
$resp = $wc->request('status');
is(reqnum($resp), 2, "second request");
is($wc->reqdone, 1, "persist to perlbal");
$resp = $wc->request('status');
is(reqnum($resp), 3, "third request");
is($wc->reqdone, 2, "persist to perlbal again");
# verify 3 count
is(req_count(), 3, 'stats show 3 requests');
# turn persisent off and see that they're not going up
ok(manage("SET test.persist_backend = 0"), "persist backend off");
# do some request to get rid of that perlbal->backend connection (it's
# undefined whether disabling backend connections immediately
# disconnects them all or not)
$resp = $wc->request('status'); # dummy request
$resp = $wc->request('status');
is(reqnum($resp), 1, "first request");
# verify 5 count
is(req_count(), 5, 'stats show 5 requests');
# make a second webclient now to test multiple requests at once, and
# perlbal making multiple backend connections
ok(manage("SET test.persist_backend = 1"), "persist backend back on");
# testing that backend persistence works
$resp = $wc->request('status');
$pid = pid_of_resp($resp);
$resp = $wc->request('status');
ok($pid == pid_of_resp($resp), "used same backend");
# verify 7 count
is(req_count(), 7, 'stats show 7 requests');
# multiple parallel backends in operation
$resp = $wc->request("subreq:$pb_port");
$pid = pid_of_resp($resp);
my $subpid = subpid_of_resp($resp);
ok($subpid, "got subpid");
ok($subpid != $pid, "two different backends in use");
# making the web server suggest not to keep the connection alive, see if
# perlbal respects it
$resp = $wc->request('keepalive:0', 'status');
$pid = pid_of_resp($resp);
$resp = $wc->request('keepalive:0', 'status');
ok(pid_of_resp($resp) != $pid, "discarding keep-alive?");
# verify 11 count
is(req_count(), 11, 'stats show 11 requests');
######
###### verify_backend requests
######
# let's flush existing connections
manage("SET test.persist_backend = 0") or die;
$resp = $wc->request('status'); # dummy to flush (see above)
is(options($resp), 0, "got a backend that didn't do options");
manage("SET test.persist_backend = 1") or die;
ok(manage("SET test.verify_backend = 1"), "enabled verify");
$resp = $wc->request('status');
is(options($resp), 1, "got a backend that did an options");
# verify 13 count
is(req_count(), 13, 'stats show 13 requests');
$resp = $wc->request({ headers => "Content-Length: -20\r\n" }, "/foo.txt");
is($resp->code, 400, 'Bad request when negative length');
ok($resp->content =~ m/Content-Length < 0/, "Error is descriptive");
sub add_all {
foreach (@web_ports) {
manage("POOL a ADD 127.0.0.1:$_") or die;
}
}
sub remove_all {
foreach (@web_ports) {
manage("POOL a REMOVE 127.0.0.1:$_") or die;
}
}
sub flush_pools {
remove_all();
add_all();
}
sub pid_of_resp {
my $resp = shift;
return 0 unless $resp && $resp->content =~ /^pid = (\d+)$/m;
return $1;
}
sub subpid_of_resp {
my $resp = shift;
return 0 unless $resp && $resp->content =~ /^subpid = (\d+)$/m;
return $1;
}
sub reqnum {
my $resp = shift;
return 0 unless $resp && $resp->content =~ /^reqnum = (\d+)$/m;
return $1;
}
sub options {
my $resp = shift;
return undef unless $resp && $resp->content =~ /^options = (\d+)$/m;
return $1;
}
sub req_count {
my $msock = msock();
print $msock "nodes\r\n";
my $ct = 0;
while (<$msock>) {
last if /^\./;
next unless /\srequests\s(\d+)/;
$ct += $1;
}
return $ct;
}
1;
Perlbal-1.80/t/32-selector.t 0000644 0001750 0001750 00000010763 11503530123 015705 0 ustar dormando dormando #!/usr/bin/perl
use strict;
use Perlbal::Test;
use Perlbal::Test::WebServer;
use Perlbal::Test::WebClient;
use Test::More tests => 38;
# option setup
my $start_servers = 2; # web servers to start
# setup a few web servers that we can work with
my @web_ports = map { start_webserver() } 1..$start_servers;
@web_ports = grep { $_ > 0 } map { $_ += 0 } @web_ports;
ok(scalar(@web_ports) == $start_servers, 'web servers started');
# setup a simple perlbal that uses the above server
my $dir = tempdir();
my $pb_port = new_port();
my $conf = qq{
LOAD Vhosts
CREATE POOL a
CREATE SERVICE ss
SET ss.listen = 127.0.0.1:$pb_port
SET ss.role = selector
SET ss.plugins = vhosts
SET ss.persist_client = on
VHOST ss proxy = pr
VHOST ss webserver = ws
VHOST ss *.webserver = ws
VHOST ss manage = mgmt
ENABLE ss
CREATE SERVICE pr
SET pr.role = reverse_proxy
SET pr.persist_client = 1
SET pr.persist_backend = 1
SET pr.pool = a
SET pr.connect_ahead = 0
ENABLE pr
CREATE SERVICE ws
SET ws.role = web_server
SET ws.docroot = $dir
SET ws.dirindexing = 0
SET ws.persist_client = 1
SET ws.enable_put = 1
SET ws.enable_delete = 1
ENABLE ws
};
my $msock = start_server($conf);
ok($msock, 'perlbal started');
foreach (@web_ports) {
manage("POOL a ADD 127.0.0.1:$_") or die;
}
# make first web client
my $wc = Perlbal::Test::WebClient->new;
$wc->server("127.0.0.1:$pb_port");
$wc->keepalive(1);
$wc->http_version('1.0');
my $resp;
# see if a single request works
okay_status();
is($wc->reqdone, 1, "one done");
# put a file
my $file_content = "foo bar yo this is my content.\n" x 1000;
$resp = $wc->request({
method => "PUT",
content => $file_content,
host => "webserver",
}, 'foo.txt');
ok($resp && $resp->code =~ /^2\d\d$/, "Good PUT");
is($wc->reqdone, 2, "two done");
# see if it made it
ok(filecontent("$dir/foo.txt") eq $file_content, "file good via disk");
okay_network();
is($wc->reqdone, 3, "three done");
# try a post
my $blob = "x bar yo yo yeah\r\n\r\n" x 5000;
my $bloblen = length $blob;
$resp = $wc->request({
method => "POST",
content => $blob,
host => "proxy",
}, 'status');
ok($resp && $resp->content =~ /^method = POST$/m && $resp->content =~ /^length = $bloblen$/m, "proxy post");
is($wc->reqdone, 4, "four done");
okay_network();
is($wc->reqdone, 5, "five done");
okay_status();
is($wc->reqdone, 6, "six done");
# test that persist_client is based on the selector service, not the selected service
ok(manage("SET pr.persist_client = 0"), "pr.persist_client off");
okay_status();
is($wc->reqdone, 7, "seven done");
okay_status();
is($wc->reqdone, 8, "eight done");
ok(manage("SET ss.persist_client = 0"), "ss.persist_client off");
okay_status();
is($wc->reqdone, 0, "zero done");
ok(manage("SET ss.persist_client = 1"), "ss.persist_client on");
okay_status();
is($wc->reqdone, 1, "one done");
ok(manage("SET pr.persist_client = 1"), "pr.persist_client on");
# test the vhost matching
$resp = $wc->request({ host => "foo.proxy" }, 'status');
ok($resp && $resp->code =~ /^[45]/, "foo.proxy - bad");
$resp = $wc->request({ host => "foo.webserver" }, 'foo.txt');
ok($resp && $resp->code =~ /^2/, "foo.webserver - good") or diag(dump_res($resp));
$resp = $wc->request({ host => "foo.bar.webserver" }, 'foo.txt');
ok($resp && $resp->code =~ /^2/, "foo.bar.webserver - good");
$resp = $wc->request({ host => "bob" }, 'foo.txt');
ok($resp && $resp->code =~ /^[45]/, "bob - bad");
ok(manage("VHOST ss * = ws"), "enabling a default");
$resp = $wc->request({ host => "bob" }, 'foo.txt');
ok($resp && $resp->code =~ /^2/, "bob - good");
# test sending a request to a management service
$resp = $wc->request({ host => "manage" }, 'foo');
ok($resp && $resp->code =~ /^5/, "mapping to invalid service");
# test some management commands (quiet_failure makes the test framework not warn when
# these commands fail, since we expect them to)
ok(! manage("VHOST ss * ws", quiet_failure => 1), "missing equals");
ok(! manage("VHOST bad_service * = ws", quiet_failure => 1), "bad service");
ok(! manage("VHOST ss *!sdfsdf = ws", quiet_failure => 1), "bad hostname");
ok(! manage("VHOST ss * = ws!!sdf", quiet_failure => 1), "bad target");
sub okay_status {
my $resp = $wc->request({ host => "proxy" }, 'status');
ok($resp && $resp->content =~ /\bpid\b/, 'status response ok') or diag(dump_res($resp));
}
sub okay_network {
my $resp = $wc->request({ host => "webserver" }, 'foo.txt');
ok($resp && $resp->content eq $file_content, 'file good via network') or diag(dump_res($resp));
}
1;
Perlbal-1.80/t/22-chunked-put.t 0000644 0001750 0001750 00000011426 11503530123 016310 0 ustar dormando dormando #!/usr/bin/perl
use strict;
use Perlbal::Test;
use IO::Socket::INET;
use Test::More 'no_plan';
my $port = new_port();
my $dir = tempdir();
my $conf = qq{
SERVER aio_mode = none
CREATE SERVICE test
SET test.role = web_server
SET test.listen = 127.0.0.1:$port
SET test.docroot = $dir
SET test.dirindexing = 0
SET test.enable_put = 1
SET test.enable_delete = 1
SET test.min_put_directory = 0
SET test.persist_client = 1
ENABLE test
};
my $msock = start_server($conf);
my $ua = ua();
ok($ua);
require HTTP::Request;
my $url = "http://127.0.0.1:$port/foo.txt";
my $disk_file = "$dir/foo.txt";
my $contentlen = 0;
my $written_content = "";
sub buffer_file_exists {
-e $disk_file;
}
# cmds can be:
# write: writes bytes
# sleep: sleeps seconds, may be fractional
# finish (sends any final writes and/or reads response)
# close close socket
# sub {} coderef to run. gets passed response object
# no-reason response has no reason
# reason: did buffering for either "size", "rate", or "time"
# empty No files in temp buffer location
# exists Yes, a temporary file exists
sub request {
my $testname = shift;
my $len = shift || 0;
my @cmds = @_;
my $curpos = 0;
my $remain = $len;
$contentlen = 0;
$written_content = "";
my $hdr = "PUT /foo.txt HTTP/1.0\r\nTransfer-Encoding: chunked\r\nExpect: 100-continue\r\n\r\n";
my $sock = IO::Socket::INET->new( PeerAddr => "127.0.0.1:$port" )
or return undef;
my $rv = syswrite($sock, $hdr);
die unless $rv == length($hdr);
# wanting HTTP/1.1 100 Continue\r\n...\r\n lines
{
my $contline = <$sock>;
die "didn't get 100 Continue line, got: $contline"
unless $contline =~ m!^HTTP/1.1 100!;
my $gotempty = 0;
while (defined(my $line = <$sock>)) {
if ($line eq "\r\n") {
$gotempty = 1;
last;
}
}
die "didn't get empty line after 100 Continue" unless $gotempty;
}
my $res = undef; # no response yet
foreach my $cmd (@cmds) {
my $writelen;
if ($cmd =~ /^write:([\d_]+)/) {
$writelen = $1;
$writelen =~ s/_//g;
} elsif ($cmd =~ /^(\d+)/) {
$writelen = $1;
} elsif ($cmd eq "finish") {
$writelen = $remain;
}
if ($cmd =~ /^sleep:([\d\.]+)/) {
select undef, undef, undef, $1;
next;
}
if ($cmd eq "close") {
close($sock);
next;
}
if ($cmd eq "exists") {
ok(buffer_file_exists(), "$testname: buffer file exists");
next;
}
if ($writelen) {
diag("Writing: $writelen");
die "Too long" if $writelen > $remain;
my $buf = chr(int(rand(26)) + 65) x $writelen;
# update what we'll be checking for later,
$contentlen += $writelen;
$written_content .= $buf;
$buf = sprintf("%x\r\n", $writelen) . $buf . "\r\n";
$remain -= $writelen;
if ($remain == 0) {
# one \r\n for chunk ending, one for chunked-body ending,
# after (our empty) trailer...
$buf .= "0\r\n\r\n";
}
my $bufsize = length($buf);
my $off = 0;
while ($off < $bufsize) {
my $rv = syswrite($sock, $buf, $bufsize-$off, $off);
die "Error writing: $!, we had finished $off of $bufsize" unless defined $rv;
die "Got rv=0 from syswrite" unless $rv;
$off += $rv;
}
next unless $cmd eq "finish";
}
if ($cmd eq "finish") {
$res = resp_from_sock($sock);
my $clen = $res ? $res->header('Content-Length') : 0;
ok($res && length($res->content) == $clen, "$testname: good response");
next;
}
if (ref $cmd eq "CODE") {
$cmd->($res, $testname);
next;
}
die "Invalid command: $cmd\n";
}
}
sub delete_file {
my $req = HTTP::Request->new(DELETE => $url);
my $res = $ua->request($req);
return $res->is_success;
}
sub verify_put {
open(my $fh, $disk_file) or die;
my $slurp = do { local $/; <$fh>; };
ok(-s $disk_file == $contentlen && $slurp eq $written_content, "verified put");
}
# disable all of it
request("buffer_off", 500_000,
"write:500",
"write:5",
"write:5",
"write:5",
"sleep:0.25",
"exists",
"write:100000",
"write:60000",
"write:1000",
"finish",
sub {
verify_put();
},
);
1;
Perlbal-1.80/t/50-plugins.t 0000644 0001750 0001750 00000000372 11712163114 015545 0 ustar dormando dormando #!/usr/bin/perl
use strict;
use Perlbal::Test;
use Perlbal;
use Test::More tests => 7;
my @plugins = qw(Highpri Palimg Queues Stats Vhosts MaxContentLength Throttle);
foreach my $plugin (@plugins) {
require_ok("Perlbal::Plugin::$plugin");
}
Perlbal-1.80/t/32-pipelining.t 0000644 0001750 0001750 00000005627 11503530123 016226 0 ustar dormando dormando #!/usr/bin/perl
#
# For now we don't support pipelining, so these tests verify we handle it
# properly, notably not poisoning the backend by injecting two when we only
# know of one, and also dealing okay with POSTs with an extra \r\n, which
# happen in the real world, without disconnecting those users thinking
# they're bogus-pipeline-flooding us.
#
use strict;
use Perlbal::Test;
use Perlbal::Test::WebServer;
use Perlbal::Test::WebClient;
use Test::More tests => 12;
require HTTP::Request;
my $port = new_port();
my $dir = tempdir();
# setup a few web servers that we can work with
my $start_servers = 1; # web servers to start
my @web_ports = map { start_webserver() } 1..$start_servers;
@web_ports = grep { $_ > 0 } map { $_ += 0 } @web_ports;
ok(scalar(@web_ports) == $start_servers, 'web servers started');
my $conf = qq{
CREATE POOL a
CREATE SERVICE test
SET test.role = reverse_proxy
SET test.listen = 127.0.0.1:$port
SET test.persist_client = 1
SET test.persist_backend = 1
SET test.pool = a
SET test.connect_ahead = 0
ENABLE test
};
my $http = "http://127.0.0.1:$port";
my $msock = start_server($conf);
ok($msock, "manage sock");
add_all();
my $sock;
my $get_sock = sub {
return IO::Socket::INET->new(PeerAddr => "127.0.0.1:$port")
or die "Failed to connect to perlbal";
};
$sock = $get_sock->();
print $sock "POST /sleep:0.2,status HTTP/1.0\r\nContent-Length: 10\r\n\r\nfoo=56789a";
like(scalar <$sock>, qr/200 OK/, "200 OK on post w/ correct len");
$sock = $get_sock->();
print $sock "POST /sleep:0.2,status HTTP/1.0\r\nContent-Length: 10\r\n\r\nfoo=56789a\r\n";
like(scalar <$sock>, qr/200 OK/, "200 OK on post w/ extra rn not in length");
# test that signal sending works
{
my $gotsig = 0;
local $SIG{USR1} = sub { $gotsig = 1; };
$sock = $get_sock->();
print $sock "GET /kill:$$:USR1,status HTTP/1.0\r\n\r\n";
like(scalar <$sock>, qr/200 OK/, "single GET okay");
ok($gotsig, "got signal");
}
# check that somebody can't sneak extra request to backend w/ both \r\n and nothing in between requests
foreach my $sep ("\r\n", "") {
diag("separator length " . length($sep));
my $gotsig = 0;
local $SIG{USR1} = sub { $gotsig = 1; };
$sock = $get_sock->();
print $sock "POST /sleep:0.5,status HTTP/1.0\r\nConnection: keep-alive\r\nContent-Length: 10\r\n\r\nfoo=569789a${sep}GET /kill:$$:USR1,status HTTP/1.0\r\n\r\n";
like(scalar <$sock>, qr/200 OK/, "200 to POST w/ pipelined GET after");
select undef, undef, undef, 0.25;
ok(!$gotsig, "didn't get signal from GET after POST");
}
$sock = $get_sock->();
print $sock "GET /status HTTP/1.0\r\n\r\n";
like(scalar <$sock>, qr/200 OK/, "single GET okay");
$sock = $get_sock->();
print $sock "GET /status HTTP/1.0\r\n\r\nGET /status HTTP/1.0\r\n\r\n";
like(scalar <$sock>, qr/\b400\b/, "pipelined when not expecting it");
sub add_all {
foreach (@web_ports) {
manage("POOL a ADD 127.0.0.1:$_") or die;
}
}
1;
Perlbal-1.80/t/12-headers.t 0000644 0001750 0001750 00000002446 11503530123 015475 0 ustar dormando dormando #!/usr/bin/perl
use strict;
use Perlbal::Test;
use Test::More 'no_plan';
use Perlbal;
use Perlbal::HTTPHeaders;
# classes we will be testing
my @classes = ('Perlbal::HTTPHeaders');
if (eval "use Perlbal::XS::HTTPHeaders 0.20; 1;") {
push @classes, $Perlbal::XSModules{headers};
}
# verify they work
foreach my $class (@classes) {
# basic request, just tests to see if the class is functioning
my $req = \ "GET / HTTP/1.0\r\n\r\n";
my $c_req = $class->new($req);
ok($c_req, "basic request - $class");
# basic response, same
my $resp = \ "HTTP/1.0 200 OK\r\n\r\n";
my $c_resp = $class->new($resp, 1);
ok($c_resp, "basic response - $class");
# test for a bug in the XS headers that caused headers with no content
# to be disconnected from the server
my $hdr = \ "GET / HTTP/1.0\r\nHeader: content\r\nAnother: \r\nSomething:\r\n\r\n";
my $obj = $class->new($hdr);
ok($obj, "headers without content 1 - $class");
is($obj->header('header'), 'content', "headers without content 2 - $class");
is($obj->header('anoTHER'), '', "headers without content 3 - $class");
is($obj->header('notthere'), undef, "headers without content 4 - $class");
is_deeply([sort map {lc} @{ $obj->headers_list }], [qw/ another header something /], 'headers_list');
}
1;
Perlbal-1.80/t/77-plugin-throttle.t 0000644 0001750 0001750 00000005677 11712163114 017253 0 ustar dormando dormando use strict;
use warnings;
use lib 't/lib';
use IO::Select;
use Perlbal::Test;
use Perlbal::Test::WebClient;
use Perlbal::Test::WebServer;
use Time::HiRes 'time';
use Test::More tests => 2;
SKIP: {
skip 'TODO', 2;
my $perlbal_port = new_port();
my $web_port = start_webserver();
ok($web_port, 'webserver started');
my $conf = qq{
LOAD Throttle
CREATE POOL a
POOL a ADD 127.0.0.1:$web_port
CREATE SERVICE proxy
SET role = reverse_proxy
SET listen = 127.0.0.1:$perlbal_port
SET pool = a
SET initial_delay = 1
SET max_delay = 8
SET log_events=all
SET plugins = Throttle
ENABLE proxy
};
my $msock = start_server($conf);
ok($msock, 'perlbal started');
for my $n (1 .. 5) {
my $wc = Perlbal::Test::WebClient->new;
$wc->server("127.0.0.1:$perlbal_port");
$wc->http_version('1.0');
my $start = time;
my $resp = $wc->request({ host => "example.com", }, "foo/bar.txt");
my $end = time;
printf "req $n took %.2fs\n", $end - $start;
#ok($resp, "got a response");
}
#print $msock "SET whitelist_file = t/helper/whitelist.txt";
#print $msock "SET blacklist_file = t/helper/blacklist.txt";
#
#
#is($resp->code, 200, "response code correct");
#
#my @readable = $select->can_read(0.1);
#if (ok(scalar(@readable), 'syslog got messages')) {
# my @msgs = <$syslogd>;
# if (is(scalar(@msgs), 7, 'syslog got right number of messages')) {
# like($msgs[0], qr/^<173>.*localhost explicit\[\d+\]: registering TestPlugin$/, 'logged Registering');
# like($msgs[1], qr/^<174>.*localhost explicit\[\d+\]: info message in plugin$/, 'logged info message');
# like($msgs[2], qr/^<171>.*localhost explicit\[\d+\]: error message in plugin$/, 'logged error message');
# like($msgs[3], qr/^<173>.*localhost explicit\[\d+\]: printing to stdout$/, 'logged via STDOUT');
# like($msgs[4], qr/^<173>.*localhost explicit\[\d+\]: printing to stderr$/, 'logged via STDERR');
# like($msgs[5], qr/^<174>.*localhost explicit\[\d+\]: beginning run$/, 'captured internal message');
# like($msgs[6], qr/^<173>.*localhost explicit\[\d+\]: handling request in TestPlugin$/, 'logged in request');
# }
#}
#my $syslog_port = new_port();
#use IO::Socket::INET;
#my $syslogd = IO::Socket::INET->new(
# Proto => 'udp',
# Type => SOCK_DGRAM,
# LocalHost => 'localhost',
# LocalPort => $syslog_port,
# Blocking => 0,
# Reuse => 1,
#) or die "failed to listen on udp $syslog_port: $!";
#my $select = IO::Select->new($syslogd);
}
1;
__END__
=head1 TODO
* Throttling
* Banning
* Logging
* Memcached/local
* Filters
* White/blacklist
* Configs:
default_action
whitelist_file
blacklist_file
blacklist_action
throttle_threshold_seconds
initial_delay
max_delay
max_concurrent
path_regex
method_regex
log_only
memcached_servers
memcached_async_clients
instance_name
ban_threshold
ban_expiration
log_events
=cut
Perlbal-1.80/t/60-child-httpd.t 0000644 0001750 0001750 00000002101 11503530123 016255 0 ustar dormando dormando #!/usr/bin/perl
use strict;
use Perlbal::Test;
use Perlbal::Test::WebServer;
use Perlbal::Test::WebClient;
use Test::More;
use FindBin qw($Bin);
unless ($ENV{PERLBAL_TEST_ALPHA}) {
plan skip_all => 'Alpha feature; test skipped without $ENV{PERLBAL_TEST_ALPHA}';
exit 0;
} else {
plan tests => 4;
}
# setup a simple perlbal that uses the above server
my $pb_port = new_port();
my $conf = qq{
CREATE SERVICE test
SET test.role = reverse_proxy
SET test.listen = 127.0.0.1:$pb_port
SET test.persist_client = 1
SET test.persist_backend = 1
SET test.connect_ahead = 0
SET test.server_process = $Bin/helper/child-httpd.pl
ENABLE test
};
my $msock = start_server($conf);
ok($msock, 'perlbal started');
# make first web client
my $wc = Perlbal::Test::WebClient->new;
$wc->server("127.0.0.1:$pb_port");
$wc->keepalive(0);
$wc->http_version('1.0');
ok($wc, 'web client object created');
# see if a single request works
my $resp = $wc->request('status');
ok($resp, 'status response ok');
like($resp->content, qr/and I am pid=/, "got response from child process");
1;
Perlbal-1.80/t/90-accesscontrol.t 0000644 0001750 0001750 00000004062 11630514744 016742 0 ustar dormando dormando #!/usr/bin/perl
use strict;
use Perlbal::Test;
use IO::Socket::INET;
use HTTP::Request;
use Test::More;
BEGIN {
eval "require Net::Netmask"
? plan 'no_plan'
: plan skip_all => 'Net::Netmask not installed';
}
my $port = new_port();
my $dir = tempdir();
my $conf = qq{
SERVER aio_mode = none
LOAD AccessControl
CREATE SERVICE test
SET test.role = web_server
SET test.plugins = AccessControl
SET test.listen = 127.0.0.1:$port
SET test.docroot = $dir
SET test.persist_client = 1
SET test.AccessControl.use_observed_ip = 1
ENABLE test
};
my $msock = start_server($conf);
{
my $filename = "$dir/foo.txt";
open my $fh, ">", $filename;
print $fh "ooblie\n";
close $fh;
ok(-e $filename, "File was written properly");
}
my $ua = ua();
ok($ua, "UA object defined");
ok(manage("USE test"), "Manage context switch success");
sub check {
my $url = "http://127.0.0.1:$port/foo.txt";
my $req = HTTP::Request->new(GET => $url, @_);
my $res = $ua->request($req);
return $res->is_success;
}
ok(check(), "Initial request succeeds");
ok(manage("ACCESS deny ip 127.0.0.1"), "ACCESS deny was set properly");
ok(!check(), "Denied");
ok(!check(["X-Forwarded-For" => "1.1.1.1"]), "Denied with XFF header");
ok(manage("SET always_trusted = 1"), "Turning always trusted on");
ok(!check(), "Denied");
ok(check(["X-Forwarded-For" => "1.1.1.1"]), "Allowed with XFF header");
ok(manage("SET always_trusted = 0"), "Turning always trusted off again");
ok(manage("SET trusted_upstream_proxies = 127.0.0.1"), "Turning trusted upstream proxies on for 127.0.0.1");
ok(!check(), "Denied");
ok(check(["X-Forwarded-For" => "1.1.1.1"]), "Allowed with XFF header");
ok(manage("SET trusted_upstream_proxies = 10.0.0.0/24, 127.0.0.1"), "Turning trusted upstream proxies on for multiple netmasks");
ok(!check(), "Denied");
ok(check(["X-Forwarded-For" => "1.1.1.1"]), "Allowed with XFF header");
ok(manage("SET test.AccessControl.use_observed_ip = 0"), "Turning off observed IP");
ok(!check(["X-Forwarded-For" => "1.1.1.1"]), "Denied with XFF header");
Perlbal-1.80/t/35-reproxy.t 0000644 0001750 0001750 00000014404 11503530123 015574 0 ustar dormando dormando #!/usr/bin/perl
use strict;
use Perlbal::Test;
use Perlbal::Test::WebServer;
use Perlbal::Test::WebClient;
use Test::More 'no_plan';
# option setup
my $start_servers = 2; # web servers to start
# setup a few web servers that we can work with
my @web_ports = map { start_webserver() } 1..$start_servers;
@web_ports = grep { $_ > 0 } map { $_ += 0 } @web_ports;
ok(scalar(@web_ports) == $start_servers, 'web servers started');
# setup a simple perlbal that uses the above server
my $webport = new_port();
my $dir = tempdir();
my $deadport = new_port();
my $pb_port = new_port();
my $conf = qq{
CREATE POOL a
CREATE SERVICE test
SET test.role = reverse_proxy
SET test.listen = 127.0.0.1:$pb_port
SET test.persist_client = 1
SET test.persist_backend = 1
SET test.pool = a
SET test.connect_ahead = 0
SET test.enable_reproxy = 1
SET test.reproxy_cache_maxsize = 150
ENABLE test
CREATE SERVICE ws
SET ws.role = web_server
SET ws.listen = 127.0.0.1:$webport
SET ws.docroot = $dir
SET ws.dirindexing = 0
SET ws.persist_client = 1
ENABLE ws
};
my $msock = start_server($conf);
ok($msock, 'perlbal started');
add_all();
# make first web client
my $wc = Perlbal::Test::WebClient->new;
$wc->server("127.0.0.1:$pb_port");
$wc->keepalive(1);
$wc->http_version('1.0');
# see if a single request works
my $resp = $wc->request('status');
ok($resp, 'status response ok');
# make a file on disk, verifying we can get it via disk/URL
my $file_content = "foo bar yo this is my content.\n" x 1000;
open(F, ">$dir/foo.txt");
print F $file_content;
close(F);
ok(filecontent("$dir/foo.txt") eq $file_content, "file good via disk");
{
my $wc2 = Perlbal::Test::WebClient->new;
$wc2->server("127.0.0.1:$webport");
$wc2->keepalive(1);
$wc2->http_version('1.0');
$resp = $wc2->request('foo.txt');
ok($resp && $resp->content eq $file_content, 'file good via network');
}
# try to get that file, via internal file redirect
ok_reproxy_file();
ok_reproxy_file();
ok($wc->reqdone >= 2, "2 on same conn");
# reproxy URL support
ok_reproxy_url();
ok_reproxy_url();
ok($wc->reqdone >= 4, "4 on same conn");
# reproxy URL support, w/ 204s from backend
ok_reproxy_url_204();
ok_reproxy_url_204();
# reproxy cache support
{
my $sig_counter = 0;
local $SIG{USR1} = sub { $sig_counter++ };
is($sig_counter, 0, "Prior to first hit, counter should be zero.");
ok_reproxy_url_cached("One");
ok_reproxy_url_cached_ims("One");
is($sig_counter, 1, "First hit to populate the cache.");
ok_reproxy_url_cached("Two");
ok_reproxy_url_cached_ims("Two");
is($sig_counter, 1, "Second hit should be cached.");
sleep 2;
is($sig_counter, 1, "Prior to third hit, counter should still be 1.");
ok_reproxy_url_cached("Three");
ok_reproxy_url_cached_ims("Three");
is($sig_counter, 2, "Third hit isn't cached, now 2.");
ok_reproxy_url_cached("Four");
ok_reproxy_url_cached_ims("Four");
is($sig_counter, 2, "Forth hit should be cached again, still 2.");
}
# back and forth every combo
# FROM / TO: status file url
# status X X X
# file X X X
# url X X X
foreach_aio {
my $mode = shift;
ok_status();
ok_status();
ok_reproxy_file();
ok_reproxy_url();
ok_status();
ok_reproxy_url();
ok_reproxy_url();
ok_reproxy_file();
ok_reproxy_file();
ok_reproxy_url();
ok_reproxy_file();
ok_status();
ok($wc->reqdone >= 12, "AIO mode $mode: 9 transitions");
};
# try to reproxy to a list of URLs, where the first one is bogus, and last one is good
ok_reproxy_url_list();
{
my $resp = $wc->request("reproxy_url:http://127.0.0.1:$webport/bar.txt http://127.0.0.1:$webport/foo.txt");
ok($resp->content eq $file_content, "reproxy URL w/ 404 one first");
}
# responses to HEAD requests should not have a body
{
$wc->keepalive(0);
my $resp = $wc->request({
method => "HEAD",
}, "reproxy_url:http://127.0.0.1:$webport/foo.txt");
ok($resp && $resp->content eq '', "no response body when req method is HEAD");
$wc->keepalive(1);
}
my $lm;
sub ok_reproxy_url_cached {
my $resp = $wc->request("reproxy_url_cached:1:http://127.0.0.1:$webport/foo.txt");
ok($resp && $resp->content eq $file_content, "reproxy with cache: $_[0]");
like($resp->header("Connection"), qr/Keep-Alive/i, "... and keep-alives are on");
$lm = $resp->header("Last-Modified");
}
sub ok_reproxy_url_cached_ims {
die "Last-Modified hasn't been set yet" unless defined $lm;
my $resp = $wc->request({ headers => "If-Modified-Since: $lm\r\n", }, "reproxy_url_cached:1:http://127.0.0.1:$webport/foo.txt");
ok($resp, "Got a response");
is($resp->code, 304, "reproxy with cache ims, got 304 correctly: $_[0]");
like($resp->header("Connection"), qr/Keep-Alive/i, "... and keep-alives are on");
}
sub ok_reproxy_url_list {
my $resp = $wc->request("reproxy_url_multi:$deadport:$webport:/foo.txt");
ok($resp->content eq $file_content, "reproxy URL w/ dead one first");
like($resp->header("Connection"), qr/Keep-Alive/i, "... and keep-alives are on");
}
sub ok_reproxy_file {
my $resp = $wc->request("reproxy_file:$dir/foo.txt");
ok($resp && $resp->content eq $file_content, "reproxy file");
like($resp->header("Connection"), qr/Keep-Alive/i, "... and keep-alives are on");
}
sub ok_reproxy_url {
my $resp = $wc->request("reproxy_url:http://127.0.0.1:$webport/foo.txt");
ok($resp->content eq $file_content, "reproxy URL") or diag(dump_res($resp));
is($resp->code, 200, "response code is 200");
like($resp->header("Connection"), qr/Keep-Alive/i, "... and keep-alives are on");
}
sub ok_reproxy_url_204 {
my $resp = $wc->request("reproxy_url204:http://127.0.0.1:$webport/foo.txt");
ok($resp->content eq $file_content, "reproxy URL") or diag(dump_res($resp));
is($resp->code, 200, "204 response code is 200");
like($resp->header("Connection"), qr/Keep-Alive/i, "... and keep-alives are on");
}
sub ok_status {
my $resp = $wc->request('status');
ok($resp && $resp->content =~ /\bpid\b/, 'status ok');
}
sub add_all {
foreach (@web_ports) {
manage("POOL a ADD 127.0.0.1:$_") or die;
}
}
sub remove_all {
foreach (@web_ports) {
manage("POOL a REMOVE 127.0.0.1:$_") or die;
}
}
sub flush_pools {
remove_all();
add_all();
}
1;
Perlbal-1.80/t/10-testharness.t 0000644 0001750 0001750 00000000171 11503530123 016414 0 ustar dormando dormando #!/usr/bin/perl
use strict;
use Perlbal::Test;
use Test::More 'no_plan';
my $msock = start_server("");
ok($msock);
1;
Perlbal-1.80/t/75-plugin-include.t 0000644 0001750 0001750 00000001712 11503530123 017005 0 ustar dormando dormando #!/usr/bin/perl
use strict;
use Perlbal::Test;
use Test::More tests => 8;
require HTTP::Request;
require HTTP::Date;
my $dir = tempdir();
my $msock = start_server();
ok($msock, "manage sock");
ok(manage("LOAD Include"), "load include");
# Build conf files
for ('a' .. 'c') {
my $port = new_port();
my $conf = qq{
CREATE SERVICE test_$_
SET test_$_.role = web_server
SET test_$_.listen = 127.0.0.1:$port
SET test_$_.docroot = $dir
SET test_$_.dirindexing = 0
SET test_$_.persist_client = 1
ENABLE test_$_
};
open(F, ">$dir/$_.conf") or die "Couldn't open $dir/$_.conf: $!\n";
print F $conf;
close F;
}
ok(manage("INCLUDE = $dir/a.conf"), "include single");
ok(manage("INCLUDE = $dir/b* $dir/c*"), "include multi");
ok(! manage("INCLUDE = $dir/d.conf", quiet_failure => 1), "error on nonexistent conf");
my $s_output = manage_multi("show SERVICE");
for ('a' .. 'c') {
like($s_output, qr/^test_$_ .+ ENABLED/m, "test_$_ loaded");
}
1;
Perlbal-1.80/t/31-realworld.t 0000644 0001750 0001750 00000010307 11503530123 016051 0 ustar dormando dormando #!/usr/bin/perl
use strict;
use Perlbal::Test;
use Perlbal::Test::WebServer;
use Perlbal::Test::WebClient;
use Test::More tests => 157;
# option setup
my $start_servers = 3; # web servers to start
# setup a few web servers that we can work with
my @web_ports = map { start_webserver() } 1..$start_servers;
@web_ports = grep { $_ > 0 } map { $_ += 0 } @web_ports;
ok(scalar(@web_ports) == $start_servers, 'web servers started');
# setup a simple perlbal that uses the above server
my $pb_port = new_port();
my $pb_ss_port = new_port();
my $pb_ss2_port = new_port();
my $buffer_dir = tempdir();
my $conf = qq{
CREATE POOL a
CREATE SERVICE test
SET test.role = reverse_proxy
SET test.listen = 127.0.0.1:$pb_port
SET test.persist_client = 1
SET test.persist_backend = 1
SET test.pool = a
SET test.connect_ahead = 0
SET buffer_uploads_path = $buffer_dir
SET buffer_uploads = off
SET buffer_upload_threshold_size = 1
ENABLE test
LOAD Vhosts
CREATE SERVICE ss
SET role = selector
SET listen = 127.0.0.1:$pb_ss_port
SET persist_client = on
SET plugins = vhosts
VHOST * = test
ENABLE ss
CREATE SERVICE ss2
SET role = selector
SET listen = 127.0.0.1:$pb_ss2_port
SET persist_client = on
SET plugins = vhosts
VHOST * = ss
ENABLE ss2
};
my $msock = start_server($conf);
ok($msock, 'perlbal started');
my $overall_count = 1;
add_all();
{
# make first web client
my $wc = Perlbal::Test::WebClient->new;
$wc->server("127.0.0.1:$pb_port");
$wc->keepalive(0);
$wc->http_version('1.0');
ok($wc, 'web client object created');
# see if a single request works
my $resp = $wc->request('status');
ok($resp, 'status response ok');
my $pid = pid_of_resp($resp);
ok($pid, 'web server functioning');
is($wc->reqdone, 0, "didn't persist to perlbal");
is(reqnum($resp), $overall_count++, "Overall request count is correct");
}
my @combinations = (
regular => "127.0.0.1:$pb_port",
selector => "127.0.0.1:$pb_ss_port",
selector2 => "127.0.0.1:$pb_ss2_port",
);
while (@combinations) {
die "Uneven list of combinations" if @combinations % 2;
my $dport = shift @combinations;
my $wc = Perlbal::Test::WebClient->new;
$wc->http_version('1.0');
$wc->server(shift @combinations);
$wc->keepalive(1);
my $ct = 0;
for my $type (qw(plain buffer_to_memory buffer_to_disk)) {
if ($type eq "plain") {
manage("SET test.buffer_backend_connect = 0") or die;
manage("SET test.buffer_uploads = off") or die;
} elsif ($type eq "buffer_to_memory") {
manage("SET test.buffer_uploads = off") or die;
ok(manage("SET test.buffer_backend_connect = 250000"), "turned on buffering to memory");
} elsif ($type eq "buffer_to_disk") {
ok(manage("SET test.buffer_uploads = on"), "turned on buffering to disk");
}
for my $extra_rn (0, 1) {
for my $post_header_pause (0, 0.75) {
for my $n (1..2) {
$ct++;
my $resp = $wc->request({ extra_rn => $extra_rn,
method => "POST",
content => "foo=bar",
post_header_pause => $post_header_pause }, 'status');
my $info = "$dport: type=$type, extra_rn=$extra_rn, pause=$post_header_pause, n=$n: good POST";
is(reqnum($resp), $overall_count++, "Overall request count is correct: $info");
is($wc->reqdone, $ct, "persisted to perlbal");
}
}
}
}
}
sub add_all {
foreach (@web_ports) {
manage("POOL a ADD 127.0.0.1:$_") or die;
}
}
sub pid_of_resp {
my $resp = shift;
return 0 unless $resp && $resp->content =~ /^pid = (\d+)$/m;
return $1;
}
sub subpid_of_resp {
my $resp = shift;
return 0 unless $resp && $resp->content =~ /^subpid = (\d+)$/m;
return $1;
}
sub reqnum {
my $resp = shift;
return 0 unless $resp && $resp->content =~ /^reqnum = (\d+)$/m;
return $1;
}
sub options {
my $resp = shift;
return undef unless $resp && $resp->content =~ /^options = (\d+)$/m;
return $1;
}
1;
# vim: filetype=perl
Perlbal-1.80/README 0000644 0001750 0001750 00000016220 11516736365 014111 0 ustar dormando dormando Perlbal
Copyright 2004, Danga Interactive, Inc.
Copyright 2005-2010, Six Apart, Ltd.
You can use and redistribute Perlbal under the same terms as Perl itself.
http://www.danga.com/perlbal/
INSTALLATION
------------
If you have CPAN installed you can install Perlbal from the command line:
$ cpan Perlbal
See Perlbal::Manual::Install for further information on installing Perlbal,
including instructions for specific operating systems and some
troubleshooting (the file lives under lib/Perlbal/Manual/Install.pod, it is
recommended that you read it using perldoc).
DESCRIPTION
-----------
Perlbal is a Perl-based reverse proxy load balancer and web server.
It processes hundreds of millions of requests a day just for LiveJournal,
TypePad and dozens of other high-traffic websites.
Perlbal is a single-threaded event-based server supporting HTTP load
balancing, web serving, and a mix of the two (see below).
Almost everything in Perlbal can be configured or reconfigured on the fly
without needing to restart the software (see Perlbal::Manual::Management).
In this file you'll find:
* GENERAL FEATURES
* PERFORMANCE
* STATISTICS AND MONITORING
* PLUGINS (EXTENSIBILITY)
* FURTHER DOCUMENTATION
* SUPPORT
* CONTRIBUTING
GENERAL FEATURES
----------------
Perlbal has many features; this is just a short list of some of them:
Role: Reverse Proxy
* Maintains pool of connected backend connections to reduce turnover
* Gets list of nodes either from asynchronously monitored node file, or
from in-server pool objects which you can add/remove nodes from
using the management interface
* Intelligent load balancing based on what backend connections are free
for a new request. No unreliable "weighting" numbers required
* Can verify (using a quick OPTIONS request) that a backend connection is
talking to a webserver and not just the kernel's listen queue before
sending client requests at it. Lower latency for the client
* Has a high priority queue for sending requests through to backends quickly
o Uses cookies to determine if a request should go to fast queue
(configurable)
o Highpri (high priority) plugin supports making requests high
priority by URI or Host
o Can specify a relief level to let low priority requests through to
prevent starvation
* Can allow X-Forwarded-For (and similar) headers from client based on
client IP
* Configurable header management before sending request to backend
* Internal redirection to file or URL(s)
o Big one for us; a backend can instruct Perlbal to fetch the user's
data from a completely separate server and port and URL, 100%
transparent to the user
o Can actually give Perlbal a list of URLs to try. Perlbal will find
one that's alive. Again, the end user sees no redirects happening
o Can also redirect to a local file, which Perlbal will serve
non-blocking. See webserver mode below
* Persistent client connections (configurable)
* Persistent backend connections (shared by multiple clients;
no "backend waste")
Role: Web Server
* Listen on a port, share from a directory
* Directory indexing
* Byte range support (clients can resume downloads)
* Can have directory index requests fall back to index file list
o I.e., requests for /foo/ go to /foo/index.html instead
o Multiple index files supported, tries one at a time until it finds
one
* Persistent client connections (configurable)
* Almost all disk operations are done asynchronously as to not stall the
event loop
* Configurable support for storing files (PUT, DELETE support)
PERFORMANCE
-------------
* Great performance "out-of-the-box" (for both small and large sites)
* 100% asynchronous in all the recommended use cases
* Lightweight
* HTTP Header processing (optionally) done in C with
Perlbal::XS::HTTPHeaders for maximum performance
* Event-based using epoll or kqueue to avoid the scalability problems of
not-so-modern systems
STATISTICS AND MONITORING
-------------------------
Perlbal's management interface provides extremely detailed and powerful
statistics in addition to runtime configuration. For example:
* CPU usage (user, system)
* Total requests served across all services
* Requests service by individual backends
* Perlbal's uptime
* All connected sockets (and tons of info about each)
* Outstanding connections to backends
* Backends that have recently failed verification
* Pending backend connections by service
* Total of all socket states by socket type
* Size (in seconds and number of connections) of all queues
* State of reproxy engine (queued requests, outstanding requests,
backends)
* Loaded plugins per service
(All statistics are in machine readable form, easy to parse and write scripts
that check on the status of Perlbal)
PLUGINS (EXTENSIBILITY)
-----------------------
Perlbal supports the concept of having per-service (and global) plugins that
can add functionality or override many parts of request handling and behavior.
There are many custom plugins that send new headers to the backends, promote
requests to the fast queue, maintain more detailed statistics, do image
header manipulation, and more.
Writing your own plugins is also easy.
For more information on how plugins work, and a list of known plugins see Perlbal::Manual::Plugins. You may also find them easily on CPAN.
FURTHER DOCUMENTATION
---------------------
Perlbal's documentation is split into several sections under
Perlbal::Manual::*.
Perlbal::Manual provides the index for the manual:
perldoc Perlbal::Manual
Individual sections can be viewed in the same manner:
perldoc Perlbal::Manual::Configuration
perldoc Perlbal::Manual::LoadBalancer
perldoc Perlbal::Manual::Plugins
If you're interested in the internals of the Perlbal:
perldoc Perlbal::Manual::Internals
The documentation is relatively new (December 2010) and was mostly written
or gathered by Bruno Martins and José Castro under a TPF grant. You can read
more about it at http://7eip.sl.pt and http://4hw3.sl.pt.
SUPPORT
-------------
Feel free to ask us questions on the mailing list:
http://groups.google.com/group/perlbal
There are also the old Perlbal List Archives for postings until June 2008:
http://lists.danga.com/pipermail/perlbal/
CONTRIBUTING
-------------
You may find information on how to contribute under
Perlbal::Manual::Contributing.
The source code currently resides in https://github.com/perlbal/Perlbal
AUTHOR
-------------
Perlbal was originally written by Brad Fitzpatrick and counts with the help
and contributions from many other people.
See Perlbal::Manual::Credits for details.
COPYRIGHT
-------------
Copyright 2004, Danga Interactive, Inc. Copyright 2005-2010, Six Apart, Ltd.
You can use and redistribute Perlbal under the same terms as Perl itself.
Perlbal-1.80/contrib/ 0000755 0001750 0001750 00000000000 11722625030 014651 5 ustar dormando dormando Perlbal-1.80/contrib/perlbal-check.yaml 0000644 0001750 0001750 00000000443 11520230210 020216 0 ustar dormando dormando sitedefault: firstcluster
firstcluster:
boxone: 10.0.0.11:8000
boxtwo: 10.0.0.12:8000
secondcluster:
boxthree: 10.0.0.13:8000
boxfour: 10.0.0.14:8000
thirdcluster:
boxfive: 10.0.0.15:8000
boxsix: 10.0.0.16:8000
twoclusters:
- firstcluster
- secondcluster
Perlbal-1.80/contrib/perlbal-check 0000755 0001750 0001750 00000027576 11713660634 017325 0 ustar dormando dormando #!/usr/bin/perl
=head1 NAME
perlbal-check - monitor traffic on one or more perlbal instances
=head1 DESCRIPTION
Allows you to monitor potentially aggregated queues, and parse the output of
"socks" into some useful formats; top urls, top clients, etc.
=head1 AUTHORS
additions by Abe Hassan
additions by Kallen
rewritten and expanded by dormando
=head1 SEE ALSO
http://www.danga.com/perlbal/
=head1 COPYRIGHT AND LICENSE
Copyright 2008-2011, Six Apart, Ltd.
You can use and redistribute Perlbal under the same terms as Perl itself.
=cut
use strict;
use warnings;
use IO::Socket::INET;
use Getopt::Long;
use Data::Dumper qw(Dumper);
use YAML qw(LoadFile);
use List::Util qw(max);
$| = 1;
# determine options to use
my %o = ( delay => 3, mode => 'queues', site => '', min => 1,
resolve => 0, config => '/etc/perlbal-check.yaml' );
my $rv = GetOptions(\%o, 'help|h', 'mode=s', 'delay=i', 'site=s', 'min=i',
'resolve', 'config=s');
my $c;
eval { $c = LoadFile($o{config}); };
die "Could not load configuration: $@" if $@;
parse_config($c, \%o);
show_help() if ($o{help});
sub show_help {
my $sites = join('|', sort keys %$c);
print "$0 [--delay 3 --mode [queues|popular|backend|clients|topclients] --site [$sites] --min 1]\n";
exit 0;
}
# Hold established perlbal sockets.
unless ($c->{$o{site}}) {
print "Unknown site passed specified by --site. see help\n";
show_help();
}
my %PERLBAL_SOCKS = %{connect_hosts($c->{$o{site}})};
watch_queues() if $o{mode} eq 'queues';
watch_popular($o{min}) if $o{mode} eq 'popular';
watch_backends() if $o{mode} eq 'backend';
watch_clients() if $o{mode} eq 'clients';
watch_top_clients($o{min}) if $o{mode} eq 'topclients';
show_help();
# Uncomment if you want an idea of how the structure looks.
#my $r = poll_socks(\%PERLBAL_SOCKS);
#print Dumper($r), "\n";
# Flattens out any aggregates in the configuration.
# Not recursive :(
sub parse_config {
my $c = shift;
my $o = shift;
if (exists $c->{sitedefault}) {
my $site = delete $c->{sitedefault};
$o->{site} = $site unless $o->{site};
}
for my $site (keys %$c) {
next unless (ref($c->{$site}) eq 'ARRAY');
my @new = ();
for my $subsite (@{$c->{$site}}) {
die "unknown site $subsite"
unless exists $c->{$subsite};
die "subsite must be a list of servers"
unless (ref($c->{$subsite}) eq 'HASH');
push(@new, map { $_ => $c->{$subsite}->{$_} }
keys %{$c->{$subsite}});
}
$c->{$site} = {@new};
}
# Generate the "all" config.
# Stuff can get overwritten, that's fine.
my @all = ();
for my $site (keys %$c) {
push(@all, map { $_ => $c->{$site}->{$_} }
keys %{$c->{$site}});
}
$c->{all} = {@all};
}
sub connect_hosts {
my $servers = shift;
my %socks = ();
for my $name (keys %{$servers}) {
my $addr = $servers->{$name};
my $sock;
# I don't trust IO::Socket::INET's timeout. Maybe I should?
# overriding sigalrm so we don't just bail entirely
local $SIG{ALRM} = sub { 1 };
alarm(2);
eval {
$sock = IO::Socket::INET->new(PeerAddr => $addr, Timeout => 2, Proto => 'tcp');
if ($sock) {
$socks{$name} = $sock;
} else {
print "WARNING: Could not establish connection to $name: $addr\n";
}
};
alarm(0);
print "WARNING: Failed to establish connection to $name: $addr ERROR: $@\n" if $@;
}
return \%socks;
}
# Hideous ip resolution sub with cache.
my %RCACHE = ();
sub resolve {
$RCACHE{$_[0]} = gethostbyaddr(inet_aton($_[0]), AF_INET)
unless $RCACHE{$_[0]};
return $RCACHE{$_[0]};
}
# Find abusive clients? Bad clients!
sub watch_top_clients {
my $minimum = shift;
while (1) {
print "\n" . localtime() . ":\n";
# Sort by IP.
my %ips = ();
my $r = poll_socks(\%PERLBAL_SOCKS);
for my $c (@$r) {
next unless $c->{type} =~ m/^Client/;
my $o = $c->{o};
next unless $o->{observed_ip};
my $ip = $o->{observed_ip};
$ips{$ip}->{total}++;
$ips{$ip}->{drain} += $o->{draining_res} ? 1 : 0;
$ips{$ip}->{waiting} += $o->{wait_res} ? 1 : 0;
$ips{$ip}->{uris}->{$c->{http}}++ if $c->{http};
}
for (sort { $ips{$b}->{total} <=> $ips{$a}->{total} } keys %ips) {
my $data = $ips{$_};
next unless $data->{total} > $minimum;
my $name = $o{resolve} ? resolve($_) : $_;
printf "\ttotal:%3d drain:%3d waiting: %3d %s (%s)\n",
$data->{total}, $data->{drain} += 0, $data->{waiting} += 0,
$_, $name ? $name : 'NA';
next unless exists $data->{uris};
while (my ($uri, $total) = each %{$data->{uris}}) {
printf "\t\t%3d %s\n", $total, $uri;
}
}
exit if $o{delay} == 0;
sleep $o{delay};
}
}
# Some overall stats on connected clients.
sub watch_clients {
while (1) {
print "\n" . localtime() . ":\n";
my $r = poll_socks(\%PERLBAL_SOCKS);
my $total = 0;
my $reqs = 0;
my $bored = 0;
my $btime = 0;
my $drain = 0;
my $dtime = 0;
my $wait = 0;
my $wtime = 0;
for my $c (@$r) {
next unless $c->{type} =~ m/^Client/;
my $o = $c->{o};
$total++;
$reqs += $o->{reqs};
if ($o->{persist_wait}) {
$bored++; $btime += $c->{t};
} elsif ($o->{draining_res}) {
$drain++; $dtime += $c->{t};
} elsif ($o->{wait_res}) {
$wait++; $wtime += $c->{t};
}
}
printf "\ttotal: %10d avg reqs: %10.2f\n",
$total, $reqs / ($total || 1);
printf "\tbored: %10d avg time bored: %10.2f\n",
$bored, $btime / ($bored || 1);
printf "\tdrain: %10d avg time draining: %10.2f\n",
$drain, $dtime / ($drain || 1);
printf "\twaiting:%10d avg time waiting: %10.2f\n",
$wait, $wtime / ($wait || 1);
exit if $o{delay} == 0;
sleep $o{delay};
}
}
# Crap sorted list of top urls.
sub watch_popular {
my $min_counted = shift;
while (1) {
print "\n" . localtime() . ":\n";
my %u = ();
my $r = poll_socks(\%PERLBAL_SOCKS);
for my $c (@$r) {
next unless $c->{http};
$u{$c->{http}}->{total}++;
$u{$c->{http}}->{wait_res} += $c->{o}->{wait_res} ? 1 : 0;
$u{$c->{http}}->{xfer_res} += $c->{o}->{xfer_res} ? 1 : 0;
}
for (sort { $u{$b}->{total} <=> $u{$a}->{total} } keys %u) {
my $data = $u{$_};
if ($data->{total} > $min_counted) {
printf "\ttotal:%3d wait:%3d xfer:%3d %s\n",
$data->{total}, $data->{wait_res}, $data->{xfer_res}, $_;
}
}
if($o{delay} == 0) {
exit;
}
sleep $o{delay};
}
}
sub watch_backends {
my %tr = ( wait_res => 0, has_attention => 0, bored => 0, xfer_res => 0 );
while (1) {
print "\n" . localtime() . ":\n";
my %n = ();
my $maxlen = 0;
my $r = poll_socks(\%PERLBAL_SOCKS);
for my $c (@$r) {
next unless ($c->{type} eq 'BackendHTTP' &&
$c->{o}->{has_attention});
my $host = resolve($c->{host}) . ":" . $c->{port};
$maxlen = length($host) if (length($host) > $maxlen);
for (keys %tr) {
if ($c->{o}->{$_}) {
$n{$host}->{$_}++;
} elsif (!$n{$host}->{$_}) {
$n{$host}->{$_} += 0;
}
}
$n{$host}->{total}++;
}
for (sort keys %n) {
my $b = $n{$_};
printf "%*s:\t%2d/%2d [bored: %02d; wait_res: %02d; xfer: %02d]\n",
$maxlen, $_,
$b->{total} - $b->{bored}, $b->{total}, $b->{bored},
$b->{wait_res}, $b->{xfer_res};
}
if($o{delay} == 0) {
exit;
}
sleep $o{delay};
}
}
# Parse a line of perlbal socks output into a hash.
sub parse_perlbal_sock {
my $line = shift;
chomp $line;
my %s = ();
if ($line =~ m/^\s+(\d+)\s+(\d+)s\s+Perlbal::(\w+)\((\w+)\): (\w+) to ([^:]+):(\d+):\s+(.*)/) {
$s{fd} = $1;
$s{t} = $2;
$s{type} = $3;
# This should be parsed out into {r} {w}
$s{rw} = $4;
$s{state} = $5;
$s{host} = $6;
$s{port} = $7;
my $r = $8;
for my $chunk (split(/;/, $r)) {
$chunk =~ s/^\s+//;
$chunk =~ s/\s+$//;
if ($chunk =~ m!^http://!i) {
$s{http} = $chunk;
} elsif ($chunk =~ m/(.*)\s*=\s*(.*)/) {
$s{o}->{$1} = $2;
} else {
$s{o}->{$chunk}++;
}
}
return \%s;
}
}
# Pass in a list of open sockets. Polls, parses, and returns socks output for
# each.
sub poll_socks {
my $socks = shift;
my $nodes = 0;
my @conns = ();
for my $name (keys %$socks) {
my $sock = $socks->{$name};
next unless $sock;
print $sock "socks\r\n";
X: while (<$sock>) {
chomp;
last X if /^\./;
my $conn = parse_perlbal_sock($_);
# Note which perlbal it was observed from.
next unless $conn;
$conn->{perlbal} = $name;
push(@conns, $conn);
}
$nodes++;
}
return \@conns;
}
sub watch_queues {
while (1) {
print "\n" . localtime() . ":\n";
my $nodes = 0;
# Track how many nodes, per service, have queues observed.
my %qnodes = ();
my %services = ();
my $maxlen = 0;
for my $name (keys %PERLBAL_SOCKS) {
my $sock = $PERLBAL_SOCKS{$name};
next unless $sock;
print $sock "queues\r\n";
X: while (<$sock>) {
chomp;
last X if /^\./;
# service queue type count
if (m/^([^-]+)-(\w+)\.(\w+)\s(\d+)/) {
$services{$1}->{$3}->{$2} = 0 unless exists
$services{$1}->{$3}->{$2};
# Keep tabs on the longest service name.
$maxlen = length($1) if (length($1) > $maxlen);
if ($3 eq 'age') {
$services{$1}->{$3}->{$2} = max($services{$1}->{$3}->{$2}, $4);
} else {
$services{$1}->{$3}->{$2} += $4;
}
$qnodes{$1}->{$name}++ if $4 > 0;
}
}
$nodes++;
}
for my $svc (sort keys %services) {
my $queues = $services{$svc};
my $queuednodes = $qnodes{$svc} ? scalar keys %{$qnodes{$svc}} : 0;
printf "%*s ", $maxlen, $svc;
printf "[norm: %5d, age: %2ds] ", $queues->{count}->{normal},
$queues->{age}->{normal};
printf "[hi: %5d, age: %2ds] ", $queues->{count}->{highpri},
$queues->{age}->{highpri};
# Some/old perlbals don't have lowpri printed.
if (exists $queues->{count}->{lowpri}) {
printf "[lo: %5d, age: %2ds] ", $queues->{count}->{lowpri},
$queues->{age}->{lowpri};
}
printf "[queues on: %2d/%2d]\n", $queuednodes, $nodes;
}
if($o{delay} == 0) {
exit;
}
sleep $o{delay};
}
}
Perlbal-1.80/CONTRIBUTING 0000644 0001750 0001750 00000000154 11503530123 015036 0 ustar dormando dormando Want to contribute? Current instructions should be at:
http://contributing.appspot.com/perlbal
Thanks!
Perlbal-1.80/META.yml 0000644 0001750 0001750 00000002451 11722625030 014464 0 ustar dormando dormando --- #YAML:1.0
name: Perlbal
version: 1.80
abstract: Reverse-proxy load balancer and webserver
author:
- Brad Fitzpatrick
license: unknown
distribution_type: module
configure_requires:
ExtUtils::MakeMaker: 0
build_requires:
ExtUtils::MakeMaker: 0
requires:
base: 0
BSD::Resource: 0
Carp: 0
constant: 0
Danga::Socket: 1.44
Errno: 0
Exporter: 0
Fcntl: 0
fields: 0
File::Path: 0
File::Temp: 0
FindBin: 0
Getopt::Long: 0
Hash::Util: 0
HTTP::Date: 0
HTTP::Request: 0
HTTP::Response: 0
IO::File: 0
IO::Handle: 0
IO::Select: 0
IO::Socket::INET: 0
lib: 0
LWP::UserAgent: 0
Scalar::Util: 0
Socket: 0
strict: 0
Sys::Syscall: 0
Test::More: 0.94
Time::HiRes: 0
vars: 0
warnings: 0
no_index:
directory:
- t
- inc
generated_by: ExtUtils::MakeMaker version 6.55_02
meta-spec:
url: http://module-build.sourceforge.net/META-spec-v1.4.html
version: 1.4
Perlbal-1.80/lib/ 0000755 0001750 0001750 00000000000 11722625030 013757 5 ustar dormando dormando Perlbal-1.80/lib/Perlbal.pm 0000644 0001750 0001750 00000117550 11722624733 015720 0 ustar dormando dormando #!/usr/bin/perl
#
# Copyright 2004, Danga Interactive, Inc.
# Copyright 2005-2007, Six Apart, Ltd.
#
=head1 NAME
Perlbal - Reverse-proxy load balancer and webserver
=head1 SEE ALSO
L
=head1 CONTRIBUTING
Got a patch? Or a bug report? Instructions on how to contribute
are located here:
L
Thanks!
=head1 COPYRIGHT AND LICENSE
Copyright 2004, Danga Interactive, Inc.
Copyright 2005-2010, Six Apart, Ltd.
You can use and redistribute Perlbal under the same terms as Perl itself.
=cut
package Perlbal;
BEGIN {
# keep track of anonymous subs' origins:
$^P |= 0x200;
}
my $has_gladiator = eval "use Devel::Gladiator; 1;";
my $has_cycle = eval "use Devel::Cycle; 1;";
my $has_devel_peek = eval "use Devel::Peek; 1;";
use vars qw($VERSION);
$VERSION = '1.80';
use constant DEBUG => $ENV{PERLBAL_DEBUG} || 0;
use constant DEBUG_OBJ => $ENV{PERLBAL_DEBUG_OBJ} || 0;
use constant TRACK_STATES => $ENV{PERLBAL_TRACK_STATES} || 0; # if on, track states for "state changes" command
use strict;
use warnings;
no warnings qw(deprecated);
use IO::Socket;
use IO::Handle;
use IO::File;
$Perlbal::SYSLOG_AVAILABLE = eval { require Sys::Syslog; 1; };
$Perlbal::BSD_RESOURCE_AVAILABLE = eval { require BSD::Resource; 1; };
# incremented every second by a timer:
$Perlbal::tick_time = time();
# Set to 1 when we open syslog, and 0 when we close it
$Perlbal::syslog_open = 0;
use Getopt::Long;
use Carp qw(cluck croak);
use Errno qw(EBADF);
use POSIX qw(SIG_BLOCK SIG_UNBLOCK SIGINT sigprocmask);
our(%TrackVar);
sub track_var {
my ($name, $ref) = @_;
$TrackVar{$name} = $ref;
}
use Perlbal::AIO;
use Perlbal::HTTPHeaders;
use Perlbal::Service;
use Perlbal::Socket;
use Perlbal::TCPListener;
use Perlbal::UploadListener;
use Perlbal::ClientManage;
use Perlbal::ClientHTTPBase;
use Perlbal::ClientProxy;
use Perlbal::ClientHTTP;
use Perlbal::BackendHTTP;
use Perlbal::ReproxyManager;
use Perlbal::Pool;
use Perlbal::ManageCommand;
use Perlbal::CommandContext;
use Perlbal::Util;
$SIG{'PIPE'} = "IGNORE"; # handled manually
our(%hooks); # hookname => subref
our(%service); # servicename -> Perlbal::Service
our(%pool); # poolname => Perlbal::Pool
our(%plugins); # plugin => 1 (shows loaded plugins)
our($last_error);
our $service_autonumber = 1; # used to generate names for anonymous services created with Perlbal->create_service()
our $vivify_pools = 1; # if on, allow automatic creation of pools
our $foreground = 1; # default to foreground
our $track_obj = 0; # default to not track creation locations
our $reqs = 0; # total number of requests we've done
our $starttime = time(); # time we started
our $pidfile = ''; # full path, default to not writing pidfile
# used by pidfile (only makes sense before run started)
# don't rely on this variable, it might change.
our $run_started = 0;
our ($lastutime, $laststime, $lastreqs) = (0, 0, 0); # for deltas
our %PluginCase = (); # lowercase plugin name -> as file is named
# setup XS status data structures
our %XSModules; # ( 'headers' => 'Perlbal::XS::HTTPHeaders' )
# now include XS files
eval "use Perlbal::XS::HTTPHeaders;"; # if we have it, load it
# activate modules as necessary
if ($ENV{PERLBAL_XS_HEADERS} && $XSModules{headers}) {
Perlbal::XS::HTTPHeaders::enable();
}
# unactivate field::new
if ($ENV{PERLBAL_REMOVE_FIELDS}) {
use Perlbal::Fields;
Perlbal::Fields->remove();
}
# setup a USR1 signal handler that tells us to dump some basic statistics
# of how we're doing to the syslog
$SIG{'USR1'} = sub {
my $dumper = sub { Perlbal::log('info', $_[0]); };
foreach my $svc (values %service) {
run_manage_command("show service $svc->{name}", $dumper);
}
run_manage_command('states', $dumper);
run_manage_command('queues', $dumper);
};
sub error {
$last_error = shift;
return 0;
}
# Object instance counts, for debugging and leak detection
our(%ObjCount); # classname -> instances
our(%ObjTotal); # classname -> instances
our(%ObjTrack); # "$objref" -> creation location
sub objctor {
if (DEBUG_OBJ) {
my $ref = ref $_[0];
$ref .= "-$_[1]" if $_[1];
$ObjCount{$ref}++;
$ObjTotal{$ref}++;
# now, if we're tracing leaks, note this object's creation location
if ($track_obj) {
my $i = 1;
my @list;
while (my $sub = (caller($i++))[3]) {
push @list, $sub;
}
$ObjTrack{"$_[0]"} = [ time, join(', ', @list) ];
}
}
}
sub objdtor {
if (DEBUG_OBJ) {
my $ref = ref $_[0];
$ref .= "-$_[1]" if $_[1];
$ObjCount{$ref}--;
# remove tracking for this object
if ($track_obj) {
delete $ObjTrack{"$_[0]"};
}
}
}
sub register_global_hook {
$hooks{$_[0]} = $_[1];
return 1;
}
sub unregister_global_hook {
delete $hooks{$_[0]};
return 1;
}
sub run_global_hook {
my $hookname = shift;
my $ref = $hooks{$hookname};
return $ref->(@_) if defined $ref; # @_ is $mc (a Perlbal::ManageCommand)
return undef;
}
sub service_names {
return sort keys %service;
}
# class method: given a service name, returns a service object
sub service {
my $class = shift;
return $service{$_[0]};
}
sub create_service {
my $class = shift;
my $name = shift;
unless (defined($name)) {
$name = "____auto_".($service_autonumber++);
}
croak("service '$name' already exists") if $service{$name};
croak("pool '$name' already exists") if $pool{$name};
# Create the new service and return it
return $service{$name} = Perlbal::Service->new($name);
}
sub pool {
my $class = shift;
return $pool{$_[0]};
}
# given some plugin name, return its correct case
sub plugin_case {
my $pname = lc shift;
return $PluginCase{$pname} || $pname;
}
# run a block of commands. returns true if they all passed
sub run_manage_commands {
my ($cmd_block, $out, $ctx) = @_;
$ctx ||= Perlbal::CommandContext->new;
foreach my $cmd (split(/\n/, $cmd_block)) {
return 0 unless Perlbal::run_manage_command($cmd, $out, $ctx);
}
return 1;
}
# allows ${ip:eth0} in config. currently the only supported expansion
sub _expand_config_var {
my $cmd = shift;
$cmd =~ /^(\w+):(.+)/
or die "Unknown config variable: $cmd\n";
my ($type, $val) = ($1, $2);
if ($type eq "ip") {
die "Bogus-looking iface name" unless $val =~ /^\w+$/;
my $conf = `/sbin/ifconfig $val`;
$conf =~ /inet addr:(\S+)/
or die "Can't find IP of interface '$val'";
return $1;
}
die "Unknown config variable type: $type\n";
}
# returns 1 if command succeeded, 0 otherwise
sub run_manage_command {
my ($cmd, $out, $ctx) = @_; # $out is output stream closure
$cmd =~ s/\#.*//;
$cmd =~ s/^\s+//;
$cmd =~ s/\s+$//;
$cmd =~ s/\s+/ /g;
# expand variables
my $orig = $cmd; # save original case for some commands
$cmd =~ s/\$\{(.+?)\}/_expand_config_var($1)/eg;
$cmd =~ s/\$(\w+)/$ENV{$1}/g;
$cmd =~ s/^([^=]+)/lc $1/e; # lowercase everything up to an =
return 1 unless $cmd =~ /^\S/;
$out ||= sub {};
$ctx ||= Perlbal::CommandContext->new;
my $err = sub {
$out->("ERROR: $_[0]");
return 0;
};
my $ok = sub {
$out->("OK") if $ctx->verbose;
return 1;
};
return $err->("invalid command") unless $cmd =~ /^(\w+)/;
my $basecmd = $1;
my $mc = Perlbal::ManageCommand->new($basecmd, $cmd, $out, $ok, $err, $orig, $ctx);
# for testing auto crashing and recovery:
if ($basecmd eq "crash") { die "Intentional crash." };
no strict 'refs';
my $handler;
if ($Perlbal::{"MANAGE_$basecmd"} && ($handler = *{"MANAGE_$basecmd"}{CODE})) {
my $rv = eval { $handler->($mc); };
return $mc->err($@) if $@;
return $rv;
}
# if no handler found, look for plugins
# call any hooks if they've been defined
my $rval = eval { run_global_hook("manage_command.$basecmd", $mc); };
return $mc->err($@) if $@;
if (defined $rval) {
# commands may return boolean, or arrayref to mass-print
if (ref $rval eq "ARRAY") {
$mc->out($_) foreach @$rval;
return 1;
}
return $rval;
}
return $mc->err("unknown command: $basecmd");
}
sub arena_ref_counts {
my $all = Devel::Gladiator::walk_arena();
my %ct;
my %run_cycle;
foreach my $it (@$all) {
$ct{ref $it}++;
if (ref $it eq "CODE") {
my $name = Devel::Peek::CvGV($it);
$ct{$name}++ if $name =~ /ANON/;
}
}
$all = undef;
return \%ct;
}
my %last_gladiator;
sub MANAGE_gladiator {
my $mc = shift->no_opts;
unless ($has_gladiator && $has_devel_peek) {
$mc->end;
return;
}
my $ct = arena_ref_counts();
my $ret;
$ret .= "ARENA COUNTS:\n";
foreach my $k (sort {$ct->{$b} <=> $ct->{$a}} keys %$ct) {
my $delta = $ct->{$k} - ($last_gladiator{$k} || 0);
$last_gladiator{$k} = $ct->{$k};
next unless $ct->{$k} > 1;
$ret .= sprintf(" %4d %-4d $k\n", $ct->{$k}, $delta);
}
$mc->out($ret);
$mc->end;
}
sub MANAGE_varsize {
my $mc = shift->no_opts;
my $emit;
$emit = sub {
my ($v, $depth, $name) = @_;
$name ||= "";
my $show;
if (ref $v eq "ARRAY") {
return unless @$v;
$show = "[] " . scalar @$v;
}
elsif (ref $v eq "HASH") {
return unless %$v;
$show = "{} " . scalar keys %$v;
}
else {
$show = " = $v";
}
my $pre = " " x $depth;
$mc->out("$pre$name $show");
if (ref $v eq "HASH") {
foreach my $k (sort keys %$v) {
$emit->($v->{$k}, $depth+1, "{$k}");
}
}
};
foreach my $k (sort keys %TrackVar) {
my $v = $TrackVar{$k} or next;
$emit->($v, 0, $k);
}
$mc->end;
}
sub MANAGE_obj {
my $mc = shift->no_opts;
foreach (sort keys %ObjCount) {
$mc->out("$_ = $ObjCount{$_} (tot=$ObjTotal{$_})");
}
$mc->end;
}
sub MANAGE_verbose {
my $mc = shift->parse(qr/^verbose (on|off)$/,
"usage: VERBOSE {on|off}");
my $onoff = $mc->arg(1);
$mc->{ctx}->verbose(lc $onoff eq 'on' ? 1 : 0);
return $mc->ok;
}
sub MANAGE_shutdown {
my $mc = shift->parse(qr/^shutdown(\s?graceful)?\s?(\d+)?$/);
# immediate shutdown
exit(0) unless $mc->arg(1);
# set connect ahead to 0 for all services so they don't spawn extra backends
foreach my $svc (values %service) {
$svc->{connect_ahead} = 0;
}
# tell all sockets we're doing a graceful stop
my $sf = Perlbal::Socket->get_sock_ref;
foreach my $k (keys %$sf) {
my Perlbal::Socket $v = $sf->{$k};
$v->die_gracefully if $v->can("die_gracefully");
}
# register a post loop callback that will end the event loop when we only have
# a single socket left, the AIO socket
Perlbal::Socket->SetPostLoopCallback(sub {
my ($descmap, $otherfds) = @_;
# Ghetto: duplicate the code we already had for our postloopcallback
Perlbal::Socket::run_callbacks();
# see what we have here; make sure we have no Clients and no unbored Backends
foreach my $sock (values %$descmap) {
my $ref = ref $sock;
return 1 if $ref =~ /^Perlbal::Client/ && $ref ne 'Perlbal::ClientManage';
return 1 if $sock->isa('Perlbal::BackendHTTP') && $sock->{state} ne 'bored';
}
return 0; # end the event loop and thus we exit perlbal
});
# If requested, register a callback to kill the perlbal process after a specified number of seconds
if (my $timeout = $mc->arg(2)) {
Perlbal::Socket::register_callback($timeout, sub { exit(0); });
}
# so they know something happened
return $mc->ok;
}
sub MANAGE_mime {
my $mc = shift->parse(qr/^mime(?:\s+(\w+)(?:\s+(\w+))?(?:\s+(\S+))?)?$/);
my ($cmd, $arg1, $arg2) = ($mc->arg(1), $mc->arg(2), $mc->arg(3));
if (!$cmd || $cmd eq 'list') {
foreach my $key (sort keys %$Perlbal::ClientHTTPBase::MimeType) {
$mc->out("$key $Perlbal::ClientHTTPBase::MimeType->{$key}");
}
$mc->end;
} elsif ($cmd eq 'set') {
if (!$arg1 || !$arg2) {
return $mc->err("Usage: set ");
}
$Perlbal::ClientHTTPBase::MimeType->{$arg1} = $arg2;
return $mc->out("$arg1 set to $arg2.");
} elsif ($cmd eq 'remove') {
if (delete $Perlbal::ClientHTTPBase::MimeType->{$arg1}) {
return $mc->out("$arg1 removed.");
} else {
return $mc->err("$arg1 not a defined extension.");
}
} else {
return $mc->err("Usage: list, remove , set ");
}
}
sub MANAGE_xs {
my $mc = shift->parse(qr/^xs(?:\s+(\w+)\s+(\w+))?$/);
my ($cmd, $module) = ($mc->arg(1), $mc->arg(2));
if ($cmd) {
# command? verify
return $mc->err('Known XS modules: ' . join(', ', sort keys %XSModules) . '.')
unless $XSModules{$module};
# okay, so now enable or disable this module
if ($cmd eq 'enable') {
my $res = eval "return $XSModules{$module}::enable();";
return $mc->err("Unable to enable module.")
unless $res;
return $mc->ok;
} elsif ($cmd eq 'disable') {
my $res = eval "return $XSModules{$module}::disable();";
return $mc->err("Unable to disable module.")
unless $res;
return $mc->out("Module disabled.");
} else {
return $mc->err('Usage: xs [ ]');
}
} else {
# no commands, so just check status
$mc->out('XS module status:', '');
foreach my $module (sort keys %XSModules) {
my $class = $XSModules{$module};
my $enabled = eval "return \$${class}::Enabled;";
my $status = defined $enabled ? ($enabled ? "installed, enabled" :
"installed, disabled") : "not installed";
$mc->out(" $module: $status");
}
$mc->out(' No modules available.') unless %XSModules;
$mc->out('');
$mc->out("To enable a module: xs enable ");
$mc->out("To disable a module: xs disable ");
}
$mc->end;
}
sub MANAGE_fd {
my $mc = shift->no_opts;
return $mc->err('This command is not available unless BSD::Resource is installed') unless $Perlbal::BSD_RESOURCE_AVAILABLE;
# called in list context on purpose, but we want the hard limit
my (undef, $max) = BSD::Resource::getrlimit(BSD::Resource::RLIMIT_NOFILE());
my $ct = 0;
# first try procfs if one exists, as that's faster than iterating
if (opendir(DIR, "/proc/self/fd")) {
my @dirs = readdir(DIR);
$ct = scalar(@dirs) - 2; # don't count . and ..
closedir(DIR);
} else {
# isatty() is cheap enough to do on everything
foreach (0..$max) {
my $res = POSIX::isatty($_);
$ct++ if $res || ($! != EBADF);
}
}
$mc->out("max $max");
$mc->out("cur $ct");
$mc->end;
}
sub MANAGE_proc {
my $mc = shift->no_opts;
$mc->out('time: ' . time());
$mc->out('pid: ' . $$);
if ($Perlbal::BSD_RESOURCE_AVAILABLE) {
my $ru = BSD::Resource::getrusage();
my ($ut, $st) = ($ru->utime, $ru->stime);
my ($udelta, $sdelta) = ($ut - $lastutime, $st - $laststime);
$mc->out("utime: $ut (+$udelta)");
$mc->out("stime: $st (+$sdelta)");
($lastutime, $laststime, $lastreqs) = ($ut, $st, $reqs);
}
my $rdelta = $reqs - $lastreqs;
$mc->out("reqs: $reqs (+$rdelta)");
$lastreqs = $reqs;
$mc->end;
}
sub MANAGE_nodes {
my $mc = shift->parse(qr/^nodes?(?:\s+(\d+.\d+.\d+.\d+)(?::(\d+))?)?$/);
my ($ip, $port) = ($mc->arg(1), $mc->arg(2) || 80);
my $spec_ipport = $ip ? "$ip:$port" : undef;
my $ref = \%Perlbal::BackendHTTP::NodeStats;
my $dump = sub {
my $ipport = shift;
foreach my $key (keys %{$ref->{$ipport}}) {
if (ref $ref->{$ipport}->{$key} eq 'ARRAY') {
my %temp;
$temp{$_}++ foreach @{$ref->{$ipport}->{$key}};
foreach my $tkey (keys %temp) {
$mc->out("$ipport $key $tkey $temp{$tkey}");
}
} else {
$mc->out("$ipport $key $ref->{$ipport}->{$key}");
}
}
};
# dump a node, or all nodes
if ($spec_ipport) {
$dump->($spec_ipport);
} else {
foreach my $ipport (keys %$ref) {
$dump->($ipport);
}
}
$mc->end;
}
# singular also works for the nodes command
*MANAGE_node = \&MANAGE_nodes;
sub MANAGE_prof {
my $mc = shift->parse(qr/^prof\w*\s+(on|off|data)$/);
my $which = $mc->arg(1);
if ($which eq 'on') {
if (Danga::Socket->EnableProfiling) {
return $mc->ok;
} else {
return $mc->err('Unable to enable profiling. Please ensure you have the BSD::Resource module installed.');
}
}
if ($which eq 'off') {
Danga::Socket->DisableProfiling;
return $mc->ok;
}
if ($which eq 'data') {
my $href = Danga::Socket->ProfilingData;
foreach my $key (sort keys %$href) {
my ($utime, $stime, $calls) = @{$href->{$key}};
$mc->out(sprintf("%s %0.5f %0.5f %d %0.7f %0.7f",
$key, $utime, $stime, $calls, $utime / $calls, $stime / $calls));
}
$mc->end;
}
}
sub MANAGE_uptime {
my $mc = shift->no_opts;
$mc->out("starttime $starttime");
$mc->out("uptime " . (time() - $starttime));
$mc->out("version $Perlbal::VERSION");
$mc->end;
}
*MANAGE_version = \&MANAGE_uptime;
sub MANAGE_track {
my $mc = shift->no_opts;
my $now = time();
my @list;
foreach (keys %ObjTrack) {
my $age = $now - $ObjTrack{$_}->[0];
push @list, [ $age, "${age}s $_: $ObjTrack{$_}->[1]" ];
}
# now output based on sorted age
foreach (sort { $a->[0] <=> $b->[0] } @list) {
$mc->out($_->[1]);
}
$mc->end;
}
sub MANAGE_socks {
my $mc = shift->parse(qr/^socks(?: (\w+))?$/);
my $mode = $mc->arg(1) || "all";
my $sf = Perlbal::Socket->get_sock_ref;
if ($mode eq "summary") {
my %count;
my $write_buf = 0;
my $open_files = 0;
while (my $k = each %$sf) {
my Perlbal::Socket $v = $sf->{$k};
$count{ref $v}++;
$write_buf += $v->{write_buf_size};
if ($v->isa("Perlbal::ClientHTTPBase")) {
my Perlbal::ClientHTTPBase $cv = $v;
$open_files++ if $cv->{'reproxy_fh'};
}
}
foreach (sort keys %count) {
$mc->out(sprintf("%5d $_", $count{$_}));
}
$mc->out();
$mc->out(sprintf("Aggregate write buffer: %.1fk", $write_buf / 1024));
$mc->out(sprintf(" Open files: %d", $open_files));
} elsif ($mode eq "all") {
my $now = time;
$mc->out(sprintf("%5s %6s", "fd", "age"));
foreach (sort { $a <=> $b } keys %$sf) {
my $sock = $sf->{$_};
my $age;
eval {
$age = $now - $sock->{create_time};
};
$age ||= 0;
$mc->out(sprintf("%5d %5ds %s", $_, $age, $sock->as_string));
}
}
$mc->end;
}
sub MANAGE_backends {
my $mc = shift->no_opts;
my $sf = Perlbal::Socket->get_sock_ref;
my %nodes; # { "Backend" => int count }
foreach my $sock (values %$sf) {
if ($sock->isa("Perlbal::BackendHTTP")) {
my Perlbal::BackendHTTP $cv = $sock;
$nodes{"$cv->{ipport}"}++;
}
}
# now print out text
foreach my $node (sort keys %nodes) {
$mc->out("$node " . $nodes{$node});
}
$mc->end;
}
sub MANAGE_noverify {
my $mc = shift->no_opts;
# shows the amount of time left for each node marked as noverify
my $now = time;
foreach my $ipport (keys %Perlbal::BackendHTTP::NoVerify) {
my $until = $Perlbal::BackendHTTP::NoVerify{$ipport} - $now;
$mc->out("$ipport $until");
}
$mc->end;
}
sub MANAGE_pending {
my $mc = shift->no_opts;
# shows pending backend connections by service, node, and age
my %pend; # { "service" => { "ip:port" => age } }
my $now = time;
foreach my $svc (values %service) {
foreach my $ipport (keys %{$svc->{pending_connects}}) {
my Perlbal::BackendHTTP $be = $svc->{pending_connects}->{$ipport};
next unless defined $be;
$pend{$svc->{name}}->{$ipport} = $now - $be->{create_time};
}
}
foreach my $name (sort keys %pend) {
foreach my $ipport (sort keys %{$pend{$name}}) {
$mc->out("$name $ipport $pend{$name}{$ipport}");
}
}
$mc->end;
}
sub MANAGE_states {
my $mc = shift->parse(qr/^states(?:\s+(.+))?$/);
my $svc;
if (defined $mc->arg(1)) {
$svc = $service{$mc->arg(1)};
return $mc->err("Service not found.")
unless defined $svc;
}
my $sf = Perlbal::Socket->get_sock_ref;
my %states; # { "Class" => { "State" => int count; } }
foreach my $sock (values %$sf) {
next unless $sock->can('state');
my $state = $sock->state;
next unless defined $state;
if (defined $svc) {
next unless $sock->isa('Perlbal::ClientProxy') ||
$sock->isa('Perlbal::BackendHTTP') ||
$sock->isa('Perlbal::ClientHTTP');
next unless $sock->{service} == $svc;
}
$states{ref $sock}->{$state}++;
}
# now print out text
foreach my $class (sort keys %states) {
foreach my $state (sort keys %{$states{$class}}) {
$mc->out("$class $state " . $states{$class}->{$state});
}
}
$mc->end;
}
sub MANAGE_queues {
my $mc = shift->no_opts;
my $now = time;
foreach my $svc (values %service) {
next unless $svc->{role} eq 'reverse_proxy';
my %queues = (
normal => 'waiting_clients',
highpri => 'waiting_clients_highpri',
lowpri => 'waiting_clients_lowpri',
);
while (my ($queue_name, $clients_key) = each %queues) {
my $age = 0;
my $count = @{$svc->{$clients_key}};
my Perlbal::ClientProxy $oldest = $svc->{$clients_key}->[0];
$age = $now - $oldest->{last_request_time} if defined $oldest;
$mc->out("$svc->{name}-$queue_name.age $age");
$mc->out("$svc->{name}-$queue_name.count $count");
}
}
$mc->end;
}
sub MANAGE_state {
my $mc = shift->parse(qr/^state changes$/);
my $hr = Perlbal::Socket->get_statechange_ref;
my %final; # { "state" => count }
while (my ($obj, $arref) = each %$hr) {
$mc->out("$obj: " . join(', ', @$arref));
$final{$arref->[-1]}++;
}
foreach my $k (sort keys %final) {
$mc->out("$k $final{$k}");
}
$mc->end;
}
sub MANAGE_leaks {
my $mc = shift->parse(qr/^leaks(?:\s+(.+))?$/);
return $mc->err("command disabled without \$ENV{PERLBAL_DEBUG} set")
unless $ENV{PERLBAL_DEBUG};
my $what = $mc->arg(1);
# iterates over active objects. if you specify an argument, it is treated as code
# with $_ being the reference to the object.
# shows objects that we think might have been leaked
my $ref = Perlbal::Socket::get_created_objects_ref;
foreach (@$ref) {
next unless $_; # might be undef!
if ($what) {
my $rv = eval "$what";
return $mc->err("$@") if $@;
next unless defined $rv;
$mc->out($rv);
} else {
$mc->out($_->as_string);
}
}
$mc->end;
}
sub MANAGE_show {
my $mc = shift;
if ($mc->cmd =~ /^show service (\w+)$/) {
my $sname = $1;
my Perlbal::Service $svc = $service{$sname};
return $mc->err("Unknown service") unless $svc;
$svc->stats_info($mc->out);
return $mc->end;
}
if ($mc->cmd =~ /^show pool(?:\s+(\w+))?$/) {
my $pool = $1;
if ($pool) {
my $pl = $pool{$pool};
return $mc->err("pool '$pool' does not exist") unless $pl;
foreach my $node (@{ $pl->nodes }) {
my $ipport = "$node->[0]:$node->[1]";
$mc->out($ipport . " " . $pl->node_used($ipport));
}
} else {
foreach my $name (sort keys %pool) {
my Perlbal::Pool $pl = $pool{$name};
$mc->out("$name nodes $pl->{node_count}");
$mc->out("$name services $pl->{use_count}");
}
}
return $mc->end;
}
if ($mc->cmd =~ /^show service$/) {
foreach my $name (sort keys %service) {
my $svc = $service{$name};
my $listen = $svc->{listen} || "not_listening";
$mc->out("$name $listen " . ($svc->{enabled} ? "ENABLED" : "DISABLED"));
}
return $mc->end;
}
return $mc->parse_error;
}
sub MANAGE_server {
my $mc = shift->parse(qr/^server (\S+) ?= ?(.+)$/);
my ($key, $val) = ($mc->arg(1), $mc->arg(2));
if ($key =~ /^max_reproxy_connections(?:\((.+)\))?/) {
return $mc->err("Expected numeric parameter") unless $val =~ /^-?\d+$/;
my $hostip = $1;
if (defined $hostip) {
$Perlbal::ReproxyManager::ReproxyMax{$hostip} = $val+0;
} else {
$Perlbal::ReproxyManager::ReproxyGlobalMax = $val+0;
}
return $mc->ok;
}
if ($key eq "max_connections") {
return $mc->err('This command is not available unless BSD::Resource is installed') unless $Perlbal::BSD_RESOURCE_AVAILABLE;
return $mc->err("Expected numeric parameter") unless $val =~ /^-?\d+$/;
my $rv = BSD::Resource::setrlimit(BSD::Resource::RLIMIT_NOFILE(), $val, $val);
unless (defined $rv && $rv) {
if ($> == 0) {
$mc->err("Unable to set limit.");
} else {
$mc->err("Need to be root to increase max connections.");
}
}
return $mc->ok;
}
if ($key eq "nice_level") {
return $mc->err("Expected numeric parameter") unless $val =~ /^-?\d+$/;
my $rv = POSIX::nice($val);
$mc->err("Unable to renice: $!")
unless defined $rv;
return $mc->ok;
}
if ($key eq "aio_mode") {
return $mc->err("Unknown AIO mode") unless $val =~ /^none|linux|ioaio$/;
return $mc->err("Linux::AIO no longer supported") if $val eq "linux";
return $mc->err("IO::AIO not available") if $val eq "ioaio" && ! $Perlbal::OPTMOD_IO_AIO;
$Perlbal::AIO_MODE = $val;
return $mc->ok;
}
if ($key eq "aio_threads") {
return $mc->err("Expected numeric parameter") unless $val =~ /^-?\d+$/;
IO::AIO::min_parallel($val)
if $Perlbal::OPTMOD_IO_AIO;
return $mc->ok;
}
if ($key eq "track_obj") {
return $mc->err("Expected 1 or 0") unless $val eq '1' || $val eq '0';
$track_obj = $val + 0;
%ObjTrack = () if $val; # if we're turning it on, clear it out
return $mc->ok;
}
if ($key eq "pidfile") {
return $mc->err("pidfile must be configured at startup, before Perlbal::run is called") if $run_started;
return $mc->err("Expected full pathname to pidfile") unless $val;
$pidfile = $val;
return $mc->ok;
}
if ($key eq "crash_backtrace") {
return $mc->err("Expected 1 or 0") unless $val eq '1' || $val eq '0';
if ($val) {
$SIG{__DIE__} = sub { Carp::confess(@_) };
} else {
$SIG{__DIE__} = undef;
}
return $mc->ok;
}
return $mc->err("unknown server option '$val'");
}
sub MANAGE_dumpconfig {
my $mc = shift;
while (my ($name, $pool) = each %pool) {
$mc->out("CREATE POOL $name");
if ($pool->can("dumpconfig")) {
foreach my $line ($pool->dumpconfig) {
$mc->out(" $line");
}
} else {
my $class = ref($pool);
$mc->out(" # Pool class '$class' is unable to dump config.");
}
} continue {
$mc->out("");
}
while (my ($name, $service) = each %service) {
$mc->out("CREATE SERVICE $name");
if ($service->can("dumpconfig")) {
foreach my $line ($service->dumpconfig) {
$mc->out(" $line");
}
} else {
my $class = ref($service);
$mc->out(" # Service class '$class' is unable to dump config.");
}
my $state = $service->{enabled} ? "ENABLE" : "DISABLE";
$mc->out("$state $name");
} continue {
$mc->out("");
}
return $mc->ok
}
sub MANAGE_reproxy_state {
my $mc = shift;
Perlbal::ReproxyManager::dump_state($mc->out);
return 1;
}
sub MANAGE_create {
my $mc = shift->parse(qr/^create (service|pool) (\w+)$/,
"usage: CREATE {service|pool} ");
my ($what, $name) = $mc->args;
if ($what eq "service") {
return $mc->err("service '$name' already exists") if $service{$name};
return $mc->err("pool '$name' already exists") if $pool{$name};
Perlbal->create_service($name);
$mc->{ctx}{last_created} = $name;
return $mc->ok;
}
if ($what eq "pool") {
return $mc->err("pool '$name' already exists") if $pool{$name};
return $mc->err("service '$name' already exists") if $service{$name};
$vivify_pools = 0;
$pool{$name} = Perlbal::Pool->new($name);
$mc->{ctx}{last_created} = $name;
return $mc->ok;
}
}
sub MANAGE_use {
my $mc = shift->parse(qr/^use (\w+)$/,
"usage: USE ");
my ($name) = $mc->args;
return $mc->err("Non-existent pool or service '$name'") unless $pool{$name} || $service{$name};
$mc->{ctx}{last_created} = $name;
return $mc->ok;
}
sub MANAGE_pool {
my $mc = shift->parse(qr/^pool (\w+) (\w+) (\d+.\d+.\d+.\d+)(?::(\d+))?$/);
my ($cmd, $name, $ip, $port) = $mc->args;
$port ||= 80;
my $good_cmd = qr/^(?:add|remove)$/;
# "add" and "remove" can be in either order
($cmd, $name) = ($name, $cmd) if $name =~ /$good_cmd/;
return $mc->err("Invalid command: must be 'add' or 'remove'")
unless $cmd =~ /$good_cmd/;
my $pl = $pool{$name};
return $mc->err("Pool '$name' not found") unless $pl;
$pl->$cmd($ip, $port);
return $mc->ok;
}
sub MANAGE_default {
my $mc = shift->parse(qr/^default (\w+) ?= ?(.+)$/,
"usage: DEFAULT = ");
my ($key, $val) = $mc->args;
return Perlbal::Service::set_defaults($mc, $key => $val);
}
sub MANAGE_set {
my $mc = shift->parse(qr/^set (?:(\w+)[\. ])?([\w\.]+) ?= ?(.+)$/,
"usage: SET [] = ");
my ($name, $key, $val) = $mc->args;
unless ($name ||= $mc->{ctx}{last_created}) {
return $mc->err("omitted service/pool name not implied from context");
}
if (my Perlbal::Service $svc = $service{$name}) {
return $svc->set($key, $val, $mc);
} elsif (my Perlbal::Pool $pl = $pool{$name}) {
return $pl->set($key, $val, $mc);
}
return $mc->err("service/pool '$name' does not exist");
}
sub MANAGE_header {
my $mc = shift->parse(qr/^header\s+(\w+)\s+(insert|remove)\s+(.+?)(?:\s*:\s*(.+))?$/i,
"Usage: HEADER {INSERT|REMOVE} [: ]");
my ($svc_name, $action, $header, $val) = $mc->args;
my $svc = $service{$svc_name};
return $mc->err("service '$svc_name' does not exist") unless $svc;
return $svc->header_management($action, $header, $val, $mc);
}
sub MANAGE_enable {
my $mc = shift->parse(qr/^(disable|enable) (\w+)$/,
"Usage: {ENABLE|DISABLE} ");
my ($verb, $name) = $mc->args;
my $svc = $service{$name};
return $mc->err("service '$name' does not exist") unless $svc;
return $svc->$verb($mc);
}
*MANAGE_disable = \&MANAGE_enable;
sub MANAGE_unload {
my $mc = shift->parse(qr/^unload (\w+)$/);
my ($fn) = $mc->args;
$fn = $PluginCase{lc $fn};
my $rv = eval "Perlbal::Plugin::$fn->unload; 1;";
$plugins{$fn} = 0;
return $mc->ok;
}
sub MANAGE_load {
my $mc = shift->parse(qr/^load \w+$/);
my $fn;
$fn = $1 if $mc->orig =~ /^load (\w+)$/i;
my $last_case;
my $last_class;
my $good_error;
# TODO case protection
foreach my $name ($fn, lc $fn, ucfirst lc $fn) {
$last_case = $name;
my $class = $last_class = "Perlbal::Plugin::$name";
my $file = $class . ".pm";
$file =~ s!::!/!g;
my $rv = eval "use $class; $class->can('load');";
if ($rv) {
$good_error = undef;
last;
}
# If we don't have a good error yet, start with this one.
$good_error = $@ unless defined $good_error;
# If the file existed perl will place an entry in %INC (though it will be undef due to compilation error)
if ($@ and exists $INC{$file}) {
$good_error = $@;
last;
}
}
unless (defined $good_error) {
my $rv = eval "$last_class->load; 1;";
if ($rv) {
$PluginCase{lc $fn} = $last_case;
$plugins{$last_case} = $last_class;
return $mc->ok;
}
$good_error = $@;
}
return $mc->err($good_error);
}
sub MANAGE_reload {
my $mc = shift->parse(qr/^reload (\w+)$/);
my ($fn) = $mc->args;
my $class = $PluginCase{lc $fn} or
return $mc->err("Unknown/unloaded plugin '$fn'");
$class = "Perlbal::Plugin::$class";
eval "$class->can_reload" or
return $mc->err("Plugin $class doesn't support reloading");
if ($class->can("pre_reload_unload")) {
eval "$class->pre_reload_unload; 1" or
return $mc->err("Error running $class->pre_reload_unload: $@");
}
eval "$class->unload; 1;" or
return $mc->err("Failed to unload $class: $@");
my $file = $class . ".pm";
$file =~ s!::!/!g;
delete $INC{$file} or
die $mc->err("Didn't find $file in %INC");
no warnings 'redefine';
eval "use $class; $class->load; 1;" or
return $mc->err("Failed to reload: $@");
return $mc->ok;
}
sub MANAGE_plugins {
my $mc = shift->no_opts;
foreach my $svc (values %service) {
next unless @{$svc->{plugin_order}};
$mc->out(join(' ', $svc->{name}, @{$svc->{plugin_order}}));
}
$mc->end;
}
sub MANAGE_help {
my $mc = shift->no_opts;
my @commands = sort map { m/^MANAGE_(\S+)$/ ? $1 : () }
keys %Perlbal::;
foreach my $command (@commands) {
$mc->out("$command");
}
$mc->end;
}
sub MANAGE_aio {
my $mc = shift->no_opts;
my $stats = Perlbal::AIO::get_aio_stats();
foreach my $c (sort keys %$stats) {
my $r = $stats->{$c};
foreach my $k (keys %$r) {
$mc->out("$c $k $r->{$k}");
}
}
$mc->end;
}
sub load_config {
my ($file, $writer) = @_;
open (my $fh, $file) or die "Error opening config file ($file): $!\n";
my $ctx = Perlbal::CommandContext->new;
$ctx->verbose(0);
while (my $line = <$fh>) {
return 0 unless run_manage_command($line, $writer, $ctx);
}
close($fh);
return 1;
}
sub daemonize {
my($pid, $sess_id, $i);
# note that we're not in the foreground (for logging purposes)
$foreground = 0;
# required before fork: (as of old Linux::AIO 1.1, still true?)
IO::AIO::max_parallel(0)
if $Perlbal::OPTMOD_IO_AIO;
my $sigset = POSIX::SigSet->new(SIGINT);
sigprocmask(SIG_BLOCK, $sigset)
or die "Can't block sigint for fork: $!";
## Fork and exit parent
if ($pid = fork) { exit 0; }
sigprocmask(SIG_UNBLOCK, $sigset)
or die "Can't unblock sigint after fork: $!";
## Detach ourselves from the terminal
croak "Cannot detach from controlling terminal"
unless $sess_id = POSIX::setsid();
# Handler for INT needs to be restored.
$SIG{INT} = 'DEFAULT';
## Change working directory
chdir "/";
## Clear file creation mask
umask 0;
## Close open file descriptors
close(STDIN);
close(STDOUT);
close(STDERR);
## Reopen stderr, stdout, stdin to /dev/null
open(STDIN, "+>/dev/null");
open(STDOUT, "+>&STDIN");
open(STDERR, "+>&STDIN");
}
# For other apps using Danga::Socket that want to embed Perlbal, this can be called
# directly to start it up. You can call this as many times as you like; it'll
# only actually do what it does the first time it's called.
sub initialize {
unless ($run_started) {
$run_started = 1;
# number of AIO threads. the number of outstanding requests isn't
# affected by this
IO::AIO::min_parallel(3) if $Perlbal::OPTMOD_IO_AIO;
# register IO::AIO pipe which gets written to from threads
# doing blocking IO
if ($Perlbal::OPTMOD_IO_AIO) {
Perlbal::Socket->AddOtherFds(IO::AIO::poll_fileno() =>
\&IO::AIO::poll_cb);
}
# The fact that this only runs the first time someone calls initialize()
# means that some things which depend on it might be unreliable when
# used in an embedded perlbal if there is a race for multiple components
# to call initialize().
run_global_hook("pre_event_loop");
}
}
# This is the function to call if you want Perlbal to be in charge of the event loop.
# It won't return until Perlbal is somehow told to exit.
sub run {
# setup for logging
Sys::Syslog::openlog('perlbal', 'pid', 'daemon') if $Perlbal::SYSLOG_AVAILABLE;
$Perlbal::syslog_open = 1;
Perlbal::log('info', 'beginning run');
my $pidfile_written = 0;
$pidfile_written = _write_pidfile( $pidfile ) if $pidfile;
Perlbal::initialize();
Danga::Socket->SetLoopTimeout(1000);
Danga::Socket->SetPostLoopCallback(sub {
$Perlbal::tick_time = time();
Perlbal::Socket::run_callbacks();
return 1;
});
# begin the overall loop to try to capture if Perlbal dies at some point
# so we can have a log of it
eval {
# wait for activity
Perlbal::Socket->EventLoop();
};
my $clean_exit = 1;
# closing messages
if ($@) {
Perlbal::log('crit', "crash log: $_") foreach split(/\r?\n/, $@);
$clean_exit = 0;
}
# Note: This will only actually remove the pidfile on 'shutdown graceful'
# A more reliable approach might be to have a pidfile object which fires
# removal on DESTROY.
_remove_pidfile( $pidfile ) if $pidfile_written;
Perlbal::log('info', 'ending run');
$Perlbal::syslog_open = 0;
Sys::Syslog::closelog() if $Perlbal::SYSLOG_AVAILABLE;
return $clean_exit;
}
sub log {
# simple logging functionality
if ($foreground) {
# syslog acts like printf so we have to use printf and append a \n
shift; # ignore the first parameter (info, warn, crit, etc)
my $message = shift;
if (@_) {
printf("$message\n", @_);
} else {
print("$message\n");
}
} else {
# just pass the parameters to syslog
Sys::Syslog::syslog(@_) if $Perlbal::syslog_open;
}
}
sub _write_pidfile {
my $file = shift;
my $fh;
unless (open($fh, ">$file")) {
Perlbal::log('info', "couldn't create pidfile '$file': $!" );
return 0;
}
unless ((print $fh "$$\n") && close($fh)) {
Perlbal::log('info', "couldn't write into pidfile '$file': $!" );
_remove_pidfile($file);
return 0;
}
return 1;
}
sub _remove_pidfile {
my $file = shift;
unlink $file;
return 1;
}
# Local Variables:
# mode: perl
# c-basic-indent: 4
# indent-tabs-mode: nil
# End:
1;
Perlbal-1.80/lib/Perlbal/ 0000755 0001750 0001750 00000000000 11722625030 015340 5 ustar dormando dormando Perlbal-1.80/lib/Perlbal/ClientHTTPBase.pm 0000644 0001750 0001750 00000101662 11630514744 020424 0 ustar dormando dormando ######################################################################
# Common HTTP functionality for ClientProxy and ClientHTTP
# possible states:
# reading_headers (initial state, then follows one of two paths)
# wait_backend, backend_req_sent, wait_res, xfer_res, draining_res
# wait_stat, wait_open, xfer_disk
# both paths can then go into persist_wait, which means they're waiting
# for another request from the user
#
# Copyright 2004, Danga Interactive, Inc.
# Copyright 2005-2007, Six Apart, Ltd.
package Perlbal::ClientHTTPBase;
use strict;
use warnings;
no warnings qw(deprecated);
use Sys::Syscall;
use base "Perlbal::Socket";
use HTTP::Date ();
use fields ('service', # Perlbal::Service object
'replacement_uri', # URI to send instead of the one requested; this is used
# to instruct _serve_request to send an index file instead
# of trying to serve a directory and failing
'scratch', # extra storage; plugins can use it if they want
# reproxy support
'reproxy_file', # filename the backend told us to start opening
'reproxy_file_size', # size of file, once we stat() it
'reproxy_fh', # if needed, IO::Handle of fd
'reproxy_file_offset', # how much we've sent from the file.
'post_sendfile_cb', # subref to run after we're done sendfile'ing the current file
'requests', # number of requests this object has performed for the user
# service selector parent
'selector_svc', # the original service from which we came
'is_ssl', # Is this socket SSL attached (restricted operations)
);
use Fcntl ':mode';
use Errno qw(EPIPE ECONNRESET);
use POSIX ();
# hard-code defaults can be changed with MIME management command
our $MimeType = {qw(
css text/css
doc application/msword
gif image/gif
htm text/html
html text/html
jpg image/jpeg
js application/x-javascript
mp3 audio/mpeg
mpg video/mpeg
pdf application/pdf
png image/png
tif image/tiff
tiff image/tiff
torrent application/x-bittorrent
txt text/plain
zip application/zip
)};
# ClientHTTPBase
sub new {
my Perlbal::ClientHTTPBase $self = shift;
my ($service, $sock, $selector_svc) = @_;
$self = fields::new($self) unless ref $self;
$self->SUPER::new($sock); # init base fields
$self->{service} = $service;
$self->{replacement_uri} = undef;
$self->{headers_string} = '';
$self->{requests} = 0;
$self->{scratch} = {};
$self->{selector_svc} = $selector_svc;
$self->{is_ssl} = 0;
$self->state('reading_headers');
$self->watch_read(1);
return $self;
}
sub new_from_base {
my $class = shift;
my Perlbal::ClientHTTPBase $cb = shift; # base object
Perlbal::Util::rebless($cb, $class);
$cb->handle_request;
return $cb;
}
sub close {
my Perlbal::ClientHTTPBase $self = shift;
# don't close twice
return if $self->{closed};
# could contain a closure with circular reference
$self->{post_sendfile_cb} = undef;
# close the file we were reproxying, if any
CORE::close($self->{reproxy_fh}) if $self->{reproxy_fh};
# now pass up the line
$self->SUPER::close(@_);
}
# given the response headers we just got, and considering our request
# headers, determine if we should be sending keep-alive header
# information back to the client
sub setup_keepalive {
my Perlbal::ClientHTTPBase $self = $_[0];
print "ClientHTTPBase::setup_keepalive($self)\n" if Perlbal::DEBUG >= 2;
# now get the headers we're using
my Perlbal::HTTPHeaders $reshd = $_[1];
my Perlbal::HTTPHeaders $rqhd = $self->{req_headers};
my $override_value = $_[2];
# for now, we enforce outgoing HTTP 1.0
$reshd->set_version("1.0");
# if we came in via a selector service, that's whose settings
# we respect for persist_client
my $svc = $self->{selector_svc} || $self->{service};
my $persist_client = $svc->{persist_client} || 0;
$persist_client = $override_value if defined $override_value;
print " service's persist_client = $persist_client\n" if Perlbal::DEBUG >= 3;
# do keep alive if they sent content-length or it's a head request
my $do_keepalive = $persist_client && $rqhd->req_keep_alive($reshd);
if ($do_keepalive) {
print " doing keep-alive to client\n" if Perlbal::DEBUG >= 3;
my $timeout = $self->{service}->{persist_client_idle_timeout};
$reshd->header('Connection', 'keep-alive');
$reshd->header('Keep-Alive', $timeout ? "timeout=$timeout, max=100" : undef);
} else {
print " doing connection: close\n" if Perlbal::DEBUG >= 3;
# FIXME: we don't necessarily want to set connection to close,
# but really set a space-separated list of tokens which are
# specific to the connection. "close" and "keep-alive" are
# just special.
$reshd->header('Connection', 'close');
$reshd->header('Keep-Alive', undef);
}
}
# overridden here from Perlbal::Socket to use the service value
sub max_idle_time {
my Perlbal::ClientHTTPBase $self = shift;
if ($self->state eq 'persist_wait') {
return $self->{service}->{persist_client_idle_timeout};
} else {
return $self->{service}->{idle_timeout};
}
}
# Called when this client is entering a persist_wait state, but before we are returned to base.
sub persist_wait {
}
# called when we've finished writing everything to a client and we need
# to reset our state for another request. returns 1 to mean that we should
# support persistence, 0 means we're discarding this connection.
sub http_response_sent {
my Perlbal::ClientHTTPBase $self = $_[0];
# close if we're supposed to
if (
! defined $self->{res_headers} ||
! $self->{res_headers}->res_keep_alive($self->{req_headers}) ||
$self->{do_die}
)
{
# do a final read so we don't have unread_data_waiting and RST
# the connection. IE and others send an extra \r\n after POSTs
my $dummy = $self->read(5);
# close if we have no response headers or they say to close
$self->close("no_keep_alive");
return 0;
}
# if they just did a POST, set the flag that says we might expect
# an unadvertised \r\n coming from some browsers. Old Netscape
# 4.x did this on all POSTs, and Firefox/Safari do it on
# XmlHttpRequest POSTs.
if ($self->{req_headers}->request_method eq "POST") {
$self->{ditch_leading_rn} = 1;
}
# now since we're doing persistence, uncork so the last packet goes.
# we will recork when we're processing a new request.
$self->tcp_cork(0);
# reset state
$self->{replacement_uri} = undef;
$self->{headers_string} = '';
$self->{req_headers} = undef;
$self->{res_headers} = undef;
$self->{reproxy_fh} = undef;
$self->{reproxy_file} = undef;
$self->{reproxy_file_size} = 0;
$self->{reproxy_file_offset} = 0;
$self->{read_buf} = [];
$self->{read_ahead} = 0;
$self->{read_size} = 0;
$self->{scratch} = {};
$self->{post_sendfile_cb} = undef;
$self->state('persist_wait');
$self->persist_wait;
if (my $selector_svc = $self->{selector_svc}) {
if (! $selector_svc->run_hook('return_to_base', $self)){
$selector_svc->return_to_base($self);
}
}
# NOTE: because we only speak 1.0 to clients they can't have
# pipeline in a read that we haven't read yet.
$self->watch_read(1);
$self->watch_write(0);
return 1;
}
sub reproxy_fh {
my Perlbal::ClientHTTPBase $self = shift;
# setter
if (@_) {
my ($fh, $size) = @_;
$self->state('xfer_disk');
$self->{reproxy_fh} = $fh;
$self->{reproxy_file_offset} = 0;
$self->{reproxy_file_size} = $size;
my $is_ssl_webserver = ( $self->{service}->{listener}->{sslopts} &&
( $self->{service}->{role} eq 'web_server') );
unless ($is_ssl_webserver) {
# call hook that we're reproxying a file
return $fh if $self->{service}->run_hook("start_send_file", $self);
# turn on writes (the hook might not have wanted us to)
$self->watch_write(1);
return $fh;
} else { # use aio_read for ssl webserver instead of sendfile
print "webserver in ssl mode, sendfile disabled!\n"
if $Perlbal::DEBUG >= 3;
# turn off writes
$self->watch_write(0);
#create filehandle for reading
my $data = '';
Perlbal::AIO::aio_read($self->reproxy_fh, 0, 2048, $data, sub {
# got data? undef is error
return $self->_simple_response(500) unless $_[0] > 0;
# seek into the file now so sendfile starts further in
my $ld = length $data;
sysseek($self->{reproxy_fh}, $ld, &POSIX::SEEK_SET);
$self->{reproxy_file_offset} = $ld;
# reenable writes after we get data
$self->tcp_cork(1); # by setting reproxy_file_offset above,
# it won't cork, so we cork it
$self->write($data);
$self->watch_write(1);
});
return 1;
}
}
return $self->{reproxy_fh};
}
sub event_read {
my Perlbal::ClientHTTPBase $self = shift;
$self->{alive_time} = $Perlbal::tick_time;
# see if we have headers?
die "Shouldn't get here! This is an abstract base class, pretty much, except in the case of the 'selector' role."
if $self->{req_headers};
my $hd = $self->read_request_headers;
$self->handle_request;
}
sub handle_request {
my Perlbal::ClientHTTPBase $self = shift;
my Perlbal::HTTPHeaders $hd = $self->{req_headers};
return unless $hd;
$self->check_req_headers;
return if $self->{service}->run_hook('start_http_request', $self);
# we must stop watching for events now, otherwise if there's
# PUT/POST overflow, it'll be sent to ClientHTTPBase, which can't
# handle it yet. must wait for the selector (which has as much
# time as it wants) to route as to our subclass, which can then
# re-enable reads.
$self->watch_read(0);
my $select = sub {
# now that we have headers, it's time to tell the selector
# plugin that it's time for it to select which real service to
# use
my $selector = $self->{'service'}->selector();
return $self->_simple_response(500, "No service selector configured.")
unless ref $selector eq "CODE";
$selector->($self);
};
my $svc = $self->{'service'};
if ($svc->{latency}) {
Danga::Socket->AddTimer($svc->{latency} / 1000, $select);
} else {
$select->();
}
}
sub reproxy_file_done {
my Perlbal::ClientHTTPBase $self = shift;
return if $self->{service}->run_hook('reproxy_fh_finished', $self);
# close the sendfile fd
CORE::close($self->{reproxy_fh});
$self->{reproxy_fh} = undef;
if (my $cb = $self->{post_sendfile_cb}) {
$cb->();
} else {
$self->http_response_sent;
}
}
# client is ready for more of its file. so sendfile some more to it.
# (called by event_write when we're actually in this mode)
sub event_write_reproxy_fh {
my Perlbal::ClientHTTPBase $self = shift;
my $remain = $self->{reproxy_file_size} - $self->{reproxy_file_offset};
$self->tcp_cork(1) if $self->{reproxy_file_offset} == 0;
$self->watch_write(0);
if ($self->{is_ssl}) { # SSL (sendfile does not do SSL)
return if $self->{closed};
if ($remain <= 0) { #done
print "REPROXY SSL done\n" if Perlbal::DEBUG >= 2;
$self->reproxy_file_done;
return;
}
# queue up next read
Perlbal::AIO::set_file_for_channel($self->{reproxy_file});
my $len = $remain > 4096 ? 4096 : $remain; # buffer size
my $buffer = '';
Perlbal::AIO::aio_read(
$self->{reproxy_fh},
$self->{reproxy_file_offset},
$len,
$buffer,
sub {
return if $self->{closed};
# we have buffer to send
my $rv = $_[0]; # arg is result of sysread
if (!defined($rv) || $rv <= 0) { # read error
# sysseek is called after sysread so $! not valid
$self->close('sysread_error');
print STDERR "Error w/ reproxy sysread\n";
return;
}
$self->{reproxy_file_offset} += $rv;
$self->tcp_cork(1); # by setting reproxy_file_offset above,
# it won't cork, so we cork it
$self->write($buffer); # start socket send
$self->watch_write(1);
}
);
return;
}
# cap at 128k sendfiles
my $to_send = $remain > 128 * 1024 ? 128 * 1024 : $remain;
my $postread = sub {
return if $self->{closed};
my $sent = Perlbal::Socket::sendfile($self->{fd},
fileno($self->{reproxy_fh}),
$to_send);
#warn "to_send = $to_send, sent = $sent\n";
print "REPROXY Sent: $sent\n" if Perlbal::DEBUG >= 2;
if ($sent < 0) {
return $self->close("epipe") if $! == EPIPE;
return $self->close("connreset") if $! == ECONNRESET;
print STDERR "Error w/ sendfile: $!\n";
$self->close('sendfile_error');
return;
}
$self->{reproxy_file_offset} += $sent;
if ($sent >= $remain) {
$self->reproxy_file_done;
} else {
$self->watch_write(1);
}
};
# TODO: way to bypass readahead and go straight to sendfile for common/hot/recent files.
# something like:
# if ($hot) { $postread->(); return ; }
if ($to_send < 0) {
Perlbal::log('warning', "tried to readahead negative bytes. filesize=$self->{reproxy_file_size}, offset=$self->{reproxy_file_offset}");
# this code, doing sendfile, will fail gracefully with return
# code, not 'die', and we'll close with sendfile_error:
$postread->();
return;
}
Perlbal::AIO::set_file_for_channel($self->{reproxy_file});
Perlbal::AIO::aio_readahead($self->{reproxy_fh},
$self->{reproxy_file_offset},
$to_send, $postread);
}
sub event_write {
my Perlbal::ClientHTTPBase $self = shift;
# Any HTTP client is considered alive if it's writable.
# if it's not writable for 30 seconds, we kill it.
# subclasses can decide what's appropriate for timeout.
$self->{alive_time} = $Perlbal::tick_time;
# if we're sending a filehandle, go do some more sendfile:
if ($self->{reproxy_fh}) {
$self->event_write_reproxy_fh;
return;
}
# otherwise just kick-start our write buffer.
if ($self->write(undef)) {
# we've written all data in the queue, so stop waiting for
# write notifications:
print "All writing done to $self\n" if Perlbal::DEBUG >= 2;
$self->watch_write(0);
}
}
# this gets called when a "web" service is serving a file locally.
sub _serve_request {
my Perlbal::ClientHTTPBase $self = shift;
my Perlbal::HTTPHeaders $hd = shift;
my $rm = $hd->request_method;
unless ($rm eq "HEAD" || $rm eq "GET") {
return $self->_simple_response(403, "Unimplemented method");
}
my $uri = Perlbal::Util::durl($self->{replacement_uri} || $hd->request_uri);
my Perlbal::Service $svc = $self->{service};
# start_serve_request hook
return 1 if $self->{service}->run_hook('start_serve_request', $self, \$uri);
# don't allow directory traversal
if ($uri =~ m!/\.\./! || $uri !~ m!^/!) {
return $self->_simple_response(403, "Bogus URL");
}
# double question mark means to serve multiple files, comma separated after the
# questions. the uri part before the question mark is the relative base directory
# TODO: only do this if $uri has ?? and the service also allows it. otherwise
# we don't want to mess with anybody's meaning of '??' on the backend service
return $self->_serve_request_multiple($hd, $uri) if $uri =~ /\?\?/;
# chop off the query string
$uri =~ s/\?.*//;
return $self->_simple_response(500, "Docroot unconfigured")
unless $svc->{docroot};
my $file = $svc->{docroot} . $uri;
# update state, since we're now waiting on stat
$self->state('wait_stat');
Perlbal::AIO::aio_stat($file, sub {
# client's gone anyway
return if $self->{closed};
unless (-e _) {
return if $self->{service}->run_hook('static_get_poststat_file_missing', $self);
return $self->_simple_response(404);
}
my $mtime = (stat(_))[9];
my $lastmod = HTTP::Date::time2str($mtime);
my $ims = $hd->header("If-Modified-Since") || "";
# IE sends a request header like "If-Modified-Since: ; length="
# so we have to remove the length bit before comparing it with our date.
# then we save the length to compare later.
my $ims_len;
if ($ims && $ims =~ s/; length=(\d+)//) {
$ims_len = $1;
}
my $not_mod = $ims eq $lastmod && -f _;
my $res;
my $not_satisfiable = 0;
my $size = -s _ if -f _;
# extra protection for IE, since it's offering the info anyway. (see above)
$not_mod = 0 if $ims_len && $ims_len != $size;
my ($status, $range_start, $range_end) = $hd->range($size);
if ($not_mod) {
$res = $self->{res_headers} = Perlbal::HTTPHeaders->new_response(304);
} elsif ($status == 416) {
$res = $self->{res_headers} = Perlbal::HTTPHeaders->new_response(416);
$res->header("Content-Range", $size ? "bytes */$size" : "*");
$res->header("Content-Length", 0);
$not_satisfiable = 1;
} elsif ($status == 206) {
# partial content
$res = $self->{res_headers} = Perlbal::HTTPHeaders->new_response(206);
} else {
return if $self->{service}->run_hook('static_get_poststat_pre_send', $self, $mtime);
$res = $self->{res_headers} = Perlbal::HTTPHeaders->new_response(200);
}
# now set whether this is keep-alive or not
$res->header("Date", HTTP::Date::time2str());
$res->header("Server", "Perlbal") if $self->{service}{server_tokens};
$res->header("Last-Modified", $lastmod);
if (-f _) {
# advertise that we support byte range requests
$res->header("Accept-Ranges", "bytes");
unless ($not_mod || $not_satisfiable) {
my ($ext) = ($file =~ /\.(\w+)$/);
$res->header("Content-Type",
(defined $ext && exists $MimeType->{$ext}) ? $MimeType->{$ext} : "text/plain");
unless ($status == 206) {
$res->header("Content-Length", $size);
} else {
$res->header("Content-Range", "bytes $range_start-$range_end/$size");
$res->header("Content-Length", $range_end - $range_start + 1);
}
}
# has to happen after content-length is set to work:
$self->setup_keepalive($res);
return if $self->{service}->run_hook('modify_response_headers', $self);
if ($rm eq "HEAD" || $not_mod || $not_satisfiable) {
# we can return already, since we know the size
$self->tcp_cork(1);
$self->state('xfer_resp');
$self->write($res->to_string_ref);
$self->write(sub { $self->http_response_sent; });
return;
}
# state update
$self->state('wait_open');
Perlbal::AIO::aio_open($file, 0, 0, sub {
my $rp_fh = shift;
# if client's gone, just close filehandle and abort
if ($self->{closed}) {
CORE::close($rp_fh) if $rp_fh;
return;
}
# handle errors
if (! $rp_fh) {
# couldn't open the file we had already successfully stat'ed.
# FIXME: do 500 vs. 404 vs whatever based on $!
return $self->close('aio_open_failure');
}
$self->state('xfer_disk');
$self->tcp_cork(1); # cork writes to self
$self->write($res->to_string_ref);
# seek if partial content
if ($status == 206) {
sysseek($rp_fh, $range_start, &POSIX::SEEK_SET);
$size = $range_end - $range_start + 1;
}
$self->{reproxy_file} = $file;
$self->reproxy_fh($rp_fh, $size);
});
} elsif (-d _) {
$self->try_index_files($hd, $res, $uri);
}
});
}
sub _serve_request_multiple {
my Perlbal::ClientHTTPBase $self = shift;
my ($hd, $uri) = @_;
my @multiple_files;
my %statinfo; # file -> [ stat fields ]
# double question mark means to serve multiple files, comma
# separated after the questions. the uri part before the question
# mark is the relative base directory
my ($base, $list) = ($uri =~ /(.+)\?\?(.+)/);
unless ($base =~ m!/$!) {
return $self->_simple_response(500, "Base directory (before ??) must end in slash.")
}
# and remove any trailing ?.+ on the list, so you can do things like cache busting
# with a ?v= at the end of a list of files.
$list =~ s/\?.+//;
my Perlbal::Service $svc = $self->{service};
return $self->_simple_response(500, "Docroot unconfigured")
unless $svc->{docroot};
@multiple_files = split(/,/, $list);
return $self->_simple_response(403, "Multiple file serving isn't enabled") unless $svc->{enable_concatenate_get};
return $self->_simple_response(403, "Too many files requested") if @multiple_files > 100;
return $self->_simple_response(403, "Bogus filenames") if grep { m!(?:\A|/)\.\./! } @multiple_files;
my $remain = @multiple_files + 1; # 1 for the base directory
my $dirbase = $svc->{docroot} . $base;
foreach my $file ('', @multiple_files) {
Perlbal::AIO::aio_stat("$dirbase$file", sub {
$remain--;
$statinfo{$file} = $! ? [] : [ stat(_) ];
return if $remain || $self->{closed};
$self->_serve_request_multiple_poststat($hd, $dirbase, \@multiple_files, \%statinfo);
});
}
}
sub _serve_request_multiple_poststat {
my Perlbal::ClientHTTPBase $self = shift;
my ($hd, $basedir, $filelist, $stats) = @_;
# base directory must be a directory
unless (S_ISDIR($stats->{''}[2] || 0)) {
return $self->_simple_response(404, "Base directory not a directory");
}
# files must all exist
my $sum_length = 0;
my $most_recent_mod = 0;
my $mime; # undef until set, or defaults to text/plain later
foreach my $f (@$filelist) {
my $stat = $stats->{$f};
unless (S_ISREG($stat->[2] || 0)) {
return if $self->{service}->run_hook('concat_get_poststat_file_missing', $self);
return $self->_simple_response(404, "One or more file does not exist");
}
if (!$mime && $f =~ /\.(\w+)$/ && $MimeType->{$1}) {
$mime = $MimeType->{$1};
}
$sum_length += $stat->[7];
$most_recent_mod = $stat->[9] if
$stat->[9] >$most_recent_mod;
}
$mime ||= 'text/plain';
my $lastmod = HTTP::Date::time2str($most_recent_mod);
my $ims = $hd->header("If-Modified-Since") || "";
# IE sends a request header like "If-Modified-Since: ; length="
# so we have to remove the length bit before comparing it with our date.
# then we save the length to compare later.
my $ims_len;
if ($ims && $ims =~ s/; length=(\d+)//) {
$ims_len = $1;
}
# What is -f _ doing here? don't we detect the existence of all files above in the loop?
my $not_mod = $ims eq $lastmod && -f _;
my $res;
if ($not_mod) {
$res = $self->{res_headers} = Perlbal::HTTPHeaders->new_response(304);
} else {
return if $self->{service}->run_hook('concat_get_poststat_pre_send', $self, $most_recent_mod);
$res = $self->{res_headers} = Perlbal::HTTPHeaders->new_response(200);
$res->header("Content-Length", $sum_length);
}
$res->header("Date", HTTP::Date::time2str());
$res->header("Server", "Perlbal") if $self->{service}{server_tokens};
$res->header("Last-Modified", $lastmod);
$res->header("Content-Type", $mime);
# has to happen after content-length is set to work:
$self->setup_keepalive($res);
return if $self->{service}->run_hook('modify_response_headers', $self);
if ($hd->request_method eq "HEAD" || $not_mod) {
# we can return already, since we know the size
$self->tcp_cork(1);
$self->state('xfer_resp');
$self->write($res->to_string_ref);
$self->write(sub { $self->http_response_sent; });
return;
}
$self->tcp_cork(1); # cork writes to self
$self->write($res->to_string_ref);
$self->state('wait_open');
# gotta send all files, one by one...
my @remain = @$filelist;
$self->{post_sendfile_cb} = sub {
unless (@remain) {
$self->write(sub { $self->http_response_sent; });
return;
}
my $file = shift @remain;
my $fullfile = "$basedir$file";
my $size = $stats->{$file}[7];
Perlbal::AIO::aio_open($fullfile, 0, 0, sub {
my $rp_fh = shift;
# if client's gone, just close filehandle and abort
if ($self->{closed}) {
CORE::close($rp_fh) if $rp_fh;
return;
}
# handle errors
if (! $rp_fh) {
# couldn't open the file we had already successfully stat'ed.
# FIXME: do 500 vs. 404 vs whatever based on $!
return $self->close('aio_open_failure');
}
$self->{reproxy_file} = $file;
$self->reproxy_fh($rp_fh, $size);
});
};
$self->{post_sendfile_cb}->();
}
sub check_req_headers {
my Perlbal::ClientHTTPBase $self = shift;
my Perlbal::HTTPHeaders $hds = $self->{req_headers};
if ($self->{service}->trusted_ip($self->peer_ip_string)) {
my @ips = split /,\s*/, ($hds->header("X-Forwarded-For") || '');
# This list may be empty, and that's OK, in that case we should unset the
# observed_ip_string, so no matter what we'll use the 0th element, whether
# it happens to be an ip string, or undef.
$self->observed_ip_string($ips[0]);
}
return;
}
sub try_index_files {
my Perlbal::ClientHTTPBase $self = shift;
my ($hd, $res, $uri, $filepos) = @_;
# make sure this starts at 0 initially, and fail if it's past the end
$filepos ||= 0;
if ($filepos >= scalar(@{$self->{service}->{index_files} || []})) {
unless ($self->{service}->{dirindexing}) {
# just inform them that listing is disabled
$self->_simple_response(200, "Directory listing disabled");
return;
}
# ensure uri has one and only one trailing slash for better URLs
$uri =~ s!/*$!/!;
# open the directory and create an index
my $body = "";
my $file = $self->{service}->{docroot} . $uri;
$res->header("Content-Type", "text/html");
opendir(D, $file);
foreach my $de (sort readdir(D)) {
if (-d "$file/$de") {
$body .= "$de
\n";
} else {
$body .= "$de
\n";
}
}
closedir(D);
$body .= "";
$res->header("Content-Length", length($body));
$self->setup_keepalive($res);
$self->state('xfer_resp');
$self->tcp_cork(1); # cork writes to self
$self->write($res->to_string_ref);
$self->write(\$body);
$self->write(sub { $self->http_response_sent; });
return;
}
# construct the file path we need to check
my $file = $self->{service}->{index_files}->[$filepos];
my $fullpath = $self->{service}->{docroot} . $uri . '/' . $file;
# now see if it exists
Perlbal::AIO::aio_stat($fullpath, sub {
return if $self->{closed};
return $self->try_index_files($hd, $res, $uri, $filepos + 1) unless -f _;
# at this point the file exists, so we just want to serve it
$self->{replacement_uri} = $uri . '/' . $file;
return $self->_serve_request($hd);
});
}
sub _simple_response {
my Perlbal::ClientHTTPBase $self = shift;
my ($code, $msg) = @_; # or bodyref
my $res = $self->{res_headers} = Perlbal::HTTPHeaders->new_response($code);
my $body;
if ($code != 204 && $code != 304) {
$res->header("Content-Type", "text/html");
my $en = $res->http_code_english;
$body = "$code" . ($en ? " - $en" : "") . "
\n";
$body .= $msg if $msg;
$res->header('Content-Length', length($body));
}
$res->header('Server', 'Perlbal') if $self->{service}{server_tokens};
$self->setup_keepalive($res);
$self->state('xfer_resp');
$self->tcp_cork(1); # cork writes to self
$self->write($res->to_string_ref);
if (defined $body) {
unless ($self->{req_headers} && $self->{req_headers}->request_method eq 'HEAD') {
# don't write body for head requests
$self->write(\$body);
}
}
$self->write(sub { $self->http_response_sent; });
return 1;
}
sub send_response {
my Perlbal::ClientHTTPBase $self = shift;
$self->watch_read(0);
$self->watch_write(1);
return $self->_simple_response(@_);
}
sub send_full_response {
my Perlbal::ClientHTTPBase $self = shift;
my $code = shift;
my $headers = shift || [];
my $bref = ref($_[0]) eq 'SCALAR' ? shift : \shift;
my $options = shift || {};
my $res = $self->{res_headers} = Perlbal::HTTPHeaders->new_response($code);
while (@$headers) {
my ($name, $value) = splice @$headers, 0, 2;
$res->header($name, $value);
}
if ($code == 204 || $code == 304) {
$res->header('Content-Length', undef);
$bref = \undef;
} elsif (defined $$bref) {
$res->header('Content-Length', length($$bref));
}
$res->header('Server', 'Perlbal') if $self->{service}{server_tokens};
# $res->header('Date', # We should do this
$self->setup_keepalive($res, $options->{persist_client});
$self->state('xfer_resp');
$self->tcp_cork(1); # cork writes to self
$self->write($res->to_string_ref);
if (defined $$bref && $self->{req_headers} && $self->{req_headers}->request_method ne 'HEAD') {
# don't write body for head requests
$self->write($bref);
}
$self->write(sub { $self->http_response_sent; });
return 1;
}
# method that sends a 500 to the user but logs it and any extra information
# we have about the error in question
sub system_error {
my Perlbal::ClientHTTPBase $self = shift;
my ($msg, $info) = @_;
# log to syslog
Perlbal::log('warning', "system error: $msg ($info)");
# and return a 500
return $self->send_response(500, $msg);
}
sub event_err { my $self = shift; $self->close('error'); }
sub event_hup { my $self = shift; $self->close('hup'); }
sub _sock_port {
my $name = $_[0];
my $port = eval { (Socket::sockaddr_in($name))[0] };
return $port unless $@;
# fallback to IPv6:
return (Socket6::unpack_sockaddr_in($name))[0];
}
sub as_string {
my Perlbal::ClientHTTPBase $self = shift;
my $ret = $self->SUPER::as_string;
my $name = $self->{sock} ? getsockname($self->{sock}) : undef;
my $lport = $name ? _sock_port($name) : undef;
my $observed = $self->observed_ip_string;
$ret .= ": localport=$lport" if $lport;
$ret .= "; observed_ip=$observed" if defined $observed;
$ret .= "; reqs=$self->{requests}";
$ret .= "; $self->{state}";
my $hd = $self->{req_headers};
if (defined $hd) {
my $host = $hd->header('Host') || 'unknown';
$ret .= "; http://$host" . $hd->request_uri;
}
return $ret;
}
1;
# Local Variables:
# mode: perl
# c-basic-indent: 4
# indent-tabs-mode: nil
# End:
Perlbal-1.80/lib/Perlbal/ChunkedUploadState.pm 0000644 0001750 0001750 00000003247 11503530123 021426 0 ustar dormando dormando package Perlbal::ChunkedUploadState;
use strict;
sub new {
my ($pkg, %args) = @_;
my $self = bless {
'buf' => '',
'bytes_remain' => 0, # remaining in chunk (ignoring final 2 byte CRLF)
}, $pkg;
foreach my $k (qw(on_new_chunk on_disconnect on_zero_chunk)) {
$self->{$k} = (delete $args{$k}) || sub {};
}
die "bogus args" if %args;
return $self;
}
sub on_readable {
my ($self, $ds) = @_;
my $rbuf = $ds->read(131072);
unless (defined $rbuf) {
$self->{on_disconnect}->();
return;
}
$self->{buf} .= $$rbuf;
while ($self->drive_machine) {}
}
# returns 1 if progress was made parsing buffer
sub drive_machine {
my $self = shift;
my $buflen = length($self->{buf});
return 0 unless $buflen;
if (my $br = $self->{bytes_remain}) {
my $extract = $buflen > $br ? $br : $buflen;
my $ch = substr($self->{buf}, 0, $extract, '');
$self->{bytes_remain} -= $extract;
die "assert" if $self->{bytes_remain} < 0;
$self->{on_new_chunk}->(\$ch);
return 1;
}
return 0 unless $self->{buf} =~ s/^(?:\r\n)?([0-9a-fA-F]+)(?:;.*)?\r\n//;
$self->{bytes_remain} = hex($1);
if ($self->{bytes_remain} == 0) {
# FIXME: new state machine state for trailer parsing/discarding.
# (before we do on_zero_chunk). for now, though, just assume
# no trailers and throw away the extra post-trailer \r\n that
# is probably in this packet. hacky.
$self->{buf} =~ s/^\r\n//;
$self->{hit_zero} = 1;
$self->{on_zero_chunk}->();
return 0;
}
return 1;
}
sub hit_zero_chunk { $_[0]{hit_zero} }
1;
Perlbal-1.80/lib/Perlbal/ManageCommand.pm 0000644 0001750 0001750 00000004503 11503530123 020362 0 ustar dormando dormando # class representing a one-liner management command. all the responses
# to a command should be done through this instance (out, err, ok, etc)
#
# Copyright 2005-2007, Six Apart, Ltd.
#
package Perlbal::ManageCommand;
use strict;
use warnings;
no warnings qw(deprecated);
use fields (
'base', # the base command name (like "proc")
'cmd',
'ok',
'err',
'out',
'orig',
'argn',
'ctx',
);
sub new {
my ($class, $base, $cmd, $out, $ok, $err, $orig, $ctx) = @_;
my $self = fields::new($class);
$self->{base} = $base;
$self->{cmd} = $cmd;
$self->{ok} = $ok;
$self->{err} = $err;
$self->{out} = $out;
$self->{orig} = $orig;
$self->{ctx} = $ctx;
$self->{argn} = [];
return $self;
}
# returns an managecommand object for functions that need one, but
# this does nothing but explode if there any problems.
sub loud_crasher {
use Carp qw(confess);
__PACKAGE__->new(undef, undef, sub {}, sub {}, sub { confess "MC:err: @_" }, "", Perlbal::CommandContext->new);
}
sub out { my $mc = shift; return @_ ? $mc->{out}->(@_) : $mc->{out}; }
sub ok { my $mc = shift; return $mc->{ok}->(@_); }
sub err {
my ($mc, $err) = @_;
$err =~ s/\n$//;
$mc->{err}->($err);
}
sub cmd { my $mc = shift; return $mc->{cmd}; }
sub orig { my $mc = shift; return $mc->{orig}; }
sub end { my $mc = shift; $mc->{out}->("."); 1; }
sub parse {
my $mc = shift;
my $regexp = shift;
my $usage = shift;
my @ret = ($mc->{cmd} =~ /$regexp/);
$mc->parse_error($usage) unless @ret;
my $i = 0;
foreach (@ret) {
$mc->{argn}[$i++] = $_;
}
return $mc;
}
sub arg {
my $mc = shift;
my $n = shift; # 1-based array, to correspond with $1, $2, $3
return $mc->{argn}[$n - 1];
}
sub args {
my $mc = shift;
return @{$mc->{argn}};
}
sub parse_error {
my $mc = shift;
my $usage = shift;
$usage .= "\n" if $usage && $usage !~ /\n$/;
die $usage || "Invalid syntax to '$mc->{base}' command\n"
}
sub no_opts {
my $mc = shift;
die "The '$mc->{base}' command takes no arguments\n"
unless $mc->{cmd} eq $mc->{base};
return $mc;
}
1;
# Local Variables:
# mode: perl
# c-basic-indent: 4
# indent-tabs-mode: nil
# End:
Perlbal-1.80/lib/Perlbal/Util.pm 0000644 0001750 0001750 00000002232 11503530123 016605 0 ustar dormando dormando # misc util functions
#
package Perlbal::Util;
use strict;
use warnings;
no warnings qw(deprecated);
sub durl {
my ($txt) = @_;
$txt =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
return $txt;
}
=head2 C< rebless >
Safely re-bless a locked (use fields) hash into another package. Note
that for our convenience elsewhere the set of allowable keys for the
re-blessed hash will be the union of the keys allowed by its old package
and those allowed for the package into which it is blessed.
=cut
BEGIN {
if ($] >= 5.010) {
eval q{
use Hash::Util qw(legal_ref_keys unlock_ref_keys lock_ref_keys)
};
*rebless = sub {
my ($obj, $pkg) = @_;
my @keys = legal_ref_keys($obj);
unlock_ref_keys($obj);
bless $obj, $pkg;
lock_ref_keys($obj, @keys,
legal_ref_keys(fields::new($pkg)));
return $obj;
};
}
else {
*rebless = sub {
my ($obj, $pkg) = @_;
return bless $obj, $pkg;
};
}
}
1;
# Local Variables:
# mode: perl
# c-basic-indent: 4
# indent-tabs-mode: nil
# End:
Perlbal-1.80/lib/Perlbal/UploadListener.pm 0000644 0001750 0001750 00000005134 11503530123 020626 0 ustar dormando dormando ######################################################################
# Listen for UDP upload status packets
#
# Copyright 2005-2007, Six Apart, Ltd.
package Perlbal::UploadListener;
use strict;
use warnings;
no warnings qw(deprecated);
use base "Perlbal::Socket";
use fields qw(service hostport);
# TCPListener
sub new {
my ($class, $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;
my $self = fields::new($class);
$self->SUPER::new($sock);
$self->{service} = $service;
$self->{hostport} = $hostport;
$self->watch_read(1);
return $self;
}
my %status;
my @todelete;
sub get_status {
my $ses = shift;
return $status{$ses};
}
# TCPListener: accepts a new client connection
sub event_read {
my Perlbal::TCPListener $self = shift;
my $buf;
$self->{sock}->recv($buf, 500);
return unless $buf =~ /^UPLOAD:(\w{5,50}):(\d+):(\d+):(\d+):(\d+)$/;
my ($ses, $done, $total, $starttime, $nowtime) = ($1, $2, $3, $4, $5);
my $now = time();
$status{$ses} = {
done => $done,
total => $total,
starttime => $starttime,
lasttouch => $now,
};
# keep a history of touched records, then we'll clean 'em
# after 30 seconds.
push @todelete, [$now, $ses];
my $too_old = $now - 4;
while (@todelete && $todelete[0][0] < $too_old) {
my $rec = shift @todelete;
my $to_kill = $rec->[1];
if (my $krec = $status{$to_kill}) {
my $last_touch = $krec->{lasttouch};
delete $status{$to_kill} if $last_touch < $too_old;
}
}
}
sub as_string {
my Perlbal::TCPListener $self = shift;
my $ret = $self->SUPER::as_string;
my Perlbal::Service $svc = $self->{service};
$ret .= ": listening on $self->{hostport} for service '$svc->{name}'";
return $ret;
}
sub as_string_html {
my Perlbal::TCPListener $self = shift;
my $ret = $self->SUPER::as_string_html;
my Perlbal::Service $svc = $self->{service};
$ret .= ": listening on $self->{hostport} for service $svc->{name}";
return $ret;
}
sub die_gracefully {
# die off so we stop waiting for new connections
my $self = shift;
$self->close('graceful_death');
}
1;
# Local Variables:
# mode: perl
# c-basic-indent: 4
# indent-tabs-mode: nil
# End:
Perlbal-1.80/lib/Perlbal/Manual/ 0000755 0001750 0001750 00000000000 11722625030 016555 5 ustar dormando dormando Perlbal-1.80/lib/Perlbal/Manual/LoadBalancer.pod 0000644 0001750 0001750 00000010452 11516736504 021604 0 ustar dormando dormando =head1 NAME
Perlbal::Manual::LoadBalancer - Using Perlbal as a Load Balancer
=head2 VERSION
Perlbal 1.78.
=head2 DESCRIPTION
How to configure a Perlbal Load Balancing service.
=head2 READ ME FIRST
Please read L first for a better explanation on how to configure Perlbal. This document will make much more sense after reading that.
=head2 Using Perlbal as a Load Balancer
For a better understanding of how to set up Perbal as a Load Balancer, it should be noted that a Load Balancer and a Reverse Proxy can often be the same thing; not always, but often.
A Load Balancer is a server (or device) that balances requests across a number of servers to spread the load. A Reverse Proxy can still do this but also have a number of other features.
Perlbal as a Reverse Proxy provides features such as buffering content, preserving connections to the backend servers, starting connections ahead of time and a high priority queue, among others.
You could almost say that a Load Balancer is a subset of a Reverse Proxy (it's not, but you could).
When it comes to Perlbal, the Load Balancer is implemented as a Reverse Proxy without all the extra options, and that's why you set the role of a Load Balancer to C:
SET role = reverse_proxy
=head3 Simple load balancing
Let's assume you want to configure two machines to serve your website and you want to let Perlbal decide how to balance the requests. For the sake of this exercise let's assume you have two servers at:
10.0.0.1:80
10.0.0.2:80
And now you want to use these two machines to serve your website at:
10.0.0.3:80
Here's a sample configuration to make this happen:
CREATE POOL mywebsite
POOL mywebsite ADD 10.0.0.1:80
POOL mywebsite ADD 10.0.0.2:80
CREATE SERVICE service_mywebsite
SET role = reverse_proxy
SET pool = mywebsite
SET listen = 10.0.0.3:80
ENABLE service_mywebsite
The first line defines a pool of machines called C. The second and third lines add your two machines to that pool (note that the indentation is not mandatory).
After that you define a service called C with the role C set to listen on C<10.0.0.3:80> and using the pool C to serve the requests.
The last line is what allows you have several services configured in a file even if they are not currently active (a common scenario is to configure everything on the file and then enable/disable services on-the-fly as required; see L for more information on this process).
=head3 The Load Balancing algorithm
Perlbal uses a highly efficient load balancing algorithm. It is very effective for distributing dynamic web requests among potentially heterogeneous hardware.
First, backend servers must have their MaxClients (for apache, or equivalent) setting tuned to a reasonable limit. If your hardware can run 20 requests in parallel before running out of CPU, set MaxClients to 20.
Next, by default Perlbal will distribute requests randomly. Opening a new connection to any available backend, and issuing the request.
The proper algorithm is able to be used if C, C, C, and C are enabled.
SET persist_backend = on
SET verify_backend = on
SET backend_persist_cache = 5
SET connect_ahead = 2
In this configuration, Perlbal will only route client requests to backends that it knows are real processes, instead of the OS listen queue. It will attempt to reuse pre-verified backends, and will attempt to create slightly more idle connections than it needs in preparation of future requests.
When you put all this together, it becomes less likely that a client will wait for Perlbal to find an available backend. By setting your MaxClients properly, backends are able to serve traffic without getting overwhelmed. If no backends are available, Perlbal will queue them internally, rather than overload backends.
You would want to disable C if you are balancing across image servers, or other extremely lightweight requests.
=head2 SEE ALSO
L,
L,
L,
L.
Perlbal-1.80/lib/Perlbal/Manual/ReverseProxy.pod 0000644 0001750 0001750 00000033646 11630514744 021761 0 ustar dormando dormando =head1 NAME
Perlbal::Manual::ReverseProxy - Configuring Perlbal as a Reverse Proxy
=head2 VERSION
Perlbal 1.78.
=head2 DESCRIPTION
How to configure a Perlbal Reverse Proxy service.
=head2 READ ME FIRST
Please read L first for a better explanation on how to configure Perlbal. This document will make much more sense after reading that.
=head2 Configuring Perlbal as a Reverse Proxy
Configuration of Perlbal as a Reverse Proxy is similar to configuration as a Load Balancer.
Check L under C for a sample configuration file and for a brief explanation of the differences between a Load Balancer and a Reverse Proxy.
=head2 Parameters
You can set parameters via commands of either forms:
SET =
SET =
=over 8
=item B = bool
Whether to trust all incoming requests' X-Forwarded-For and related headers. Set to true only if you know that all incoming requests from your own proxy servers that clean/set those headers.
Default is false.
=item B = int
The number of backend connections to keep alive on reserve while there are no clients.
Default is 2.
=item B = bool
Flag to disable any modification of X-Forwarded-For, X-Host, and X-Forwarded-Host headers.
Default is false.
=item B = size
How much content-body (POST/PUT/etc) data we read from a client before we start sending it to a backend web node. If C is enabled, this value is used to determine how many bytes are read before Perlbal makes a determination on whether or not to spool the upload to disk.
Default is 100k.
=item B = size
How much ahead of a client we'll get while copying from a backend to a client. If a client gets behind this much, we stop reading from the backend for a bit. Once all remaining data fits in the buffer, the backend is released and may be reused.
Default is 256k.
=item B = size
How much ahead of a client we'll get while copying from a reproxied URL to a client. If a client gets behind this much, we stop reading from the reproxied URL for a bit. The default is lower than the regular C (50k instead of 256k) because it's assumed that you're only reproxying to large files on event-based webservers, which are less sensitive to many open connections, whereas the 256k buffer size is good for keeping heavy process-based free of slow clients.
Default if 50k.
=item B = int
If an upload is coming in at a rate less than this value in bytes per second, it will be buffered to disk. A value of 0 means the rate will not be checked.
Default is 0.
=item B = size
If an upload is larger than this size in bytes, it will be buffered to disk. A value of 0 means the size will not be checked.
Default is 250k.
=item B = int
If an upload is estimated to take more than this number of seconds, it will be buffered to disk. A value of 0 means the time will not be estimated.
Default is 5.
=item B = bool
Used to enable or disable the buffer uploads to disk system. If enabled, C bytes worth of the upload will be stored in memory. At that point, the buffer upload thresholds will be checked to see if we should just send this upload to the backend or if we should spool it to the disk.
Default if false.
=item B = path/to/directory
Directory root for storing files used to buffer uploads.
=item B = I
How large to set the client's socket SNDBUF.
Default is 0.
=item B = int
How many extra backend connections we keep alive in addition to the current ones, in anticipation of new client connections.
Default is 0.
=item B = bool
Whether Perlbal should transparently retry requests to backends if a backend returns a 500 server error.
Default is false.
=item B = bool
Enable 'reproxying' (end-user-transparent internal redirects) to either local files or other URLs. When enabled, the backend servers in the pool that this service is configured for will have access to tell this Perlbal instance to serve any local readable file, or connect to any other URL that this Perlbal can connect to. Only enable this if you trust the backend web nodes.
Default is false.
See the section C in this document for more information.
=item B = string of comma-separated seconds (full or partial)
String of comma-separated seconds (full or partial) to delay between retries. For example "0,2" would mean do at most two retries, the first zero seconds after the first failure, and the second 2 seconds after the second failure. You probably don't need to modify the default value.
Default it 0,0.25,0.50,1,1,1,1,1
=item B = bool
Enable SSL to the client.
Default is false.
=item B = cookie_name
The cookie name to inspect to determine if the client goes onto the high-priority queue.
See L for more information.
=item B = string
A string that the C must contain to go onto the high-priority queue.
See L for more information.
=item B = int
Timeout in seconds for idle connections to the end user. It's also the limit for how long a backend may take to respond or transfer data.
Default is 30.
=item B = ip:port
The ip:port to listen on. For a service to work, you must either make it listen, or make another selector service map to a non-listening service.
=item B = int
The maximum number of requests to be made on a single persistent backend connection before releasing the connection.
A value of 0 means there is no limit and the connection will only be discarded once the backend asks it to be or when Perlbal is sufficiently idle.
Default is 0.
=item B = size
The maximum size that will be accepted for a chunked request (which is written to disk, buffered uploads must be on). A value of 0 means no limit.
Default is 209715200 (200MB).
=item B = bool
Whether to enable HTTP keep-alives to the end user.
Default is false.
=item B = bool
Whether to enable HTTP keep-alives to the backend webnodes.
Default is false, but setting it to true is highly recommended if Perlbal is the only client to your backends. If not, beware that Perlbal will hog the connections, starving other clients.
=item B = int
Timeout in seconds for HTTP persist_client_idle_timeout keep-alives to the end user.
Default is 30.
=item B = int (B)
Set both the persist_client_timeout persist_client_idle_timeout and idle_timeout.
Deprecated.
=item B
Name of previously-created pool object containing the backend nodes that this reverse proxy sends requests to.
=item B = int:0-100
Chance (percentage) to take a standard priority request when we're in pressure relief mode.
Default is 0.
See L for more information.
=item B = int
Number of outstanding standard priority connections to activate pressure relief at.
A value of 0 disables the high priority queue system entirely.
Default is 0.
See L for more information.
=item B = int
Set the maximum number of cached reproxy results (X-REPROXY-CACHE-FOR) that may be kept in the service cache. These cached requests take up about 1.25KB of RAM each (on Linux x86), but will vary with usage. Perlbal still starts with 0 in the cache and will grow over time. Be careful when adjusting this and watch your RAM usage like a hawk.
Default is 0, which means cache is disabled.
=item B = reverse_proxy|web_server|management|selector
What type of service. One of 'reverse_proxy' for a service that load balances to a pool of backend webserver nodes, 'web_server' for a typical webserver', 'management' for a Perlbal management interface (speaks both command-line or HTTP, auto-detected), or 'selector', for a virtual service that maps onto other services.
=item B
Executable which will be the HTTP server on stdin/stdout. (B, B)
=item B = path/to/file
Path to certificate PEM file for SSL.
Default is C.
=item B = cipher list
OpenSSL-style cipher list.
Default is C.
=item B = path/to/file
Path to private key PEM file for SSL.
Default is C.
=item B = Net::Netmask filter
A comma separated list of L filters (e.g. 10.0.0.0/24, see L) that determines whether upstream clients are trusted or not, where trusted means their X-Forwarded-For/etc headers are not munged.
=item B = comma separated list of hosts
Comma separated list of hosts in form 'a.b.c.d:port' which will receive UDP upload status packets no faster than once a second per HTTP request (PUT/POST) from clients that have requested an upload status bar, which they request by appending the URL get argument ?client_up_sess=[xxxxx] where xxxxx is 5-50 'word' characters (a-z, A-Z, 0-9, underscore).
=item B = bool
Whether Perlbal should send a quick OPTIONS request to the backends before sending an actual client request to them. If your backend is Apache or some other process-based webserver, this is highly recommended. All too often a loaded backend box will reply to new TCP connections, but it's the kernel's TCP stack Perlbal is talking to, not and actual Apache process yet. Using this option reduces end-user latency a ton on loaded sites.
Default if false.
=item B = path
What path the OPTIONS request sent by C should use.
Default is C<*>.
=item B = bool
Whether to provide a "Server" header.
Perlbal by default adds a header to all replies (such as the web_server role). By setting this default to "off", you can prevent Perlbal from identifying itself.
Default is C.
=back
=head2 More on Parameters
=head3 backend_persist_cache vs. connect_ahead
The C parameter refers to connections kept alive after being used, while C refers to connections opened in anticipation.
For instance:
SET backend_persist_cache = 2
SET connect_ahead = 1
Let's assume, for simplification purposes, that your service only has one server. Here's an example of what could happen:
=over 4
=item * Perlbal starts
No connections open until the very first request comes in (this may change in the future).
=item * one requests arrives
This request starts being served on the open connection; Perlbal opens another connection because C's value tells it to always open one in anticipation.
=item * a second request arrives
(the first request hasn't concluded yet)
The second connection is used, a third one is created so we still have one in anticipation.
=item * the first request finishes
The connection is kept open; this means we now have three open connections: two being used and one free (the first and the third one are free).
=item * the second request finishes
The connection is killed, as we already have two other open connections (the first and the third), and that's the number set by C for the number of connections to be kept alive.
=back
=head3 Reproxying
Perlbal supports the concept of reproxying. Basically, this gives it the ability to ask a backend node for a file and get back a specific header that says "this file is really over there, get it there." Perlbal will then load that file or URL and send it to the user transparently, without them ever knowing that they got reproxied to another location.
Add the following line to your F to enable reproxying on a per service basis ( reproxying is disabled by default in >= 1.38 ):
SET enable_reproxy = true
This can be useful for having URLs that get mapped to files on disk without giving users enough information to map out your directory structure. For example, you can create a file structure such as:
/home/pics/$userid/$pic
Then you can have URLs such as:
http://foo.com/mysite/users/$userid/picture/$pic
When this URL gets passed to the backend web node, it could return a simple response that includes this header:
X-REPROXY-FILE: /home/pics/$userid/$pic
Perlbal will then use asynchronous IO to send the file to the user without slowing down Perlbal at all.
This support also extends to URLs that can be located anywhere Perlbal has access to. It's the same syntax, nearly:
X-REPROXY-URL: http://foo.com:80/resource.html
You can also specify multiple URLs:
X-REPROXY-URL: http://foo.com:80/resource.html http://baz.com:8080/res.htm
Just specify any number of space separated URLs. Perlbal will request them one by one until one returns a response code of 200. At that point Perlbal will proxy the response back to the user just like normal.
Note that the user's headers are NOT passed through to the web server. To the target server, it looks simply like Perlbal is requesting the resource for itself. This behavior may change at some point.
One final note: the server that returns the reproxy header can also return a C header. If present, Perlbal will consider a reproxy a failure if the file returned by the target system is of a different size than what the expected size header says. On failure, Perlbal tries the next URI in the list. If it's a file being reproxied, a 404 is returned if the file size is different.
=head2 SEE ALSO
L,
L,
L,
L.
Perlbal-1.80/lib/Perlbal/Manual/Roles.pod 0000644 0001750 0001750 00000001667 11516736504 020371 0 ustar dormando dormando =head1 NAME
Perlbal::Manual::Roles - Roles supported by Perlbal
=head2 VERSION
Perlbal 1.78.
=head2 DESCRIPTION
Brief description of Perlbal's roles.
=head2 Available Roles
=head3 management
Used to set a management service (a port you can telnet to and issue configuration/debugging commands to).
See L for more information.
=head3 reverse_proxy
Used for reverse proxy and load balancing.
See L and L for more information.
=head3 selector
A virtual service that maps onto other services.
See L for more information.
=head3 web_server
Used to set Perlbal as a web server.
See L for more information.
=head2 SEE ALSO
L,
L,
L,
L,
L.
Perlbal-1.80/lib/Perlbal/Manual/Management.pod 0000644 0001750 0001750 00000034265 11516736504 021361 0 ustar dormando dormando =head1 NAME
Perlbal::Manual::Management - Managing Perlbal on-the-fly
=head2 VERSION
Perlbal 1.78.
=head2 DESCRIPTION
How to configure a Perlbal C service for on-the-fly configuration and debugging.
=head2 READ ME FIRST
Please read L first for a better explanation on how to configure Perlbal. This document will make much more sense after reading that.
=head2 Setting up a management service
You can create a management service in the following way:
CREATE SERVICE mgmt
SET role = management
SET listen = 127.0.0.1:16000
ENABLE mgmt
=head3 Consulting information with a browser
If you access the management service (in this case, 127.0.0.1 on port 16000) with a browser you'll reach a page with information on the services that are enabled. You're also able to click those services and reach further information on each of them.
=head3 Managing Perlbal via telnet
If you telnet to the management service you get a connection that allows you manage your Perlbal's instance.
$ telnet 127.0.0.1 16000
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
You now have access to several commands (which are case insensitive):
=head4 Admin Commands
=over 4
=item create pool
=item create service
Creates a pool or a service.
create pool my_new_pool
create service my_new_service
=item disable
Disables a service (stops listening).
disable my_service
Note that you can not disable C services.
=item enable
Enables a service (starts listening).
enable my_service
=item header insert :
=item header remove
Inserts or removes headers from the request before they're passed on to the backend.
When adding headers you must state the value:
header my_service insert x-myamazingheader:myamazingvalue
When removing headers you don't need the state any value, the header will be removed regardless of the value it holds:
header my_service remove Connection
Note: the usage of C in the beginning of your header is not required, but the convention is that non-standard headers be prefixed with C.
=item load
Loads a Perlbal plugin.
load AccessControl
=item plugins
Lists plugins
=item pool add
=item pool remove
Allows you to add or remove nodes from a service.
pool add my_service 127.0.0.1:1337
pool remove my_service 127.0.0.1:1337
Note that adding a node that already exists or removing a non-existing node do not result in an error message.
Also note that the port number defaults to :80 if omitted.
=item reproxy_state
Dumps the state of reproxy status.
reproxy_state
SERVER max_reproxy_connections = 0
=item server =
Sets a server parameter, where param is one of:
=over 8
=item * max_reproxy_connections =
Maximum number of concurrent connections to the backends.
=item * max_reproxy_connections(C) =
Maximum number of concurrent connections to one specific backend.
=item * max_connections =
Sets the value of C (maximum number of open files for this process).
Requires L. Also, Perlbal must be run under a superuser.
=item * nice_level =
Sets the C level for the process.
=item * aio_mode =
One of C, C for L, or C for L.
This controls how disk IO is done asynchronously. Highly recommended to use L or L for webserving or reproxying files. For purely reverse proxy or only reproxying URLs, none is fine.
=item * aio_threads =
Number of child threads doing disk IO. Use between 2 and 50.
=item * track_obj =
Developer option to track objects.
=item * pidfile =
Filename to write pidfile to (no pidfile if not specified).
=item * crash_backtrace =
1 or 0 indicating whether to perform a backtrace while the server is crashing.
=back
=item set [] =
Sets a property on a pool. If the pool was just created, specifying the pool name is optional.
Setting the file to autoload nodes from (Perlbal will periodically check the file for updates):
SET my_pool nodefile = /path/to/file
Unsetting the file to autoload nodes from (note that this does not remove current members):
SET my_pool nodefile = none
C, C, "" and '' are interpreted just like C.
Note that manually modifying the pool (via POOL ADD or POOL REMOVE) will disable the periodic checking of the nodefile.
Setting the load balancing method:
SET pool balance_method = 'random'
Yes, we do realize that C is the only method currently available, but hey.
=item set [] =
Sets a property on a service. If the service was just created, specifying the service name is optional.
See L, L and L for the list of available parameters.
=item show service []
=item show pool []
List all services:
show service
Show details of a service:
show service my_service
Lists all pools, nodes and services using them:
show pool
Show the members of a pool:
show pool my_pool
=item shutdown [graceful]
Shuts down the server (you'll also lose your telnet session).
Can be used instantly, killing all active connections:
shutdown
Using the parameter C listening sockets are closed and perlbal stays alive until clients bleed off.
shutdown graceful
=item unload
Unloads a plugin.
unload AccessControl
=item use
=item use
Sets the implied service or pool for future operations.
For instance, after:
use my_pool
You can just type:
pool add 127.0.0.1:6000
Note that creating a service or pool also sets it as the implied service.
=item xs
=item xs [enable ]
=item xs [disable ]
Show status of XS modules loaded:
xs
Turn on an already-loaded XS module:
xs enable module_name
Turn off an already-loaded XS module:
xs disable module_name
=back
=head4 Diagnostic Commands
=over 4
=item aio
Shows L stats.
=item backends
Lists the open connections to the backends.
backends
127.0.0.1:3080 1
127.0.0.1:3081 2
In the above example, there are 3 open connections (they may be being used or they may simply be open according to the value of C).
=item dumpconfig
Shows the current configuration for each service.
If a service uses a plugin that supports C, that plugin's configuration is also dumped (see C under L for more information).
=item fd
Shows the maximum number of file descriptors and how many are currently in use.
fd
max 1024
cur 8
=item gladiator
Requires L.
Iterate's Perl's internal memory structures and can be used to enumerate all the currently live SVs.
This can be used to hunt leaks and to profile memory usage.
=item help
Displays the list of available commands.
=item leaks
=item leaks
Iterates over active objects.
If an argument is specified, it is treated as code with C<$_> being the reference to the object.
Shows objects that might have been leaked.
=item mime
Lists known mime types.
mime
css text/css
doc application/msword
...
=item node
Dumps information on a specific node.
node 127.0.0.1:8181
127.0.0.1:8181 attempts 10
127.0.0.1:8181 lastattempt 1290461126
Calling C with a parameter is the same as calling C.
=item nodes
Dumps information on all nodes.
nodes
127.0.0.1:8181 attempts 10
127.0.0.1:8181 lastattempt 1290461126
127.0.0.1:8081 connects 19
127.0.0.1:8081 lastconnect 1290461127
127.0.0.1:8081 attempts 19
127.0.0.1:8081 responsecodes 200 11
127.0.0.1:8081 lastattempt 1290461127
=item noverify
Shows the amount of time left for each node marked as noverify.
If the C parameter is set to a true value, perlbal tries to send an C command to a node before sending it the actual client request. If the node doesn't support the C command, the node is added to an internal hash so that an C command is not issued for new requests during the next 60 seconds.
noverify
127.0.0.1:8081 42
In the above example, node 127.0.0.1:8081 is on that list and still has 42 seconds until an C command is tried again.
If the time is a negative value it means that the C command will be issued before the next request.
=item obj
Shows objects count in scope.
The environment variable C must be set to a true value (see C under L for more information).
=item pending
Shows pending backend connections by service, node, and age.
=item proc
Shows CPU usage, current time, pid, and total requests processed.
=item prof on
=item prof off
=item prof data
Enables/disables profiling or dumps profile data.
=item queues
Shows status of all queues (normal, high priority and low priority) for all enabled services.
queues
service_mywebsite-normal.age 1
service_mywebsite-normal.count 8
service_mywebsite-highpri.age 0
service_mywebsite-highpri.count 0
service_mywebsite-lowpri.age 0
service_mywebsite-lowpri.count 0
=item reload
Reloads a plugin that supports reloading.
=item socks [all]
=item socks summary
Lists all sockets or shows a summary of the sockets (by default lists all sockets).
socks
fd age
3 748s Perlbal::ClientManage(R): open to 85.245.86.253:52248
4 1003s Perlbal::TCPListener(R): open: listening on 82.102.30.112:80 for service 'ws'
6 1003s Perlbal::TCPListener(R): open: listening on 0.0.0.0:60000 for service 'mgmt'
socks summary
1 Perlbal::ClientManage
2 Perlbal::TCPListener
Aggregate write buffer: 0.0k
Open files: 0
=item state changes
Lists recent state changes of requests.
For instance, while a request is being processed this command may issue something like:
state changes
Perlbal::ClientProxy=HASH(0x12d7ec28): reading_headers, wait_backend, backend_req_sent, wait_res
Perlbal::BackendHTTP=HASH(0x12d75f30): connecting, bored, sending_req, wait_res
wait_res 2
And as soon as the request is processed:
state changes
Perlbal::BackendHTTP=HASH(0x12d75f30): connecting, bored, sending_req, wait_res, xfer_res, closed
closed 1
Note: This functionality depends on the environment variable C being set to a true value.
=item states
=item states
Shows how many sockets of which type and in which state currently exist for all services.
states
Perlbal::ClientProxy draining_res 1
If a service is specified, shows the information for that service only.
states ws
Perlbal::BackendHTTP wait_res 1
Perlbal::ClientProxy wait_res 1
=item track
Dumps objects tracked, sorted by age.
track
15s Perlbal::HTTPHeaders=HASH(0x1a043a50): Perlbal::HTTPHeaders::clone, Perlbal::BackendHTTP::assign_client, Perlbal::Service::register_boredom, Perlbal::BackendHTTP::event_write, Danga::Socket::EpollEventLoop, (eval), Perlbal::run
15s Perlbal::HTTPHeaders=HASH(0x19594fe0): Perlbal::HTTPHeaders::new, Perlbal::Socket::read_headers, Perlbal::Socket::read_response_headers, Perlbal::BackendHTTP::event_read_waiting_options, Perlbal::BackendHTTP::event_read, Danga::Socket::EpollEventLoop, (eval), Perlbal::run
15s Perlbal::HTTPHeaders=HASH(0x1a043d80): Perlbal::HTTPHeaders::new, Perlbal::Socket::read_headers, Perlbal::Socket::read_response_headers, Perlbal::BackendHTTP::event_read, Danga::Socket::EpollEventLoop, (eval), Perlbal::run
15s Perlbal::BackendHTTP=HASH(0x1a044608): Perlbal::BackendHTTP::new, Perlbal::Service::spawn_backends, Perlbal::Service::note_bad_backend_connect, Perlbal::BackendHTTP::event_err, Danga::Socket::EpollEventLoop, (eval), Perlbal::run
15s Perlbal::BackendHTTP=HASH(0x1a042588): Perlbal::BackendHTTP::new, Perlbal::Service::spawn_backends, Perlbal::Service::note_bad_backend_connect, Perlbal::BackendHTTP::verify_failure, Perlbal::BackendHTTP::event_read_waiting_options, Perlbal::BackendHTTP::event_read, Danga::Socket::EpollEventLoop, (eval), Perlbal::run
15s Perlbal::BackendHTTP=HASH(0x1a044488): Perlbal::BackendHTTP::new, Perlbal::Service::spawn_backends, Perlbal::Service::request_backend_connection, Perlbal::ClientProxy::request_backend, Perlbal::ClientProxy::handle_request, Perlbal::ClientProxy::event_read, Danga::Socket::EpollEventLoop, (eval), Perlbal::run
For this to work you must have the environment variable C set to a true value and the server option C on:
server track_obj = 1
OK
=item uptime
=item version
Shows the time the server was started, the current uptime and Perlbal's version.
=item varsize
Debug management command to track size of internal data structures.
varsize
svc-ws-bored_backends [] 2
svc-ws-pending_connects {} 2
{127.0.0.1:3080} =
{127.0.0.1:3081} = Perlbal::BackendHTTP=HASH(0x1cfd2490)
=item verbose on
=item verbose off
Turns verbose mode on or off.
verbose on
load AccessControl
OK
verbose off
load AccessControl
verbose on
OK
load AccessControl
OK
Yes, verbose mode only activates the printing of an OK message upon completion of some commands such as C. Errors are displayed regardless of verbose status.
=back
=head2 CAVEAT
Note that this functionality doesn't implement any sort of authentication. If you can telnet to the address you specify, you can access the information and change things on the fly.
Some alternatives to authentication are commonly employed:
=over 4
=item * specifying the address as being 127.0.0.1 means that no one outside the machine can telnet to the management service;
=item * setting the management address as a private IP available only through a VPN, for instance, will allow you to manage these accesses on the VPN instead.
=back
=head2 SEE ALSO
L,
L.
Perlbal-1.80/lib/Perlbal/Manual/Logging.pod 0000644 0001750 0001750 00000012011 11516736504 020654 0 ustar dormando dormando =head1 NAME
Perlbal::Manual::Logging - How Perlbal's logging system works
=head2 VERSION
Perlbal 1.78.
=head2 DESCRIPTION
Perlbal supports logging of a few messages (and you can log your messages in your plugins, for instance).
This document describes how to achieve that.
=head2 IMPORTANT: foreground vs. background
If Perlbal is running on the foreground, it logs by calling C, which means you should get the logs on C.
If Perlbal is running on the background, it logs through L. If L is not available, there will be no logging, and THAT'S THE MOST IMPORTANT THING TO KNOW ABOUT PERLBAL'S LOGGING SYSTEM.
=head2 How to log a message
You can log a message by calling C as you'd call L's C:
Perlbal::log( $priority, $format, @args );
You should read the documentation for L for more information, but here's an example:
Perlbal::log( 'info', 'beginning run' );
And here's another example:
Perlbal::log( 'crit', "this thing crashed: $!" );
=head2 What is logged?
=over 4
=item * When we try to read from or write to a filehandle that is undefined, L logs a critical message:
Perlbal::log("crit", "Undef \$fh: $stack_trace");
=item * When failing to create a socket, L logs a critical message:
Perlbal::log('crit', "Error creating socket: $!");
=item * When C fails to create a socket, L logs a critical message:
Perlbal::log('crit', "inet_aton failed creating socket for $ip");
=item * When writing to a client, if we try to read more than we should from the backend, L logs a warning message:
Perlbal::log('warning', "tried to readahead negative bytes. filesize=$self->{reproxy_file_size}, offset=$self->{reproxy_file_offset}");
=item * When opening a file being PUT for writing to disk, if there's an error (which is going to originate a 500 server error), L logs a warning message:
Perlbal::log('warning', "system error: $msg ($info)");
=item * If we receive a request with a content lenght different from the actual length of the request, L logs a critical message:
Perlbal::log('crit', "Content length of $clen declared but $self->{buoutpos} bytes written to disk");
=item * When trying to buffer data to disk, if the operation fails L logs a critical message:
Perlbal::log('crit', "Failure to open $fn for buffered upload output");
=item * After buffering data to disk, if the file is empty, L logs a critical message:
Perlbal::log('crit', "Error writing buffered upload: $!. Tried to do $len bytes at $self->{buoutpos}.");
=item * When purging a buffered upload on the disk, if an error occurs, L logs a critical message:
Perlbal::log('warning', "Unable to link $self->{bufilename}: $!");
=item * When marking a backend as pending, if there's already another one in that ip/port, L