Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Arthur Axel 'fREW' Schmidt committed Oct 17, 2015
0 parents commit 6572ed3
Show file tree
Hide file tree
Showing 7 changed files with 293 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.build
Net-Async-EmptyPort-*
32 changes: 32 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
language: perl
perl:
- "5.10"
- "5.12"
- "5.14"
- "5.16"
- "5.18"
- "5.20"
- "5.22"
- "blead"

matrix:
allow_failures:
- perl: "blead"

before_install:
- git clone git://github.com/haarg/perl-travis-helper
- source perl-travis-helper/init
- build-perl
- perl -V

install:
- export RELEASE_TESTING=1 AUTOMATED_TESTING=1 AUTHOR_TESTING=1 HARNESS_OPTIONS=c HARNESS_TIMER=1
- cpanm --quiet --notest Devel::Cover::Report::Coveralls
- cpanm --quiet --notest --installdeps .

script:
- PERL5OPT=-MDevel::Cover=-coverage,statement,branch,condition,path,subroutine prove -lrsv t
- cover

after_success:
- cover -report coveralls
4 changes: 4 additions & 0 deletions Changes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Revision history for {{$dist->name}}

{{$NEXT}}
- Initial Release
8 changes: 8 additions & 0 deletions cpanfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
requires perl => '5.010';
requires Moo => 2;
requires Future => 0.33;
requires 'IO::Async' => 0.68;

on test => sub {
requires 'Test::More';
};
19 changes: 19 additions & 0 deletions dist.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name = Net-Async-EmptyPort
author = Arthur Axel "fREW" Schmidt <frioux+cpan@gmail.com>
license = Perl_5
copyright_holder = Arthur Axel "fREW" Schmidt
version = 0.001000

[NextRelease]
[@Git]
[@Basic]
[GithubMeta]
issues = 1

[MetaJSON]
[PodWeaver]
[PkgVersion]
[ReadmeFromPod]
[PodSyntaxTests]
[Prereqs::FromCPANfile]

193 changes: 193 additions & 0 deletions lib/Net/Async/EmptyPort.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package Net::Async::EmptyPort;

# ABSTRACT: Asynchronously wait for a port to open

use Moo;
use Future::Utils 'try_repeat_until_success', 'try_repeat';

has _loop => (
is => 'ro',
init_arg => 'loop',
required => 1,
handles => {
_connect => 'connect',
_listen => 'listen',
_delay => 'delay_future',
_timeout => 'timeout_future',
},
);

my %family_map = (
tcp => 'stream',
udp => 'dgram',
);
sub empty_port {
my ($self, $args) = @_;

$args //= {};
$args->{host} //= '127.0.0.1';
$args->{port} //= 0;
$args->{proto} //= 'tcp';

if ($args->{port} == 0) {
$self->_listen(
on_socket => sub {},
host => $args->{host},
socktype => $family_map{$args->{proto}},
service => $args->{port},
)
} else {
my $port = $args->{port};

try_repeat {
$self->_listen(
on_socket => sub {},
host => $args->{host},
socktype => $family_map{$args->{proto}},
service => $port++,
)
} while => sub {
!shift->is_done && $port < 65000
},
}
}

sub wait_port {
my ($self, $args) = @_;

die 'port is a required argument'
unless $args->{port};

$args->{host} //= '127.0.0.1';
$args->{proto} //= 'tcp';
$args->{max_wait} //= 10;

my $amount = 2;
my $attempt = 0;

my $f = try_repeat_until_success {
$self->_delay(
after => $amount * (2 ** $attempt++) - $amount,
)->then(sub {
$self->_connect(
host => $args->{host},
socktype => $family_map{$args->{proto}},
service => $args->{port},
)
})
};

$f = Future->wait_any(
$f,
$self->_timeout( after => $args->{max_wait} )
) if $args->{max_wait} > 0;

$f
}

1;

__END__
=pod
=head1 SYNOPSIS
use IO::Async::Loop;
use Net::Async::EmptyPort;
my $loop = IO::Async::Loop->new;
my $ep = Net::Async::EmptyPort->new(
loop => $loop,
);
# could take a while to start...
my $chosen_port = start_server_in_background();
$ep->wait_port({ port => $chosen_port })->get;
=head1 DESCRIPTION
This module is an asynchronous port of L<Net::EmptyPort>. The interface is
different and thus simplified from the original. A couple of the original
methods are not implemented; specifically C<can_bind> and C<check_port>. They
are not hard to implement but I don't have a good idea of why someone would use
them.
=head1 METHODS
=head2 empty_port
my $listen_future = $ep->empty_port({
host => '192.168.1.1',
port => 8000,
proto => 'tcp',
});
This method has no required arguments but accepts the following named
parameters:
=over
=item * C<host>
Defaults to C<127.0.0.1>
=item * C<port>
Defaults to C<0>; which means the kernel will immediately provide an open port.
Alternately, if you provide a port C<Net::Async::EmptyPort> will try that port
up through to port C<65000>.
=item * C<proto>
Defaults to C<tcp>; the other option is C<udp>.
=back
The return value is an L<IO::Async::Listener>. The easiest way (though this
will introduce a race condition) to make it work like the original is as
follows:
$ep->empty_port->then(sub { Future->done(shift->read_handle->sockport) })
Then the Future will simply contain the port, though a better option is to pass
the actual listener or socket to whatever will use it if possible.
=head2 wait_port
my $socket_future = $ep->wait_port({
port => 8080,
proto => 'tcp',
host => '192.168.1.1',
max_wait => 60,
});
This method takes the following named parameters:
=over
=item * C<host>
Defaults to C<127.0.0.1>
=item * C<port>
Required.
=item * C<proto>
Defaults to C<tcp>; the other option is C<udp>.
=item * C<max_wait>
Defaults to C<10> seconds. Set to C<-1> to wait indefinitely.
=back
The return value is a L<Future> containing an L<IP::Socket::IP>. You can use
that for connecting, but unlike L</empty_port> there is no race condition here
so it makes perfect sense to just use C<wait_port> as a "blocker."
C<wait_port> uses a basic exponential backoff to avoid quickly polling.
Eventually the backoff method will be configurable.
35 changes: 35 additions & 0 deletions t/basic.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use strict;

use Test::More;

use Net::Async::EmptyPort;

use IO::Async::Loop;

my $l = IO::Async::Loop->new;

ok(
my $ep = Net::Async::EmptyPort->new( loop => $l ),
'instantiation',
);

{
my $attempt = $ep->empty_port;
ok($attempt->get->read_handle->sockport, 'listened');
note('GOT PORT ' . $attempt->get->read_handle->sockport);
}

{
my $attempt = $ep->empty_port({ port => 50_000 });
ok($attempt->get->read_handle->sockport, 'listened');
note('GOT PORT ' . $attempt->get->read_handle->sockport);
}

my $listen = $ep->empty_port->get;

ok(
$ep->wait_port({ port => $listen->read_handle->sockport })->get,
'wait_port',
);

done_testing;

0 comments on commit 6572ed3

Please sign in to comment.