openSUSE / openSUSE/Mojo-IOLoop-ReadWriteProcess

Leaks and eventually runs out of file descriptors due to a ref cycle

Open
#86 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Perl
Stars
12
Forks
14
PR merge metrics
No merged PRs in 30d

Description

Hi,

the docs don't mention this so I don't think I am "holding it wrong". If I do this:

sub do_a_thing
{
    my $self = shift;
    my $process = process(execute => '/usr/local/bin/whatever')->args(['arg', 'arg']);
    $self->child_process($process);
    $process->on(stop => sub {
        my $process = shift;
        if ($process->exit_status)
        {
            warn "child failed: " . $process->exit_status;
            $self->clear_child_process;
            return;
        }
        my $output = $process->read_stdout;
        warn $output;
        $self->clear_child_process;
        return;
    });
    $process->start;
}

After the process exits, I don't hold any handles to $process, so I'd expect it to go out of scope and eventually lose the file descriptors for the IPC pipes. But it doesn't, because of a cycle:

Cycle (1):
        $Mojo::IOLoop::ReadWriteProcess::A->{'session'} => \%Mojo::IOLoop::ReadWriteProcess::Session::B
        $Mojo::IOLoop::ReadWriteProcess::Session::B->{'process_table'} => \%C
                          $C->{'2160'} => \$D
                                   $$D => \%Mojo::IOLoop::ReadWriteProcess::A

and this never gets called:

package Mojo::IOLoop::ReadWriteProcess;

sub DESTROY
{
    warn "destroying $_[0]";
}

I can resolve this manually by calling $process->session->clean, but I think the "proper" fix is for this ref to be weakened immediately after it is created:

https://github.com/openSUSE/Mojo-IOLoop-ReadWriteProcess/blob/5831e6499c54a3ae0ee51c8708a9232ab99190f1/lib/Mojo/IOLoop/ReadWriteProcess/Session.pm#L101-L105

I've verified the following monkey patch works which means "weaken($process)" above would be a bare minimum fix:

package Mojo::IOLoop::ReadWriteProcess::Session;

use Scalar::Util qw(weaken);

sub register
{
    my $self = shift;
    my $pid = shift;
    my $process = shift;
    $self->process_table()->{$pid} = \$process;
    weaken($process);
    $self->emit(register => $process);
}

But there's probably a wider architectural question, since process "registers" itself to the session when it starts, perhaps it should also "unregister" itself from the session when it stops so it's not just relying on going out of scope to drop the (by then defunct) pipes.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in lib/Mojo/IOLoop/ReadWriteProcess/Session.pm at the register implementation around lines 101-105, then trace how the process table is cleaned when a process stops. Reproduce the example and inspect the reference cycle and DESTROY behavior. Done means stopped processes no longer remain retained by the session or leak their IPC file descriptors.

Written by the indexing model from the issue text.

Assessment

Tech stack
perl
Domain
operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.