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+
41+ # This should hit EAGAIN in io_write_loop and wait
42+ # Bug: waits for READABLE instead of WRITABLE
43+ @selector . io_write ( Fiber . current , local , buffer , buffer . size )
44+ write_completed = true
45+ end
46+
47+ # Start writer - should yield back when hitting EAGAIN
48+ writer . transfer
49+
50+ # Writer should be stuck waiting (either for right or wrong event)
51+ expect ( writer . alive? ) . to be == true
52+ expect ( write_completed ) . to be == false
53+
54+ # Drain some data to make socket writable
55+ remote . read_nonblock ( 2048 )
56+
57+ # Give selector multiple chances to process writable event.
58+ # With fix: writer should wake up and complete.
59+ # With bug: writer stays stuck because it's waiting for READABLE.
60+ timeout_count = 0
61+ while writer . alive? && timeout_count < 10
62+ @selector . select ( 0.1 ) # Short intervals for responsiveness, many iterations for tolerance
63+ timeout_count += 1
64+ end
65+
66+ expect ( write_completed ) . to be == true
67+ expect ( writer ) . not . to be ( :alive? )
68+ ensure
69+ local . close rescue nil
70+ remote . close rescue nil
71+ end
72+ end
73+ end
74+
75+ # Test all available selectors
76+ IO ::Event ::Selector . constants . each do |name |
77+ klass = IO ::Event ::Selector . const_get ( name )
78+
79+ describe ( klass , unique : name ) do
80+ def before
81+ @loop = Fiber . current
82+ @selector = subject . new ( @loop )
83+ end
84+
85+ def after ( error = nil )
86+ @selector &.close
87+ end
88+
89+ attr :loop
90+ attr :selector
91+
92+ it_behaves_like WriteDeadlock
93+ end
94+ end
0 commit comments