Skip to content

Instantly share code, notes, and snippets.

@jnthn

jnthn/hmmm.md Secret

Created August 3, 2016 14:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jnthn/ec19c88a592c44684ffafb41953ad25a to your computer and use it in GitHub Desktop.
Save jnthn/ec19c88a592c44684ffafb41953ad25a to your computer and use it in GitHub Desktop.
An issue with closing certain kinds of supplies

An issue with closing certain kinds of supplies

The problem

The async socket tests are fragile because they do something like this:

  1. Start listening on port P by tapping the listen supply, yielding tap T.
  2. Close tap T, so as to stop listening.
  3. 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.

The cheap test fix

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.

Is there a real world, non-testing use for addressing this?

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.

How might we solve it?

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 awaited 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.

What does Rx have to say about this?

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment