Skip to content

Commit 0d9710e

Browse files
committed
Open file with blocking: nil or false in another thread
We can't reliably detect without significant overhead whether the file at *path* might block for a while before calling open; while O_NONBLOCK has no effect on regular disk files, special file types are a different story. Open with O_NONBLOCK will fail with ENXIO for O_WRONLY (no connected reader) but it will always succeed for O_RDONLY (regardless of a connected writer or not), then any attempt to read will return EOF, leaving no means to wait until a writer connects. We thus rely on the *blocking* arg: when false the file might be a special file type, so we check it; if it's a fifo (named pipe) or a character device, we open in another thread so we don't risk blocking the current thread (and thus other fibers) until a reader or writer is also connected. We need preview_mt to safely re-enqueue the current fiber from the thread.
1 parent 2d1742b commit 0d9710e

File tree

4 files changed

+101
-16
lines changed

4 files changed

+101
-16
lines changed

spec/std/file_spec.cr

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -94,19 +94,25 @@ describe "File" do
9494
ret = LibC.mkfifo(path, File::DEFAULT_CREATE_PERMISSIONS)
9595
raise RuntimeError.from_errno("mkfifo") unless ret == 0
9696

97-
# FIXME: open(2) will block when opening a fifo file until another
98-
# thread or process also opened the file; we should pass
99-
# O_NONBLOCK to the open(2) call itself, not afterwards
100-
file = nil
101-
new_thread { file = File.new(path, "w", blocking: nil) }
97+
# open(2) blocks when opening a pipe/fifo or chardev file until
98+
# another thread or process also opened the file in the opposite mode
99+
w = nil
100+
{% if flag?(:preview_mt) %}
101+
# the evloop handles opening the fifo file without blocking the
102+
# current thread (only the current fiber)
103+
spawn { w = File.open(path, "w", blocking: nil) }
104+
{% else %}
105+
# we must explicitly spawn a thread
106+
new_thread { w = File.new(path, "w") }
107+
{% end %}
102108

103109
begin
104-
File.open(path, "r", blocking: false) do |file|
105-
file.blocking.should be_false
110+
File.open(path, "r", blocking: false) do |r|
111+
r.blocking.should be_false
106112
end
107113
ensure
108114
File.delete(path)
109-
file.try(&.close)
115+
w.try(&.close)
110116
end
111117
end
112118
{% end %}

src/crystal/event_loop/libevent.cr

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,37 @@ class Crystal::EventLoop::LibEvent < Crystal::EventLoop
111111
def open(path : String, flags : Int32, permissions : File::Permissions, blocking : Bool?) : {System::FileDescriptor::Handle, Bool} | Errno
112112
path.check_no_null_byte
113113

114-
fd = LibC.open(path, flags | LibC::O_CLOEXEC, permissions)
115-
return Errno.value if fd == -1
114+
fd = 0
115+
flags |= LibC::O_CLOEXEC
116+
blocking = !System::File.special_type?(path) if blocking.nil?
117+
118+
# We can't reliably detect without significant overhead whether the file at
119+
# *path* might block for a while before calling open; while O_NONBLOCK has
120+
# no effect on regular disk files, special file types are a different story.
121+
# Open with O_NONBLOCK will fail with ENXIO for O_WRONLY (no connected
122+
# reader) but it will always succeed for O_RDONLY (regardless of a connected
123+
# writer or not), then any attempt to read will return EOF, leaving no means
124+
# to wait until a writer connects.
125+
#
126+
# We thus rely on the *blocking* arg: when false the file might be a special
127+
# file type, so we check it; if it's a fifo (named pipe) or a character
128+
# device, we open in another thread so we don't risk blocking the current
129+
# thread (and thus other fibers) until a reader or writer is also connected.
130+
#
131+
# We need preview_mt to safely re-enqueue the current fiber from the thread.
132+
{% if flag?(:preview_mt) && !flag?(:interpreted) %}
133+
if blocking
134+
fd = LibC.open(path, flags, permissions)
135+
return Errno.value if fd == -1
136+
else
137+
fd, errno = System::File.async_open(path, flags, permissions)
138+
return errno if fd == -1
139+
end
140+
{% else %}
141+
fd = LibC.open(path, flags, permissions)
142+
return Errno.value if fd == -1
143+
{% end %}
116144

