The async socket tests are fragile because they do something like this:
- Start listening on port P by tapping the listen supply, yielding tap T.
- Close tap T, so as to stop listening.
- Start listening again on port P, by tapping the listen supply.
Closing the socket involves dispatching a message requesting it be closed. This operation completes asynchronously (both the dispatch to the event loop inside of MoarVM, and furthermore inside of libuv).
This introduces a race, which is why the test fails occasionally. If the close of the listening socket has not completed before we try to start listening again, the address is still in use and the test will fail as a result.
If we decide to change nothing inside of Perl 6, then the test could be fixed by simply using different port numbers for each of the test servers we bring up.
It would appear so, yes. Of note, consider we want a server to gracefully shut down on Ctrl+C, having finished processing outstanding connections. We might write something like this:
my $server = IO::Socket::Async.listen($hostname, $port).tap(...);
signal(SIGINT).tap({
$server.close;
exit;
});
Unfortunately, since close
returns immediately, we'd exit without the
socket having been shut down cleanly.
It's worth recapping the current implementation of Tap
, which is as follows:
my class Tap {
has &!on-close;
submethod BUILD(:&!on-close --> Nil) { }
method new(&on-close) {
self.bless(:&on-close)
}
method close() {
&!on-close() if &!on-close;
True;
}
}
So, close
simply does whatever &!on-close
wishes. In the case of listening
sockets, that is to dispatch the request to close, and immediately return.
One option is to start paying attention to the return value of &!on-close
.
Should it return a Promise
, this could be await
ed by &!on-close
:
method close() {
if &!on-close {
my $completion = &!on-close();
await $completion if $completion ~~ Promise;
}
True
}
Since, as of 6.d, await
will be non-blocking when done in the thread pool, this
would still render close a non-thread-blocking operation, while making it a synchronous
operation from the programmer point of view. (Its introduction today would have it
truly blocking.)
Making close
do an await
and changing IO::Socket::Async's usage of it to
return a Promise
would be a change of behavior from today. However, in the
sense that it would make an existing test in the 6.c suite pass reliably rather
than flap, it's actually a conformance improvement!
Also up for debate is whether we'd like to expose some kind of close-async
.
We've generally avoided such things, however, and since start $tap.close
is
the very same number of characters it's probably best to simply guide people
to write it in that direction. Composition for the win.
It's no secret that Perl 6 supplies were in many ways inspired by the Reactive Extensions, so it's worth looking at the Rx take.
Disposing an Observable can complete asynchronously in Rx also. Rx.Net re-used
the existing .Net IDisposable API, which makes a lot of sense in context and
was almost certainly the right choice. However, in C#, await
must go in an
async
method, implying refactoring up the whole call chain. The choice of
re-using IDisposable
precludes that option - and again, this is a quite
marginal use case.
It's worth noting that we have opportunities that the .Net Rx API designers
did not. In Perl 6, we can await
anywhere down the call chain. (In that
sense, start
/await
work very much like gather
/take
, where you can
take
as deep into the call chain as you want.) Thus, sticking an await
in our close
isn't a huge deal, and won't have to tie up an OS thread.