Skip to content

Backport(v1.19): test: rework unused_port_tcp_udp to try random candidate ports (#5427) - #5434

Merged
kenhys merged 1 commit into
v1.19from
backport-pr5427
Jul 13, 2026
Merged

Backport(v1.19): test: rework unused_port_tcp_udp to try random candidate ports (#5427)#5434
kenhys merged 1 commit into
v1.19from
backport-pr5427

Conversation

@Watson1978

Copy link
Copy Markdown
Contributor

Which issue(s) this PR fixes:
Backport #5427
Fixes #

What this PR does / why we need it:
unused_port_tcp_udp() collected 200 TCP ephemeral ports at once and returned the first one that could also be bound for UDP. On Windows, Hyper-V/WinNAT/HNS reserve "excluded port ranges" inside the ephemeral port range (49152-65535), the reservations are managed separately for TCP and UDP, and binding a port inside such a range fails with EACCES instead of EADDRINUSE.
The TCP ephemeral allocator assigns ports sequentially and knows nothing about UDP exclusions, so the 200 candidates form a contiguous block that can land entirely inside a UDP-only excluded range. When that happens, every candidate fails the UDP bind check and test setup fails with:

  RuntimeError: can't find unused port
  test/helper.rb:94:in `unused_port_tcp_udp'
  test/helper.rb:81:in `unused_port'
  test/plugin/test_in_forward.rb:22:in `setup'

This was captured on a windows-2025 GitHub Actions runner whose UDP exclusion ranges (netsh interface ipv4 show excludedportrange protocol=udp) were 54315-54414 and 54415-54514: exactly 200 consecutive ports, while the TCP exclusions were elsewhere (49691-49890).

Instead, draw random candidate ports from a fixed high range and verify that each can be bound for both TCP and UDP, trying another candidate on failure. Because the candidates are independent draws, reserved or in-use ports only matter through their overall fraction of the range: contiguous reservation blocks cannot defeat the search, and the chance of exhausting the retry limit is negligible. The range is kept wide (10001 ports) so that reservation blocks, which come in units of about 100-200 ports, can only ever cover a small fraction of it.

This also removes the need to hold 200 sockets at once, which the old implementation capped because of file descriptor limits, and the checks now bind both protocols explicitly on the same IPv4 wildcard address.

Docs Changes:
N/A

Release Note:
N/A

**Which issue(s) this PR fixes**:
Fixes #

**What this PR does / why we need it**:
`unused_port_tcp_udp()` collected 200 TCP ephemeral ports at once and
returned the first one that could also be bound for UDP.
On Windows, Hyper-V/WinNAT/HNS reserve "excluded port ranges" inside the
ephemeral port range (49152-65535), the reservations are managed
separately for TCP and UDP, and binding a port inside such a range fails
with `EACCES` instead of `EADDRINUSE`.
The TCP ephemeral allocator assigns ports sequentially and knows nothing
about UDP exclusions, so the 200 candidates form a contiguous block that
can land entirely inside a UDP-only excluded range. When that happens,
every candidate fails the UDP bind check and test setup fails with:

```
  RuntimeError: can't find unused port
  test/helper.rb:94:in `unused_port_tcp_udp'
  test/helper.rb:81:in `unused_port'
  test/plugin/test_in_forward.rb:22:in `setup'
```

This was captured on a windows-2025 GitHub Actions runner whose UDP
exclusion ranges (netsh interface ipv4 show excludedportrange
protocol=udp) were 54315-54414 and 54415-54514: exactly 200 consecutive
ports, while the TCP exclusions were elsewhere (49691-49890).

Instead, draw random candidate ports from a fixed high range and verify
that each can be bound for both TCP and UDP, trying another candidate on
failure. Because the candidates are independent draws, reserved or
in-use ports only matter through their overall fraction of the range:
contiguous reservation blocks cannot defeat the search, and the chance
of exhausting the retry limit is negligible. The range is kept wide
(10001 ports) so that reservation blocks, which come in units of about
100-200 ports, can only ever cover a small fraction of it.

This also removes the need to hold 200 sockets at once, which the old
implementation capped because of file descriptor limits, and the checks
now bind both protocols explicitly on the same IPv4 wildcard address.

**Docs Changes**:
N/A

**Release Note**:
N/A

---------

Signed-off-by: Shizuo Fujita <fujita@clear-code.com>
Co-authored-by: Kentaro Hayashi <kenhys@gmail.com>
Signed-off-by: Shizuo Fujita <fujita@clear-code.com>
@Watson1978 Watson1978 added the CI Test/CI issues label Jul 13, 2026
@Watson1978 Watson1978 added this to the v1.19.4 milestone Jul 13, 2026
@Watson1978
Watson1978 requested a review from kenhys July 13, 2026 06:50
@kenhys
kenhys merged commit 60c50a9 into v1.19 Jul 13, 2026
19 checks passed
@kenhys
kenhys deleted the backport-pr5427 branch July 13, 2026 07:45
kenhys pushed a commit that referenced this pull request Jul 27, 2026
**Which issue(s) this PR fixes**: 
Fixes #

**What this PR does / why we need it**: 
On Windows, Hyper-V/WinNAT/HNS take their reservations from the dynamic
port range, which is 49152-65535 as observed on GitHub Actions runners,
and a reservation can appear between the check and the actual bind.

When searching for unused ports, this PR will avoid this range to
prevent following error.

```
3) Error: test: send_with_time_as_integer(ForwardOutputTest): Errno::EACCES: Permission denied - bind(2) for 127.0.0.1:58707
C:/hostedtoolcache/windows/Ruby/4.0.5/arm64/lib/ruby/4.0.0/socket.rb:183:in 'Socket#bind'
C:/hostedtoolcache/windows/Ruby/4.0.5/arm64/lib/ruby/4.0.0/socket.rb:183:in 'Addrinfo#bind'
C:/a/fluentd/fluentd/lib/fluent/plugin_helper/server.rb:402:in 'Fluent::PluginHelper::Server#server_create_udp_socket'
C:/a/fluentd/fluentd/lib/fluent/plugin_helper/server.rb:178:in 'Fluent::PluginHelper::Server#server_create'
C:/a/fluentd/fluentd/lib/fluent/plugin/in_forward.rb:192:in 'Fluent::Plugin::ForwardInput#start'
C:/a/fluentd/fluentd/lib/fluent/test/driver/base.rb:120:in 'Fluent::Test::Driver::Base#instance_start'
C:/a/fluentd/fluentd/lib/fluent/test/driver/base.rb:78:in 'Fluent::Test::Driver::Base#run'
C:/a/fluentd/fluentd/lib/fluent/test/driver/base_owner.rb:130:in 'Fluent::Test::Driver::BaseOwner#run'
C:/a/fluentd/fluentd/test/plugin/test_out_forward.rb:516:in 'block (2 levels) in <class:ForwardOutputTest>'
C:/hostedtoolcache/windows/Ruby/4.0.5/arm64/lib/ruby/gems/4.0.0/gems/test-unit-rr-1.0.5/lib/test/unit/rr.rb:98:in 'Test::Unit::RR::Adapter#assert_rr'
C:/a/fluentd/fluentd/test/plugin/test_out_forward.rb:515:in 'block in <class:ForwardOutputTest>'
```

https://github.com/fluent/fluentd/actions/runs/29884012909/job/88810722450#step:6:5337

Related to #5434

**Docs Changes**:
N/A

**Release Note**: 
N/A

Signed-off-by: Shizuo Fujita <fujita@clear-code.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI Test/CI issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants