Skip Menu |

This queue is for tickets about the IO-Socket-IP CPAN distribution.

Report information
The Basics
Id: 92869
Status: rejected
Priority: 0/
Queue: IO-Socket-IP

People
Owner: Nobody in particular
Requestors: rjray [...] blackperl.com
Cc:
AdminCc:

Bug Information
Severity: Important
Broken in: 0.27
Fixed in: (no value)



Subject: IO::Socket::IP holds on to addresses beyond object destruction
Summary: When an object using an IO::Socket::IP-based socket object is destroyed, the address+port will appear to be available, but if a second object is created with the same address+port too quickly, it will fail with "Address already in use". I recently got a series of FAIL reports from cpantesters for my RPC-XML distribution. Most of the failures all had the same problem, and also the same characteristic; HTTP::Daemon was failing to bind to a given address, and the tester had installed Acme::Override::INET to monkey-patch IO::Socket::IP over IO::Socket::INET. I installed these two modules, and suddenly I too was getting the same errors. The errors were only occurring in places where I allocated more than one server object through the life of the test. It was always the second object that failed to instantiate, with the error message from HTTP::Daemon, "Unable to create HTTP::Daemon object: Address already in use". I found that if I forced the second instantiation to use a higher port number than the first one had, the tests then all passed. Note that at this time, the previous server object had been completely destroyed. Not just the HTTP::Daemon part of it, but the full object. To make it more interesting, I also found that once I patched my test, if I ran it twice in rapid succession the *first* object allocation would fail on the second test-run. So not only does IO::Socket::IP hold on to the address too long within a running process, apparently it also causes the address to be "wedged" for some time after the process has exited. In my test code, I have the following utility function that I use to find a free port on localhost: sub find_port { my $start_at = $_[0] || 9000; my ($port, $sock); for ($port = $start_at; $port < ($start_at + 2000); $port++) { $sock = IO::Socket->new(Domain => AF_INET, PeerAddr => 'localhost', PeerPort => $port); return $port unless ref $sock; } -1; } This code should return the first "available" port on localhost, that being the first port that IO::Socket->new() couldn't connect to. Without IO::Socket::IP, this code ends up using IO::Socket::INET and correctly returns an available port (both times it returns port 9000). But when IO::Socket::IP comes in to play, the second call returns port 9000 as being available, but when HTTP::Daemon tries to bind the address it fails, saying the address is already in use. I haven't been able to distill this down to a smaller example that exhibits the same failure, but the following steps should reproduce it: 1. Have Acme::Override::INET installed (as well as IO::Socket::IP, of course) 2. Download and untar RPC-XML-0.78.tar.gz. 3. cd into the directory and "perl Makefile.PL". 4. Do a "make". 5. Do "prove -b t/40_server.t" (you need the "-b", or other tests will fail). 6. Test will fail on or near line 446, because the second allocation of an RPC::XML::Server object failed. 7. Immediately re-run prove. 8. This time, it should fail on/near line 176, because the *first* allocation has failed this time, because the address is still considered in use. I will try to generate a smaller test script that exhibits the problem. If I can, I will update this ticket with the smaller test. Randy -- Randy J. Ray rjray@blackperl.com randy.j.ray@gmail.com
On Sun Feb 09 15:27:25 2014, RJRAY wrote: Show quoted text
> Summary: When an object using an IO::Socket::IP-based socket object is > destroyed, the address+port will appear to be available, but if a > second object is created with the same address+port too quickly, it > will fail with "Address already in use".
Yes; this has nothing to do with IO::Socket::IP or INET or Perl in general. This is the TCP linger time. See SO_LINGER etc, and the SO_REUSEADDR option. You likely just want to set the latter. -- Paul Evans
On Sun Feb 09 15:27:25 2014, RJRAY wrote: Show quoted text
> In my test code, I have the following utility function that I use to > find a free port on localhost: > > sub find_port > { > my $start_at = $_[0] || 9000; > > my ($port, $sock); > > for ($port = $start_at; $port < ($start_at + 2000); $port++) > { > $sock = IO::Socket->new(Domain => AF_INET, > PeerAddr => 'localhost', > PeerPort => $port); > return $port unless ref $sock; > } > > -1; > }
Also, why on earth do you need to do this? Just ask to bind to port 0, and the kernel will allocate you a free port. No need to guess one on the offchance you hit something free (which in any case is inherently racey) -- Paul Evans
On Mon Feb 10 10:01:09 2014, PEVANS wrote: Show quoted text
> Yes; this has nothing to do with IO::Socket::IP or INET or Perl in > general. This is the TCP linger time. > > See SO_LINGER etc, and the SO_REUSEADDR option. You likely just want > to set the latter.
This doesn't explain why the code passes all tests when IO::Socket::IP is not in the chain. If run *without* IO::Socket::IP standing in for IO::Socket::INET, then t/40_server.t will pass all tests without issue. Also, I am using HTTP::Daemon which creates the IO::Socket::INET object under the hood. So I'm not sure how I would apply SO_LINGER in this case? (Which, by the way, I cannot find "SO_LINGER" in any of the man pages for Socket, IO::Socket, or IO::Socket::INET, so I don't even know *how* I would apply it?) Randy -- Randy J. Ray rjray@blackperl.com randy.j.ray@gmail.com
On Mon Feb 10 10:02:52 2014, PEVANS wrote: Show quoted text
> Also, why on earth do you need to do this? Just ask to bind to port 0, > and the kernel will allocate you a free port. No need to guess one on > the offchance you hit something free (which in any case is inherently > racey)
The short answer: I suck at network programming. No, really, I always have. It's been a weakness of mine for years. Longer answer: I knew that this has a potential race condition, but I didn't know about binding to port 0 to get the kernel to allocate a free port. I just assumed that HTTP::Daemon (which will bind to a random free port if you don't pass a specific port) did the work of locating a free port for you. I'll fix my tests to use this instead of the port-searching. However, it's worth noting that the kernel *did* think this port was free-- the code I used detected this as a free port, it just then proceeded to fail once I tried to bind to it. Randy -- Randy J. Ray rjray@blackperl.com randy.j.ray@gmail.com
This morning I re-wrote parts of several of the tests to let the kernel choose a port randomly, instead of using my port-search routine. As a result, all the tests now pass with IO::Socket::IP taking the place of IO::Socket::INET. But that is because the consecutive server instance objects are no longer using the same port-- each time the kernel is assigning a new port number. Plus, there are two places where I still have to use my port-search code, so in theory the problem could still arise. I still don't know why the same code, running under the same version of Perl, succeeds with IO::Socket::INET but not with IO::Socket::IP. I still haven't been able to distill this down to a minimal test that exhibits the problem, unfortunately. Randy -- Randy J. Ray rjray@blackperl.com randy.j.ray@gmail.com
No activity; seems to be fixed. Closing. -- Paul Evans