How do I localize an object that is inside a property of a Moo object in Perl?

168 Views Asked by At

I've got an object that stores an LWP::UserAgent. I want to use different cookie jars for different calls with that UA, so I decided to make the cookie_jar local when doing a call.

The following code shows what I did without debug stuff (for reading, not running). Below is another version with lots of debugging output.

package Foo;
use strictures;
use Moo;
use LWP::UserAgent;

has ua => (
  is      => 'ro',
  default => sub { my $ua = LWP::UserAgent->new; $ua->cookie_jar( {} ); return $ua; },
);

sub request {
    my ($self, $cookie_jar) = @_;

    local $self->{ua}->{cookie_jar} = $cookie_jar;
    $self->ua->get('http://www.stackoverflow.com');
}

package main;
my $foo = Foo->new;
my $new_jar = HTTP::Cookies->new;
$foo->request( $new_jar );

So basically I decided to locally overwrite the cookie jar. Unfortunately, when we call get it will still use the cookie jar that is originally inside the UA object.

package Foo;
use strictures;
use Moo;
use LWP::UserAgent;
use HTTP::Cookies;
use Data::Printer;
use feature 'say';

has ua => (
  is      => 'ro',
  default => sub { my $ua = LWP::UserAgent->new; $ua->cookie_jar( {} ); return $ua; },
);

sub request {
    my ($self, $cookie_jar) = @_;

    say "before local " . $self->{ua}->{cookie_jar};
    local $self->{ua}->{cookie_jar} = $cookie_jar;
    $self->ua->get('http://www.stackoverflow.com');
    print "local jar " . p  $self->{ua}->{cookie_jar};
    say "after local " . $self->{ua}->{cookie_jar};
}

package main;
use Data::Printer;
use HTTP::Cookies;

my $foo = Foo->new;
say "before outside of local " . $foo->{ua}->{cookie_jar};
my $new_jar = HTTP::Cookies->new;
say "before outside of local " . $new_jar;
$foo->request( $new_jar );
say "after outside of local " . $foo->{ua}->{cookie_jar};
print "global jar " . p $foo->ua->cookie_jar;

__END__
before outside of local HTTP::Cookies=HASH(0x30e1848)
before outside of local HTTP::Cookies=HASH(0x30e3b20)
before local HTTP::Cookies=HASH(0x30e1848)
local jar HTTP::Cookies  {
    public methods (13) : add_cookie_header, as_string, clear, clear_temporary_cookies, DESTROY, extract_cookies, load, new, revert, save, scan, set_cookie, set_cookie_ok
    private methods (3) : _host, _normalize_path, _url_path
    internals: {
        COOKIES   {}
    }
}after local HTTP::Cookies=HASH(0x30e3b20)
after outside of local HTTP::Cookies=HASH(0x30e1848)
global jar HTTP::Cookies  {
    public methods (13) : add_cookie_header, as_string, clear, clear_temporary_cookies, DESTROY, extract_cookies, load, new, revert, save, scan, set_cookie, set_cookie_ok
    private methods (3) : _host, _normalize_path, _url_path
    internals: {
        COOKIES   {
            stackoverflow.com   {
                /   {
                    prov   [
                        [0] 0,
                        [1] "185e95c6-a7f4-419a-8802-42394776ef63",
                        [2] undef,
                        [3] 1,
                        [4] undef,
                        [5] 2682374400,
                        [6] undef,
                        [7] {
                            HttpOnly   undef
                        }
                    ]
                }
            }
        }
    }
}

As you can see, the HTTP::Cookies object gets localized and replaced correctly. The addresses look totally correct.

But the output of p tells a different story. LWP::UA has not used the local cookie jar at all. That remains a fresh, empty one.

How can I make it use the local one instead?

I have tried using Moo, Moose and classic bless objects. All show this behaviour.


Edit: Since this came up in the comments, let me give a little more background why I need to do this. This is going to be a bit of a rant.

TLDR: Why I do not want alternative solution but understand and fix the problem

I'm building a Dancer2-based webapp that will run with Plack and multiple workers (Twiggy::Prefork - multiple threads in multiple forks). It will allow users to use a service of a third company. That company offers a SOAP webservice. Think of my application as a custom frontend to this service. There is a call to 'log the user in' on the webservice. It returns a cookie (sessionid) for that specific user and we need to pass that cookie with each consecutive call.

To do the SOAP-stuff I am using XML::Compile::WSDL11. Compiling the thing is pretty costly, so I do not want to do that each time a route is handled. That would be way inefficient. Thus the SOAP client will be compiled from the WSDL file when the application starts. It will then be shared by all workers.

If the client object is shared, the user agent inside is shared as well. And so is the cookie jar. That means that if there are two requests at the same time, the sessionids might get mixed up. The app could end up sending wrong stuff to the users.

That's why I decided to localize the cookie jar. If it's a local unique one for a request, it will never be able to interfere with another worker's request that is happening in parallel. Just making a new cookie jar for each request will not cut it. They would still be shared, and might even get lost because they would overwrite each other in the worst case.

Another approach would be to implement a locking mechanism, but that would totally beat the purpose of having multiple workers.

The only other solution I see is using another SOAP-client alltogether. There is SOAP::WSDL, which does not run on newer Perls. according to CPAN testers it breaks on 5.18 andI have verified that. It would be more efficient as it works like a code generator and precreates classes that are cheaper to use than just compiling the WSDL file every time. But since it's broken, it is out of the question.

SOAP::Lite will compile the WSDL, and badly. It is not something anyone should use in production if it can be avoided in my opinion. The only alternative left that I see is to implement the calls without using the WSDL file and parsing the results directly with an XML parser, ignoring the schema. But those are BIG results. It would be very inconvenient.

My conclusion to this rant is that I would really like to understand why Perl does not want to localize the cookie jar in this case and fix that.

1

There are 1 best solutions below

4
On BEST ANSWER

Perhaps instead of using local you use the clone and cookie_jar methods of LWP::UserAgent.

...

sub request {
    my ($self, $new_cookie_jar) = @_;
    my $ua = $self->ua; # cache user agent

    if( defined $new_cookie_jar ){
        # create a new user agent with the new cookie jar
        $ua = $ua->clone;
        $ua->cookie_jar( $new_cookie_jar );
    }

    my $result = $ua->get('http://www.stackoverflow.com');

    # allow returning the newly cloned user agent
    return ( $result, $ua ) if wantarray;
    return $result;
}

If you don't want to do that, you should at least use the methods instead of manipulating the internals of the objects.

...

sub request {
    my ($self, $new_cookie_jar) = @_;
    my $ua = $self->ua; # cache user agent

    my $old_cookie_jar = $ua->cookie_jar( $new_cookie_jar );

    my $result = $ua->get('http://www.stackoverflow.com');

    # put the old cookie jar back in place
    $ua->cookie_jar( $old_cookie_jar );

    return $result;
}