Skip to content

fix(utils): report port-scan exhaustion instead of returning a busy port - #10264

Open
Lesereingrape wants to merge 1 commit into
modelscope:mainfrom
Lesereingrape:fix/find-free-port-returns-busy
Open

Lesereingrape wants to merge 1 commit into
modelscope:mainfrom
Lesereingrape:fix/find-free-port-returns-busy

Conversation

@Lesereingrape

Copy link
Copy Markdown
Contributor

PR type

  • Bug Fix
  • New Feature
  • Document Updates
  • More Models or Datasets Support

PR information

Closes #10263

find_free_port() reused its loop variable as the return value, so when every bind inside
[start_port, start_port + retry) failed the function returned the last candidate that had just
raised
EADDRINUSE / WinError 10048 — a busy port reported as free:

for port in range(start_port, start_port + retry):
    ...
        except OSError:
            pass
return port          # == start_port + retry - 1, which is known to be taken

DeployArguments.__post_init__ (swift/arguments/deploy_args.py:57) and AppArguments.__post_init__
(swift/arguments/app_args.py:50) assign that value straight to the server port, so swift deploy /
swift app on an occupied port window come up configured for a port that is not theirs and only fail
later inside uvicorn/gradio, with an error that no longer mentions the port scan. The sibling path
retry <= 0 never runs the loop body, so return port raises
UnboundLocalError: cannot access local variable 'port'.

The fix keeps the successful path identical — return the port as soon as the bind succeeds, which is
also how the repository's own private helper _find_free_port()
(swift/ray/megatron/checkpoint_engine/base.py:21) is written — and turns an exhausted window into the
error a caller would already have received from socket.bind, now naming the range that was tried:

             try:
                 sock.bind(('', port))
-                port = sock.getsockname()[1]
-                break
+                return sock.getsockname()[1]
             except OSError:
                 pass
-    return port
+    raise OSError(f'No free port in the range [{start_port}, {start_port + retry}).')

tests/utils/test_find_free_port.py covers both broken paths and pins three controls (the default
ephemeral call, a free explicitly requested port, and a partially used window) so the happy path cannot
change silently. The tests only use socket; no model, GPU or network access is needed.

Prior art: #9870 went in the same direction and was closed unmerged by its own author on 2026-08-12
without maintainer review. This is an independent minimal change plus tests; it deliberately does not
add the extra start_port validation that PR carried.

Disclosure: an AI agent prepared and submitted this PR on the account of @Lesereingrape; every number
below is measured output, and no human reviewed the diff before submission.

Experiment results

python -m unittest tests.utils.test_find_free_port -v on Windows 11 / Python 3.12, same interpreter
for both legs:

  • base dbc395ff: Ran 5 tests ... FAILED (failures=1, errors=1) —
    test_exhausted_range_does_not_report_a_busy_port fails with AssertionError: OSError not raised
    (the call returns the busy last candidate), and test_zero_retry_raises_oserror errors with
    UnboundLocalError: cannot access local variable 'port' where it is not associated with a value.
    The three control tests already pass, which is what pins the unchanged happy path.
  • with this patch: Ran 5 tests ... OK.

Direct symptom check on base, occupying 47110-47112 and asking for a free port in that window:
find_free_port(47110, retry=3) returned 47112, and binding the returned port raised
OSError [WinError 10048].

Parity for the module directory, same interpreter in both legs
(python -m unittest discover -s tests/utils -p "test_*.py" -t .):

leg result
base dbc395ff Ran 162 tests in 319.987s / FAILED (failures=2, errors=7, skipped=22)
with this patch Ran 167 tests in 348.006s / FAILED (failures=2, errors=7, skipped=22)

The +5 are exactly the five tests added here and the counts are unchanged; the sorted lists of the 9
failing test ids are identical (diff is empty), so nothing that passed on base fails here. All 9 are
local-environment cases that already fail on base: 5 test modules do not import for optional trainer
dependencies (msgspec, and pytest for one of them), 2 need sklearn, and 2 are Windows filesystem
behaviour (test_copy_files on a nested directory copy, test_last_checkpoint_symlink where
os.symlink is unavailable without the symlink privilege).

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] find_free_port() reports the last busy port as free when its scan window is exhausted

1 participant