Skip Menu |

This queue is for tickets about the IO-Async-Loop-Epoll CPAN distribution.

Report information
The Basics
Id: 119835
Status: resolved
Priority: 0/
Queue: IO-Async-Loop-Epoll

People
Owner: Nobody in particular
Requestors: DAKKAR [...] cpan.org
Cc:
AdminCc:

Bug Information
Severity: (no value)
Broken in: 0.17
Fixed in: 0.19



Subject: Use ->post_fork to avoid sharing parent's epoll fd
After a fork, the epoll fd (and hence the instance) would be shared among the forked children, in case the loop had been initialised in the parent. In a case like a pre-fork server that initialised the loop in the parent, the children would be adding the same file descriptors multiple times, failing horribly (at the very least, they all would add the listening socket). A possible (vaguely tested some time ago) implementation would be: sub post_fork { my ($loop) = @_; # let's make sure that each child has a separate epoll instance $loop->{epoll} = Linux::Epoll->new; # then we want to re-add all the file descriptors that were # registered in the parent's loop my $watches = $loop->{iowatches}; return unless $watches && ref($watches); # $watches is a hashref keyed on file descriptors: we don't # actually care about the keys for my $watch (values %$watches) { my %args; # this unpacks $watch the same way as # IO::Async::Loop::__watch_io (yes, I'm depending on an # implementation detail, so sue me) @args{qw(handle on_read_ready on_write_ready on_hangup)} = @$watch; # finally we re-add the watch to the loop $loop->watch_io(%args); } return; }
On Thu Jan 12 11:17:51 2017, DAKKAR wrote: Show quoted text
> A possible (vaguely tested some time ago) implementation would be:
... I've borrowed that inspiration, and added the lines of code to actually call it at the appropriate time. -- Paul Evans
Subject: rt119835.patch
=== modified file 'lib/IO/Async/Loop/Epoll.pm' --- lib/IO/Async/Loop/Epoll.pm 2018-01-22 00:00:15 +0000 +++ lib/IO/Async/Loop/Epoll.pm 2018-06-25 16:03:40 +0000 @@ -117,6 +117,8 @@ $self->{signals} = {}; # {$name} => SignalWatch $self->{masks} = {}; + $self->{pid} = $$; + # epoll gets very upset if applications close() filehandles without telling # it, and then try to add that mask a second time. We can attempt to detect # this by storing the mapping from fileno to refaddr($fh) @@ -165,6 +167,8 @@ my $self = shift; my ( $timeout ) = @_; + $self->post_fork if $self->{pid} != $$; + $self->_adjust_timeout( \$timeout ); # Round up to next milisecond to avoid zero timeouts @@ -234,6 +238,8 @@ my $self = shift; my %params = @_; + $self->post_fork if $self->{pid} != $$; + my $epoll = $self->{epoll}; $self->__watch_io( %params ); @@ -319,6 +325,8 @@ my $self = shift; my %params = @_; + $self->post_fork if $self->{pid} != $$; + $self->__unwatch_io( %params ); my $epoll = $self->{epoll}; @@ -413,6 +421,26 @@ sigprocmask( SIG_UNBLOCK, POSIX::SigSet->new( $signum ) ); } +sub post_fork +{ + my $self = shift; + + $self->{epoll} = Linux::Epoll->new; + $self->{pid} = $$; + + my $watches = $self->{iowatches} or return; + + foreach my $watch ( values %$watches ) { + my ( $handle, $on_read_ready, $on_write_ready, $on_hangup ) = @$watch; + $self->watch_io( + handle => $handle, + on_read_ready => $on_read_ready, + on_write_ready => $on_write_ready, + on_hangup => $on_hangup, + ); + } +} + =head1 SEE ALSO =over 4 === added file 't/10loop-fork.t' --- t/10loop-fork.t 1970-01-01 00:00:00 +0000 +++ t/10loop-fork.t 2018-06-25 16:03:40 +0000 @@ -0,0 +1,53 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use Test::More; +use IO::Async::OS; +use IO::Async::Loop::Epoll; + +plan skip_all => "Cannot fork" unless IO::Async::OS->HAVE_POSIX_FORK; + +my $loop = IO::Async::Loop::Epoll->new; + +my @kids = map { + defined( my $pid = fork ) or die "Cannot fork() - $!"; + if( $pid ) { + $pid; + } + else { + test_in_child(); + exit 0; + } +} 1 .. 3; + +sub test_in_child +{ + my ( $rd, $wr ) = IO::Async::OS->pipepair; + + my $readable; + + $loop->watch_io( + handle => $rd, + on_read_ready => sub { $readable++ }, + ); + + sleep 1; + + $wr->autoflush; + $wr->print( "HELLO\n" ); + + my $count = 5; + + $loop->loop_once( 0.1 ) until $readable or !$count--; + + die "[$$] FAILED\n" if !$readable; +} + +foreach my $kid ( @kids ) { + waitpid $kid, 0; + is( $?, 0, "Child $kid exited OK" ); +} + +done_testing;
Fixed in 0.19 -- Paul Evans