The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/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:<length>     writes <length> bytes
#    sleep:<duration>   sleeps <duration> 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:<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;