Skip to content

fix: Keep polling for Connect's port until the timeout - #75

Merged
nealrichardson merged 4 commits into
mainfrom
fix-port-poll
Oct 23, 2026
Merged

nealrichardson merged 4 commits into
mainfrom
fix-port-poll

Conversation

@nealrichardson

Copy link
Copy Markdown
Collaborator

Fixes #57.

Problem

is_port_open() made one TCP connect right after the container started. On Docker Desktop the published host port can take a fraction of a second to bind after containers.run() returns. During that window the connect is refused at once, so with-connect reported "Posit Connect did not start within 60 seconds" after a few milliseconds, then stopped a container that was starting normally. The timeout=60.0 only limited how long a single connect attempt could take. It didn't make with-connect keep trying.

Fix

  • is_port_open() retries every 0.25 seconds (poll_interval) until the timeout expires. Each attempt's connect timeout is capped at the time left. After a failed attempt it sleeps for whichever is shorter, the poll interval or the time left, so it keeps trying right up to the deadline.
  • is_port_open() takes an optional container. On each attempt it calls container.reload() and stops waiting if the status is exited or dead, so a Connect that crashes on startup doesn't wait the full 60 seconds. The call site passes the container and raises "Posit Connect container exited before port N opened." in that case. A timeout still raises the "did not start within 60 seconds" error.

Verification

Run locally on macOS with Docker Desktop and ghcr.io/posit-dev/connect:latest:

  • uv run main.py --license README.md --image ghcr.io/posit-dev/connect:latest --port 43821 -- true got past "Waiting for port 43821 to open...", logged "Running Posit Connect v2026.10.0", and failed as expected with "Unable to obtain a valid license" (README.md stands in for a license, since no valid one was available).
  • The race didn't reproduce on this machine with the unfixed code; containers.run() returned after the port was already bound. To exercise it, a script polled a port and started the container 2 seconds later with docker run -p:
    • A single connect (the old behavior) got ConnectionRefusedError after 0.001s.
    • The new is_port_open() returned True after 2.3s and 10 attempts.
    • With a container whose entrypoint is false, is_port_open(..., container=c) returned False right away (status exited) instead of waiting 60s.

Tests

Added after test_extract_server_version_dev and registered in the __main__ block:

  • test_is_port_open_retries_until_port_opens: two refused connects, then a success, gives True after 3 attempts.
  • test_is_port_open_times_out_on_closed_port: a closed local port gives False after the full 0.5s timeout, not right away.
  • test_is_port_open_stops_when_container_exits: an exited container gives False without trying to connect.

uv run test_main.py and uv run --with pytest pytest -q pass.

Merge conflicts with other open PRs

🤖 Generated with Claude Code

nealrichardson and others added 4 commits October 23, 2026 10:03
is_port_open() made a single TCP connect right after the container
started. On Docker Desktop the published port can take a fraction of a
second to bind, so the connect was refused instantly and with-connect
reported "did not start within 60 seconds" in milliseconds.

Retry the connection every 0.25 seconds until the deadline. While
waiting, check the container status and stop early if it has exited,
reporting that the container exited instead of a timeout.

Fixes #57.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Avoids a merge conflict with #72, which adds tests after
test_valid_license_http_server_starts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

is_port_open single-shot check races Docker Desktop port binding → container SIGKILLed, misleading "did not start within 60 seconds"

1 participant