Don't continually ask the OS for a port, raise an exception (#13)
Don't continually ask the OS for a port, raise an exception instead of
returning None if no port could be found from `pick_unused_port()`.
On very busy machines, we might not be able to get a port from the OS
that is free on both TCP and UDP. Rather than spinning forever, only try
a handful of times.
diff --git a/src/portpicker.py b/src/portpicker.py
index c800161..cd92952 100644
--- a/src/portpicker.py
+++ b/src/portpicker.py
@@ -61,6 +61,11 @@
_random_ports = set()
+class NoFreePortFoundException(Exception):
+ """Exception indicating that no free port could be found."""
+ pass
+
+
def add_reserved_port(port):
"""Add a port that was acquired by means other than the port server."""
_free_ports.add(port)
@@ -148,6 +153,9 @@
Returns:
A port number that is unused on both TCP and UDP.
+
+ Raises:
+ NoFreePortFoundException: No free port could be found.
"""
if _free_ports:
port = _free_ports.pop()
@@ -177,7 +185,10 @@
should not be called by code outside of this module.
Returns:
- A port number that is unused on both TCP and UDP. None on error.
+ A port number that is unused on both TCP and UDP.
+
+ Raises:
+ NoFreePortFoundException: No free port could be found.
"""
# Try random ports first.
rng = random.Random()
@@ -187,10 +198,10 @@
_random_ports.add(port)
return port
- # Try OS-assigned ports next.
+ # Next, try a few times to get an OS-assigned port.
# Ambrose discovered that on the 2.6 kernel, calling Bind() on UDP socket
# returns the same port over and over. So always try TCP first.
- while True:
+ for _ in range(10):
# Ask the OS for an unused port.
port = bind(0, _PROTOS[0][0], _PROTOS[0][1])
# Check if this port is unused on the other protocol.
@@ -198,6 +209,9 @@
_random_ports.add(port)
return port
+ # Give up.
+ raise NoFreePortFoundException()
+
def get_port_from_port_server(portserver_address, pid=None):
"""Request a free a port from a system-wide portserver.
diff --git a/src/tests/portpicker_test.py b/src/tests/portpicker_test.py
index b6ac959..8220ca0 100644
--- a/src/tests/portpicker_test.py
+++ b/src/tests/portpicker_test.py
@@ -201,10 +201,17 @@
return None
with mock.patch.object(portpicker, 'bind', bind_with_error):
+ got_at_least_one_port = False
for _ in range(100):
- port = portpicker._pick_unused_port_without_server()
- self.assertTrue(self.IsUnusedTCPPort(port))
- self.assertTrue(self.IsUnusedUDPPort(port))
+ try:
+ port = portpicker._pick_unused_port_without_server()
+ except portpicker.NoFreePortFoundException:
+ continue
+ else:
+ got_at_least_one_port = True
+ self.assertTrue(self.IsUnusedTCPPort(port))
+ self.assertTrue(self.IsUnusedUDPPort(port))
+ self.assertTrue(got_at_least_one_port)
def testIsPortFree(self):
"""This might be flaky unless this test is run with a portserver."""