1+ # frozen_string_literal: true
2+
3+ # Released under the MIT License.
4+ # Copyright, 2026, by Samuel Williams.
5+
6+ require "io/event"
7+ require "io/event/selector"
8+ require "socket"
9+
10+ WriteDeadlock = Sus ::Shared ( "write deadlock" ) do
11+ with "a pipe that fills up" do
12+ it "should not deadlock when waiting for writable" do
13+ # Skip on Windows which doesn't have the same socket behavior
14+ skip_if_ruby_platform ( /mswin|mingw|cygwin/ )
15+
16+ # Use UNIXSocket pair for more predictable behavior
17+ local , remote = UNIXSocket . pair ( :STREAM )
18+
19+ # Set small buffer to encourage EAGAIN
20+ local . setsockopt ( Socket ::SOL_SOCKET , Socket ::SO_SNDBUF , 4096 )
21+ remote . setsockopt ( Socket ::SOL_SOCKET , Socket ::SO_RCVBUF , 4096 )
22+
23+ eagain_hit = false
24+ write_completed = false
25+
26+ # Fill buffer until we actually hit EAGAIN
27+ begin
28+ chunk = "X" * 1024 # 1KB chunks
29+ 100 . times { local . write_nonblock ( chunk ) } # Write up to 100KB
30+ rescue IO ::WaitWritable
31+ eagain_hit = true
32+ end
33+
34+ # Skip test if we can't create EAGAIN condition
35+ skip "Could not trigger EAGAIN condition" unless eagain_hit
36+
37+ # Writer fiber that should hit EAGAIN and wait for WRITABLE
38+ writer = Fiber . new do
39+ buffer = IO ::Buffer . for ( "test" * 64 ) # 256 bytes
40+ @selector . io_write ( Fiber . current , local , buffer , buffer . size )
41+ write_completed = true
42+ end
43+
44+ # Start writer - should yield back when hitting EAGAIN
45+ writer . transfer
46+
47+ # Writer should be stuck waiting (either for right or wrong event)
48+ expect ( writer . alive? ) . to be == true
49+ expect ( write_completed ) . to be == false
50+
51+ # Drain some data to make socket writable
52+ remote . read_nonblock ( 2048 )
53+
54+ # Give selector multiple chances to process writable event.
55+ # With fix: writer should wake up and complete.
56+ # With bug: writer stays stuck because it's waiting for READABLE.
57+ timeout_count = 0
58+ while writer . alive? && timeout_count < 10
59+ @selector . select ( 1.0 ) # Short intervals for responsiveness, many iterations for tolerance
60+ timeout_count += 1
61+ end
62+
63+ expect ( write_completed ) . to be == true
64+ expect ( writer ) . not . to be ( :alive? )
65+ ensure
66+ local . close rescue nil
67+ remote . close rescue nil
68+ end
69+ end
70+ end
71+
72+ # Test all available selectors
73+ IO ::Event ::Selector . constants . each do |name |
74+ klass = IO ::Event ::Selector . const_get ( name )
75+
76+ describe ( klass , unique : name ) do
77+ def before
78+ @loop = Fiber . current
79+ @selector = subject . new ( @loop )
80+ end
81+
82+ def after ( error = nil )
83+ @selector &.close
84+ end
85+
86+ attr :loop
87+ attr :selector
88+
89+ it_behaves_like WriteDeadlock
90+ end
91+ end
0 commit comments