Skip Menu |

This queue is for tickets about the POE CPAN distribution.

Report information
The Basics
Id: 23181
Status: resolved
Priority: 0/
Queue: POE

People
Owner: Nobody in particular
Requestors: perl [...] pied.nu
Cc:
AdminCc:

Bug Information
Severity: Important
Broken in: 0.38
Fixed in: 0.32



Subject: POE::Wheel::Run ref count problems
A session's ref count isn't maintained properly when using POE::Wheel::Run. I suspect it is being decremented 1 time to many in the Wheel's DESTROY. I've tested this with multiple POE versions : - 0.38 - 0.37 - 0.9500 Perl : 5.8.8, 5.8.6.
Subject: test-sub.t
#!/usr/bin/perl -w # $Id$ use strict; sub POE::Kernel::TRACE_REFCNT () { 0 } sub POE::Kernel::ASSERT_DATA () { 1 } use POE; use Data::Dumper; use Test::More ( tests => 2 ); sub DEBUG () { 0 } SKIP: { eval 'use POE::API::Peek;'; skip "Must have POE::API::Peek installed", 2 if $@; my $object = Tester->spawn( [qw(client)] ); diag( "This test takes about 20 seconds" ); $poe_kernel->run(); pass( "Everything exited" ); is( $object->{refcnt}, 0, "Session refcnt is zero to exit" ); # DEBUG and warn Dumper $object; } ########################################################################## package Tester; use strict; use POE; use POE::Wheel::Run; my( $NODE, $SERVER, $CLIENT ); BEGIN { *DEBUG = \&main::DEBUG; $CLIENT = sub { use strict; $|++; print "I am PID=$$\n"; my $N=5; my $server = shift @_; while( $N-- ) { sleep 1; print "still here\n"; } }; } my $HAVE_DAEMON=0; eval q{ use POE::Component::Daemon; $HAVE_DAEMON=1; }; sub spawn { my( $package, $todo ) = @_; my $self = bless { STARTUP=>$todo }, $package; $self->create_session; return $self; } sub create_session { my( $self ) = @_; POE::Session->create( inline_states => { client_stdout => sub { $self->client_stdout( @_[ARG0..$#_] ) }, client_stderr => sub { $self->client_stderr( @_[ARG0..$#_] ) }, client_close => sub { $self->client_close( @_[ARG0..$#_] ) }, error => sub { $self->error( @_[ARG0..$#_] ) }, shutdown => sub { $self->shutdown( @_[ARG0..$#_] ) }, _start => sub { $self->start( @_[ARG0..$#_] ) }, _stop => sub { $self->stop( @_[ARG0..$#_] ) }, do_next => sub { $self->do_next( @_[ARG0..$#_] ) }, CHLD => sub { $self->CHLD( @_[ARG0..$#_] ) }, USR2 => sub { Daemon->peek( 1 ) if $HAVE_DAEMON; }, } ); } ################################################### sub start { my( $self ) = @_; if( $HAVE_DAEMON ) { # ::diag( "$0 is $$" ); $poe_kernel->sig( USR2 => 'USR2' ); } $poe_kernel->sig( CHLD => 'CHLD' ); $poe_kernel->yield( 'do_next' ); } ################################################### sub stop { my( $self ) = @_; my $api = POE::API::Peek->new(); $self->{refcnt} = $api->get_session_refcount; DEBUG and warn "Tester: STOP\n"; } ################################################### sub do_next { my( $self ) = @_; my $next = shift @{ $self->{STARTUP} }; unless( $next ) { DEBUG and warn "Tester: Startup completed\n"; return; } $self->can( $next )->( $self ); $poe_kernel->delay( 'do_next', 2 ); } ################################################### # Spawn node-client, which will connect to ikc-node and wait for node-server # Also spawn a second ikc-node, which connects to first and listens sub client { my( $self ) = @_; # die "No server PID???!" unless $self->{server_PID}; DEBUG and warn "Tester: Launching client $self->{server_PID}\n"; # $poe_kernel->refcount_increment( $poe_kernel->get_active_session->ID, "Wheel::Run bug" ); $self->{client_wheel} = POE::Wheel::Run->new( Program => $CLIENT, ProgramArgs => [ $self->{server_PID} ], ErrorEvent => 'error', CloseEvent => 'client_close', StdoutEvent => 'client_stdout', StderrEvent => 'client_stderr', Conduit => 'socketpair' ); $self->{ PIDs }{ $self->{client_wheel}->PID } = 1; } ###################################################### sub error { my( $self, $syscall, $errno, $errstr, $id, $handle ) = @_; return if $errno == 0; warn "Error in wheel $id during $syscall: $errno ($errstr) handle=$handle\n"; } ###################################################### sub CHLD { my( $self, $signal, $PID, $status ) = @_; unless( delete $self->{ PIDs }{ $PID } ) { warn "CHLD for someone else: PID=$PID status=$status"; } else { DEBUG and warn "Tester: CHLD for $PID\n"; } } ###################################################### sub node_stdout { my( $self, $input, $wid ) = @_; DEBUG and warn "NODE: $input\n"; if( $input =~ /PID=(\d+)/ ) { $self->{node_PID} = $1; $self->child_pid( $self->{node_PID} ); END { kill 15, $self->{node_PID} if $self->{node_PID} }; } else { push @{ $self->{node_output} }, $input; } } sub node_stderr { my( $self, $input, $wid ) = @_; # DEBUG and warn "NODE err: $input\n"; push @{ $self->{node_error} }, $input; } sub node_close { my( $self, $wid ) = @_; DEBUG and warn "NODE wheel: closed\n"; delete $self->{node_wheel}; $self->{node_close}++; } ###################################################### sub node2_stdout { my( $self, $input, $wid ) = @_; DEBUG and warn "NODE2: $input\n"; if( $input =~ /PID=(\d+)/ ) { $self->{node2_PID} = $1; $self->child_pid( $self->{node2_PID} ); END { kill 15, $self->{node2_PID} if $self->{node2_PID} }; } else { push @{ $self->{node2_output} }, $input; } } sub node2_stderr { my( $self, $input, $wid ) = @_; DEBUG and warn "NODE2 err: $input\n"; if( $input =~ /An address was in use/ ) { $self->{node2_in_use}++; } else { push @{ $self->{node2_error} }, $input; } } sub node2_close { my( $self, $wid ) = @_; DEBUG and warn "NODE2 wheel: closed\n"; delete $self->{node2_wheel}; $self->{node2_close}++; } ###################################################### sub server_stdout { my( $self, $input, $wid ) = @_; DEBUG and warn "SERVER: $input\n"; if( $input =~ /PID=(\d+)/ ) { $self->{server_PID} = $1; $self->child_pid( $self->{server_PID} ); END { kill 15, $self->{server_PID} if $self->{server_PID} }; } elsif( $input =~ /^I am ([-.\w]+-[0-9a-f]+)/ ) { $self->{server_ID} = $1; } elsif( $input =~ /^Remote kernel (?:alias )?([-:.\w]+) went HELLO!/ ) { $self->{server_connect}{$1}++; } elsif( $input =~ m(^Connected poe://(Client\d+)/me/pulse) ) { $self->{client_connect}{$1}++; } elsif( $input =~ /^(Running server|Sending time|Done|Server exited).../ ) { $self->{$1}++; } elsif( $input =~ m(^poe://(Client\d+)/me/pulse -- ) ) { $self->{client_pulse}{$1}++; } elsif( $input =~ /\S/ ) { push @{ $self->{server_output} }, $input; } } sub server_stderr { my( $self, $input, $wid ) = @_; if( $input =~ /Not connected to proxy-node/ ) { die "Race-condition in server! ($input)\n"; } DEBUG and warn "SERVER err: $input\n"; push @{ $self->{server_error} }, $input; } sub server_close { my( $self, $wid ) = @_; DEBUG and warn "SERVER wheel: close\n"; delete $self->{server_wheel}; $self->{server_close}++; } ###################################################### sub client_stdout { my( $self, $input, $wid ) = @_; DEBUG and warn "CLIENT: $input\n"; if( $input =~ /PID=(\d+)/ ) { $self->{client_PID} = $1; $self->child_pid( $self->{client_PID} ); END { kill 15, $self->{client_PID} if $self->{client_PID} }; } elsif( $input =~ /^I am ([-.\w]+-[0-9a-f]+)/ ) { $self->{client_ID} = $1; } elsif( $input =~ /^\* connection to ([-:.\w]+)/ ) { $self->{client_connect}{$1}++; } elsif( $input =~ m(^\*\*\*\*\* Connected to (Pulse)) ) { $self->{client_connect_Pulse}++; } elsif( $input =~ m(^\*\*\*\*\* Disconnected from (Pulse)) ) { $self->{client_connect_Pulse}--; } elsif( $input =~ m(^HONK HONK) ) { $self->{client_honk}++; } elsif( $input =~ /^(Running client|Creating sessions).../ ) { $self->{$1}++; } elsif( $input =~ m(Subscribed to HASH.+ on ([-:.\w]+)) ) { $self->{client_subscribe}{$1}++; } elsif( $input =~ m(\|+ Foreign time is (.+)) ) { $self->{client_time} = $1; } elsif( $input =~ m([+|]+\* disconnection from Pulse) ) { $self->{client_disconnect} = 'Pulse'; } elsif( $input =~ /\S/ ) { push @{ $self->{client_output} }, $input; } } sub client_stderr { my( $self, $input, $wid ) = @_; DEBUG and warn "CLIENT err: $input\n"; push @{ $self->{client_error} }, $input; } sub client_close { my( $self, $wid ) = @_; DEBUG and warn "Tester: Client closed; All done"; die "NO node2_PID!!" unless $self->{node2_PID}; # POE now waits for all children to exit before exiting kill 15, $self->{node2_PID} if $self->{node2_PID}; $self->{client_close}++; DEBUG and warn "Tester: Client close: shutdown\n"; if( $HAVE_DAEMON ) { ::diag( "kill -USR2 $$ # to find out why this isn't exiting" ); } delete $self->{client_wheel}; delete $self->{server_wheel}; delete $self->{node_wheel}; delete $self->{node2_wheel}; die "NO node_PID!!" unless $self->{node_PID}; # POE now waits for all children to exit before exiting kill 15, $self->{node_PID} if $self->{node_PID}; } sub child_pid { my( $self, $pid ) = @_; return unless $pid; return unless $poe_kernel->can( 'sig_child' ); $poe_kernel->sig_child( $pid ); } 1; __END__
Turns out this had nothing to do with POE::Wheel::Run and everything to do with me not using sig_child() properly. The included patch makes sure sig_child() can't be abused. And includes a test case.
diff -ruN POE-0.9500/lib/POE/Resource/Signals.pm POE-0.9500-PG/lib/POE/Resource/Signals.pm --- POE-0.9500/lib/POE/Resource/Signals.pm 2006-09-16 01:34:11.000000000 -0400 +++ POE-0.9500-PG/lib/POE/Resource/Signals.pm 2006-11-16 11:16:36.000000000 -0500 @@ -276,6 +276,14 @@ sub _data_sig_pid_watch { my ($self, $session, $pid, $event) = @_; + my $already = 0; + + # had the session previously registered a PID watcher? + if( $kr_pids_to_events{$pid}{$session} ) { + # don't increment refcount a second time + $already = 1; + } + $kr_pids_to_events{$pid}{$session} = [ $session, # PID_SESSION $event, # PID_EVENT @@ -284,28 +292,43 @@ $self->_data_sig_signal_watch($session, "CHLD"); $kr_sessions_to_pids{$session}{$pid} = 1; - $self->_data_ses_refcount_inc($session); + $self->_data_ses_refcount_inc($session) unless $already; } sub _data_sig_pid_ignore { my ($self, $session, $pid) = @_; + my $removed = 0; + # Remove PID to event mapping. - delete $kr_pids_to_events{$pid}{$session}; - delete $kr_pids_to_events{$pid} unless ( - keys %{$kr_pids_to_events{$pid}} - ); + if( delete $kr_pids_to_events{$pid}{$session} ) { + $removed++; + delete $kr_pids_to_events{$pid} unless ( + keys %{$kr_pids_to_events{$pid}} + ); + } # Remove session to PID mapping. - delete $kr_sessions_to_pids{$session}{$pid}; - unless (keys %{$kr_sessions_to_pids{$session}}) { - delete $kr_sessions_to_pids{$session}; - $self->_data_sig_signal_ignore($session, "CHLD"); + if( delete $kr_sessions_to_pids{$session}{$pid} ) { + $removed++; + unless (keys %{$kr_sessions_to_pids{$session}}) { + delete $kr_sessions_to_pids{$session}; + $self->_data_sig_signal_ignore($session, "CHLD"); + } + } + if( $removed ) { + $self->_data_ses_refcount_dec($session); + if( ASSERT_DATA and $removed != 2 ) { + _trap( "<dt> $session/$pid was not present in both structures ($removed)" ); + } + } + else { + if( ASSERT_USAGE ) { + _carp( "<us> Trying to ignore a signal you never watched in the first place" ); + } } - - $self->_data_ses_refcount_dec($session); } sub _data_sig_pids_ses { diff -ruN POE-0.9500/t/90_regression/leolo_sig_child.t POE-0.9500-PG/t/90_regression/leolo_sig_child.t --- POE-0.9500/t/90_regression/leolo_sig_child.t 1969-12-31 19:00:00.000000000 -0500 +++ POE-0.9500-PG/t/90_regression/leolo_sig_child.t 2006-11-16 11:11:38.000000000 -0500 @@ -0,0 +1,49 @@ +#!/usr/bin/perl -w +# $Id$ +# Make sure that pid_child( $pid ) w/o the pid_child( $pid, $event +# doesn't blow up in our face. + +use strict; + +# The next line is the entire point of this unit test; It will cause the +# test to blow up because of the edge cases we trip below + +sub POE::Kernel::ASSERT_DATA () { 1 } + +use POE; +use Data::Dumper; +use Test::More ( tests => 2 ); + +sub DEBUG () { 1 } + +POE::Session->create( + inline_states => { + _start => sub { + # Ignore an unwatched child. Previously this + # would cause a solitary refcount_dec + $poe_kernel->sig_child( 1234 ); + $poe_kernel->yield( 'work' ); + }, + work => sub { + # This is the proper way + $poe_kernel->sig_child( 1234, "HONK" ); + $poe_kernel->yield( 'more' ); + }, + more => sub { + # Change a watcher. Previously this would cause + # a second refcount_inc() + $poe_kernel->sig_child( 1234, "HONK1" ); + $poe_kernel->yield( 'done' ); + }, + done => sub { + # Our refcount could be back to 0 + $poe_kernel->sig_child( 1234 ); + pass( "done" ); + }, + HONK => sub { die "Why did I get a SIGCHLD!"; }, + HONK1 => sub { die "Why did I get a SIGCHLD!"; }, + } +); + +$poe_kernel->run(); +pass( "Didn't blow up" );
Thanks for the problem description in IRC. I've added a much smaller test case and fixed the underlying issue.