117-
blocking = !System::File.special_type?(fd) if blocking.nil?
118145
unless blocking
119146
status_flags = System::FileDescriptor.fcntl(fd, LibC::F_GETFL)
120147
System::FileDescriptor.fcntl(fd, LibC::F_SETFL, status_flags | LibC::O_NONBLOCK)

src/crystal/event_loop/polling.cr

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,37 @@ abstract class Crystal::EventLoop::Polling < Crystal::EventLoop
157157
def open(path : String, flags : Int32, permissions : File::Permissions, blocking : Bool?) : {System::FileDescriptor::Handle, Bool} | Errno
158158
path.check_no_null_byte
159159

160-
fd = LibC.open(path, flags | LibC::O_CLOEXEC, permissions)
161-
return Errno.value if fd == -1
160+
fd = 0
161+
flags |= LibC::O_CLOEXEC
162+
blocking = !System::File.special_type?(path) if blocking.nil?
163+
164+
# We can't reliably detect without significant overhead whether the file at
165+
# *path* might block for a while before calling open; while O_NONBLOCK has
166+
# no effect on regular disk files, special file types are a different story.
167+
# Open with O_NONBLOCK will fail with ENXIO for O_WRONLY (no connected
168+
# reader) but it will always succeed for O_RDONLY (regardless of a connected
169+
# writer or not), then any attempt to read will return EOF, leaving no means
170+
# to wait until a writer connects.
171+
#
172+
# We thus rely on the *blocking* arg: when false the file might be a special
173+
# file type, so we check it; if it's a fifo (named pipe) or a character
174+
# device, we open in another thread so we don't risk blocking the current
175+
# thread (and thus other fibers) until a reader or writer is also connected.
176+
#
177+
# We need preview_mt to safely re-enqueue the current fiber from the thread.
178+
{% if flag?(:preview_mt) && !flag?(:interpreted) %}
179+
if blocking
180+
fd = LibC.open(path, flags, permissions)
181+
return Errno.value if fd == -1
182+
else
183+
fd, errno = System::File.async_open(path, flags, permissions)
184+
return errno if fd == -1
185+
end
186+
{% else %}
187+
fd = LibC.open(path, flags, permissions)
188+
return Errno.value if fd == -1
189+
{% end %}
162190

163-
blocking = !System::File.special_type?(fd) if blocking.nil?
164191
unless blocking
165192
status_flags = System::FileDescriptor.fcntl(fd, LibC::F_GETFL)
166193
System::FileDescriptor.fcntl(fd, LibC::F_SETFL, status_flags | LibC::O_NONBLOCK)

src/crystal/system/unix/file.cr

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,37 @@ module Crystal::System::File
1414
end
1515
end
1616

17+
{% if flag?(:preview_mt) && !flag?(:interpreted) %}
18+
protected def self.async_open(filename, flags, permissions)
19+
fd = -1
20+
errno = Errno::NONE
21+
fiber = ::Fiber.current
22+
23+
::Thread.new do
24+
fd = LibC.open(filename, flags, permissions)
25+
errno = Errno.value if fd == -1
26+
27+
{% if flag?(:execution_context) %}
28+
fiber.enqueue
29+
{% else %}
30+
# HACK: avoid Fiber#enqueue because it would lazily create a scheduler
31+
# for the thread just to send the fiber to another scheduler
32+
fiber.get_current_thread.not_nil!.scheduler.send_fiber(fiber)
33+
{% end %}
34+
end
35+
::Fiber.suspend
36+
37+
{fd, errno}
38+
end
39+
{% end %}
40+
1741
protected def system_init(mode : String, blocking : Bool) : Nil
1842
end
1943

20-
def self.special_type?(fd)
44+
def self.special_type?(path : String) : Bool
45+
path.check_no_null_byte
2146
stat = uninitialized LibC::Stat
22-
ret = fstat(fd, pointerof(stat))
47+
ret = stat(path, pointerof(stat))
2348
ret != -1 && (stat.st_mode & LibC::S_IFMT).in?(LibC::S_IFCHR, LibC::S_IFIFO)
2449
end
2550

0 commit comments

Comments
 (0)