diff --git a/swift/utils/utils.py b/swift/utils/utils.py index f75a5146c1..64bbbd2d38 100644 --- a/swift/utils/utils.py +++ b/swift/utils/utils.py @@ -295,15 +295,20 @@ def find_node_ip() -> Optional[str]: def find_free_port(start_port: Optional[int] = None, retry: int = 100) -> int: if start_port is None: start_port = 0 - for port in range(start_port, start_port + retry): + if not 0 <= start_port <= 65535: + raise ValueError(f'Invalid start_port: {start_port}') + if retry < 1: + raise ValueError(f'Invalid retry: {retry}') + + stop_port = min(start_port + retry, 65536) + for port in range(start_port, stop_port): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: try: sock.bind(('', port)) - port = sock.getsockname()[1] - break except OSError: - pass - return port + continue + return sock.getsockname()[1] + raise OSError(f'No free port found in range [{start_port}, {stop_port})') def copy_files_by_pattern(source_dir, dest_dir, patterns, exclude_patterns=None): diff --git a/tests/utils/test_port_utils.py b/tests/utils/test_port_utils.py new file mode 100644 index 0000000000..d314614f25 --- /dev/null +++ b/tests/utils/test_port_utils.py @@ -0,0 +1,42 @@ +import unittest +from unittest.mock import call, patch + +from swift.utils import find_free_port + + +class TestFindFreePort(unittest.TestCase): + + @patch('swift.utils.utils.socket.socket') + def test_returns_next_available_port(self, socket_cls): + sock = socket_cls.return_value.__enter__.return_value + sock.bind.side_effect = [OSError('occupied'), None] + sock.getsockname.return_value = ('0.0.0.0', 30001) + + port = find_free_port(30000, retry=2) + + self.assertEqual(port, 30001) + self.assertEqual(sock.bind.call_args_list, [call(('', 30000)), call(('', 30001))]) + + @patch('swift.utils.utils.socket.socket') + def test_raises_when_candidate_range_is_exhausted(self, socket_cls): + sock = socket_cls.return_value.__enter__.return_value + sock.bind.side_effect = OSError('occupied') + + with self.assertRaisesRegex(OSError, r'\[30000, 30003\)'): + find_free_port(30000, retry=3) + + self.assertEqual(sock.bind.call_count, 3) + + @patch('swift.utils.utils.socket.socket') + def test_does_not_scan_past_max_port(self, socket_cls): + sock = socket_cls.return_value.__enter__.return_value + sock.bind.side_effect = OSError('occupied') + + with self.assertRaisesRegex(OSError, r'\[65535, 65536\)'): + find_free_port(65535, retry=2) + + sock.bind.assert_called_once_with(('', 65535)) + + +if __name__ == '__main__': + unittest.main()