The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
package LWP::Server;
use strict;

use vars qw($DEBUG);

sub new
{
    my($class, $ua, $proto, $host, $port) = @_;

    die "Bad proto" unless $proto =~ /^([a-zA-Z0-9\+\-\.]+)$/;
    my $conn_class = uc($1);  # untaint as well
    $conn_class =~ s/\+/_PLUS_/g;
    $conn_class =~ s/\./_DOT_/g;
    $conn_class =~ s/\-/_MINUS_/g;
    $conn_class = "LWP::Conn::$conn_class";

    my $self = bless
          {
            ua            => $ua,

	    proto         => $proto,
	    conn_class    => $conn_class,

	    created       => time(),
	    request_count => 0,

	    req_queue     => [],
	    conns         => [],
	    idle_conns    => [],
	  }, $class;

    if ($host) {
	$self->{'host'} = $host;
	$self->{'port'} = $port;
    }

    $self;
}

# General parameters

sub proto   {  $_[0]->{'proto'};  }
sub host    {  $_[0]->{'host'};   }
sub port    {  $_[0]->{'port'};   }

sub id
{
    my $self = shift;
    if (my $host = $self->{'host'}) {
	if (my $port = $self->{'port'}) {
	    return "$self->{'proto'}://$host:$port";
	}
	return "$self->{'proto'}://$host";
    }
    return "$self->{'proto'}:";
}

sub c_status
{
    my $self = shift;
    (scalar(@{$self->{req_queue}}),
     scalar(@{$self->{conns}}),
     scalar(@{$self->{idle_conns}}),
     $self->max_conn,
    );
}

# Managing the request queue

sub add_request
{
    my($self, $req) = @_;
    my $pri = $req->priority;
    # XXX should really keep sorted by 'pri' field.  Wouldn't it be nice
    # if Perl had a library similar to Python's bisect.py
    # (perhaps it already has?)
    if ($pri && $pri > 50) {
	push(@{$self->{req_queue}}, $req);
    } else {
	unshift(@{$self->{req_queue}}, $req);
    }
    $self->{'request_count'}++;
    $self->activate_idles;
}

sub stop
{
    my $self = shift;
    # stop all connections
    my @conns = @{$self->{conns}};  # iterate over a copy
    $self->{stopping}++;
    for (@conns) {
	$_->stop;
    }
    $self->kill_queued_requests;
    delete $self->{stopping};
}

sub stop_idle
{
    my $self = shift;
    my @idle = @{$self->{idle_conns}};  # iterate over a copy
    for (@idle) {
	$_->stop;
    }
}

sub kill_queued_requests
{
    my($self, $code, $message, $more) = @_;
    if (!$code) {
	$code = 590;
	$message = "No response";
    }
    while (@{$self->{req_queue}}) {
	my $req = shift @{$self->{req_queue}};
	$req->give_response($code, $message, $more);
    }
}


# Connection management

sub max_conn
{
    my $self = shift;
    $self->{'ua'}->uri_attr_plain($self->id, 'max_conn_per_server') || 2;
}

sub conn_param
{
    my $self = shift;
    my %param;
    for my $hash ($self->{'ua'}->uri_attr_plain($self->id, "conn_param")) {
	while (my($k,$v) = each %$hash) {
	    next if exists $param{$k};
	    $param{$k} = $v;
	}
    }
    # these are always overridden
    $param{ManagedBy} = $self;
    $param{Host} = $self->{'host'};
    $param{Port} = $self->{'port'};

    if (@_) {
	return @param{@_};
    }
    wantarray ? %param : \%param;
}


sub create_connection
{
    my $self = shift;
    my $conn_class = $self->{'conn_class'};
    no strict 'refs';
    unless (defined %{"$conn_class\::"}) {
	eval "require $conn_class";
	if ($@) {
	    $self->kill_queued_requests(590, "No handler for '$self->{'proto'}' scheme", $@);
	    return;
	}
    }
    
    my $conn;
    eval {
	$conn = $conn_class->new($self->conn_param);
    };
    if ($@) {
	print STDERR $@ if $DEBUG;
	chomp($@);
	$self->kill_queued_requests(590, $@);
	$self->done;
	return;
    }
    if ($conn) {
	push(@{$self->{conns}}, $conn);
    } else {
	if (@{$self->{req_queue}}) {
	    my $msg = "Can't connect to " . $self->id;
	    $self->kill_queued_requests(590, $msg, $!);
	}
	$self->done;
    }
}

# Connection protocol

sub get_request
{
    my($self, $conn) = @_;
    my $req = shift(@{$self->{req_queue}});
    if ($req) {
	my $time = time;
	$self->{'last_request_time'} = time;
	$req->sending_start($time);
    }
    $req;
}

sub pushback_request
{
    my $self = shift;
    my $conn = shift;
    unshift(@{$self->{req_queue}}, @_);
    $self->activate_idles;
}

sub activate_connections
{
    my $self = shift;
    my @iconns = @{$self->{idle_conns}};
    my @conns = @{$self->{conns}};
    my %seen;
    # activate idle connections first
    foreach (@iconns) {
	$_->activate;
	$seen{$_}++;
    }
    foreach (@conns) {
	next if $seen{$_};
	$_->activate;
    }
}

sub activate_idles
{
    my $self = shift;
    my @iconns = @{$self->{idle_conns}};
    foreach (@iconns) {
	$_->activate;
    }
}

sub remove_from_refarray
{
    my($self, $arr, $ref) = @_;
    for my $i (0 .. @$arr - 1) {
	if (int($arr->[$i]) == int($ref)) {
	    splice(@$arr, $i, 1);
	    return 1;
	}
    }
    return 0;
}

sub connection_active
{
    my($self, $conn) = @_;
    print STDERR "ACTIVE $conn\n" if $DEBUG;
    $self->remove_from_refarray($self->{idle_conns}, $conn);
}


sub connection_idle
{
    my($self, $conn) = @_;
    print STDERR "IDLE $conn\n" if $DEBUG;
    if ($self->remove_from_refarray($self->{idle_conns}, $conn)) {
	warn "$conn was already in idle_conns";
    }
    push(@{$self->{idle_conns}}, $conn);
}

sub connection_closed
{
    my($self, $conn) = @_;
    print STDERR "CLOSED $conn\n" if $DEBUG;
    $self->remove_from_refarray($self->{idle_conns}, $conn);
    $self->remove_from_refarray($self->{conns}, $conn) or
	warn "$conn was not registered";

    unless (@{$self->{conns}}) {
	# This was the last connection
	if (@{$self->{req_queue}} && !$self->{stoppping}) {
	    $self->create_connection
	} else {
	    $self->done;
	}
    }
}

sub done  # this really just deallocates this LWP::Server entry
{
    my $self = shift;
    my $ua = delete $self->{'ua'};
    $ua->forget_server($self->id);
}

1;