Skip to content

Commit 51abed4

Browse files
Fix write deadlocks.
1 parent 3422cfa commit 51abed4

3 files changed

Lines changed: 100 additions & 2 deletions

File tree

lib/io/event/selector/select.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ def io_write(fiber, io, buffer, length, offset = 0)
230230

231231
if result < 0
232232
if length > 0 and again?(result)
233-
self.io_wait(fiber, io, IO::READABLE)
233+
self.io_wait(fiber, io, IO::WRITABLE)
234234
else
235235
return result
236236
end
@@ -287,7 +287,7 @@ def io_write(fiber, io, buffer, length, offset = 0)
287287

288288
if again?(result)
289289
if length > 0
290-
self.io_wait(fiber, io, IO::READABLE)
290+
self.io_wait(fiber, io, IO::WRITABLE)
291291
else
292292
return result
293293
end

releases.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Releases
22

3+
## Unreleased
4+
5+
- Fix several implementation bugs that could cause deadlocks on blocking writes.
6+
37
## v1.14.0
48

59
### Enhanced `IO::Event::PriorityHeap` with deletion and bulk insertion methods
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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

Comments
 (0)