Skip to content

Commit 9a0e95c

Browse files
committed
File: let the event loop decide the blocking mode
- The `blocking` args of File constructors now defaults to `nil`. - The polling evloops now set the fd to non blocking by default. - The IOCP evloop uses OVERLAPPED IO by default.
1 parent 34875ff commit 9a0e95c

File tree

6 files changed

+67
-127
lines changed

6 files changed

+67
-127
lines changed

spec/std/file_spec.cr

Lines changed: 43 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require "./spec_helper"
22
require "../support/thread"
3+
require "wait_group"
34

45
private def it_raises_on_null_byte(operation, file = __FILE__, line = __LINE__, end_line = __END_LINE__, &block)
56
it "errors on #{operation}", file, line, end_line do
@@ -47,112 +48,64 @@ describe "File" do
4748
end
4849
end
4950

50-
describe "blocking" do
51-
it "opens regular file as blocking" do
52-
with_tempfile("regular") do |path|
53-
File.open(path, "w") do |file|
54-
file.blocking.should be_true
55-
end
51+
{% if LibC.has_method?(:mkfifo) %}
52+
# interpreter doesn't support threads yet (#14287)
53+
pending_interpreted "can read/write fifo file without blocking" do
54+
path = File.tempname("chardev")
55+
ret = LibC.mkfifo(path, File::DEFAULT_CREATE_PERMISSIONS)
56+
raise RuntimeError.from_errno("mkfifo") unless ret == 0
5657

57-
File.open(path, "w", blocking: nil) do |file|
58-
file.blocking.should be_true
59-
end
60-
end
61-
end
58+
# FIXME: open(2) will block when opening a fifo file until another thread
59+
# or process also opened the file
60+
writer = nil
61+
new_thread { writer = File.new(path, "w") }
6262

63-
it "opens regular file as non-blocking" do
64-
with_tempfile("regular") do |path|
65-
File.open(path, "w", blocking: false) do |file|
66-
file.blocking.should be_false
67-
end
68-
end
69-
end
63+
rbuf = Bytes.new(5120)
64+
wbuf = Bytes.new(5120)
65+
Random::DEFAULT.random_bytes(wbuf)
7066

71-
{% if flag?(:unix) %}
72-
if File.exists?("/dev/tty")
73-
it "opens character device" do
74-
File.open("/dev/tty", "r") do |file|
75-
file.blocking.should be_true
76-
end
77-
78-
File.open("/dev/tty", "r", blocking: false) do |file|
79-
file.blocking.should be_false
80-
end
81-
82-
File.open("/dev/tty", "r", blocking: nil) do |file|
83-
file.blocking.should be_false
67+
File.open(path, "r") do |reader|
68+
WaitGroup.wait do |wg|
69+
wg.spawn do
70+
64.times do |i|
71+
reader.read_fully(rbuf)
72+
end
8473
end
85-
rescue File::Error
86-
# The TTY may not be available (e.g. Docker CI)
87-
end
88-
end
89-
90-
{% if LibC.has_method?(:mkfifo) %}
91-
# interpreter doesn't support threads yet (#14287)
92-
pending_interpreted "opens fifo file as non-blocking" do
93-
path = File.tempname("chardev")
94-
ret = LibC.mkfifo(path, File::DEFAULT_CREATE_PERMISSIONS)
95-
raise RuntimeError.from_errno("mkfifo") unless ret == 0
9674

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) }
102-
103-
begin
104-
File.open(path, "r", blocking: false) do |file|
105-
file.blocking.should be_false
75+
wg.spawn do
76+
64.times do |i|
77+
writer.not_nil!.write(wbuf)
10678
end
107-
ensure
108-
File.delete(path)
109-
file.try(&.close)
79+
writer.not_nil!.close
11080
end
11181
end
112-
{% end %}
113-
{% end %}
114-
115-
it "reads non-blocking file" do
116-
File.open(datapath("test_file.txt"), "r", blocking: false) do |f|
117-
f.gets_to_end.should eq("Hello World\n" * 20)
11882
end
119-
end
12083

121-
it "writes and reads large non-blocking file" do
122-
with_tempfile("non-blocking-io.txt") do |path|
123-
File.open(path, "w+", blocking: false) do |f|
124-
f.puts "Hello World\n" * 40000
125-
f.pos = 0
126-
f.gets_to_end.should eq("Hello World\n" * 40000)
127-
end
128-
end
129-
end
130-
131-
it "can append non-blocking to an existing file" do
132-
with_tempfile("append-existing.txt") do |path|
133-
File.write(path, "hello")
134-
File.write(path, " world", mode: "a", blocking: false)
135-
File.read(path).should eq("hello world")
136-
end
84+
rbuf.should eq(wbuf)
85+
ensure
86+
File.delete(path) if path
87+
writer.try(&.close)
13788
end
89+
{% end %}
13890

139-
it "returns the actual position after non-blocking append" do
140-
with_tempfile("delete-file.txt") do |filename|
141-
File.write(filename, "hello")
142-
143-
File.open(filename, "a", blocking: false) do |file|
144-
file.tell.should eq(0)
91+
# This test verifies that the workaround for a win32 bug with the O_APPEND
92+
# equivalent with OVERLAPPED operations is working as expected.
93+
it "returns the actual position after append" do
94+
with_tempfile("delete-file.txt") do |filename|
95+
File.write(filename, "hello")
14596

146-
file.write "12345".to_slice
147-
file.tell.should eq(10)
97+
File.open(filename, "a") do |file|
98+
file.tell.should eq(0)
14899

149-
file.seek(5, IO::Seek::Set)
150-
file.write "6789".to_slice
151-
file.tell.should eq(14)
152-
end
100+
file.write "12345".to_slice
101+
file.tell.should eq(10)
153102

154-
File.read(filename).should eq("hello123456789")
103+
file.seek(5, IO::Seek::Set)
104+
file.write "6789".to_slice
105+
file.tell.should eq(14)
155106
end
107+
108+
File.read(filename).should eq("hello123456789")
156109
end
157110
end
158111

src/crystal/event_loop/iocp.cr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ class Crystal::EventLoop::IOCP < Crystal::EventLoop
244244
end
245245

246246
def open(path : String, flags : Int32, permissions : File::Permissions, blocking : Bool?) : {System::FileDescriptor::Handle, Bool} | WinError
247-
access, disposition, attributes = System::File.posix_to_open_opts(flags, permissions, blocking)
247+
access, disposition, attributes = System::File.posix_to_open_opts(flags, permissions, !!blocking)
248248

249249
handle = LibC.CreateFileW(
250250
System.to_wstr(path),

src/crystal/event_loop/libevent.cr

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,8 @@ class Crystal::EventLoop::LibEvent < Crystal::EventLoop
130130
fd = LibC.open(path, flags | LibC::O_CLOEXEC, permissions)
131131
return Errno.value if fd == -1
132132

133-
blocking = !System::File.special_type?(fd) if blocking.nil?
134-
unless blocking
135-
status_flags = System::FileDescriptor.fcntl(fd, LibC::F_GETFL)
136-
System::FileDescriptor.fcntl(fd, LibC::F_SETFL, status_flags | LibC::O_NONBLOCK)
137-
end
138-
139-
{fd, blocking}
133+
System::FileDescriptor.set_blocking(fd, false) unless blocking
134+
{fd, !!blocking}
140135
end
141136

142137
def read(file_descriptor : Crystal::System::FileDescriptor, slice : Bytes) : Int32

src/crystal/event_loop/polling.cr

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,8 @@ abstract class Crystal::EventLoop::Polling < Crystal::EventLoop
176176
fd = LibC.open(path, flags | LibC::O_CLOEXEC, permissions)
177177
return Errno.value if fd == -1
178178

179-
blocking = !System::File.special_type?(fd) if blocking.nil?
180-
unless blocking
181-
status_flags = System::FileDescriptor.fcntl(fd, LibC::F_GETFL)
182-
System::FileDescriptor.fcntl(fd, LibC::F_SETFL, status_flags | LibC::O_NONBLOCK)
183-
end
184-
185-
{fd, blocking}
179+
System::FileDescriptor.set_blocking(fd, false) unless blocking
180+
{fd, !!blocking}
186181
end
187182

188183
def read(file_descriptor : System::FileDescriptor, slice : Bytes) : Int32

src/crystal/system/unix/file_descriptor.cr

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,18 @@ module Crystal::System::FileDescriptor
2424
end
2525

2626
private def system_blocking=(value)
27-
current_flags = fcntl(LibC::F_GETFL)
27+
FileDescriptor.set_blocking(fd, value)
28+
end
29+
30+
protected def self.set_blocking(fd, value)
31+
current_flags = fcntl(fd, LibC::F_GETFL)
2832
new_flags = current_flags
2933
if value
3034
new_flags &= ~LibC::O_NONBLOCK
3135
else
3236
new_flags |= LibC::O_NONBLOCK
3337
end
34-
fcntl(LibC::F_SETFL, new_flags) unless new_flags == current_flags
38+
fcntl(fd, LibC::F_SETFL, new_flags) unless new_flags == current_flags
3539
end
3640

3741
protected def system_blocking_init(blocking : Bool?)

src/file.cr

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ class File < IO::FileDescriptor
128128
# This constructor is for constructors to be able to initialize a `File` with
129129
# a *path* and *fd*. The *blocking* param is informational and must reflect
130130
# the non/blocking state of the underlying fd.
131-
private def initialize(@path, fd : Int, mode = "", blocking = true, encoding = nil, invalid = nil)
131+
private def initialize(@path, fd : Int, mode = "", blocking = nil, encoding = nil, invalid = nil)
132132
super(handle: fd)
133133
system_init(mode, blocking)
134134
set_encoding(encoding, invalid: invalid) if encoding
@@ -156,19 +156,12 @@ class File < IO::FileDescriptor
156156
# Line endings are preserved on all platforms. The `b` mode flag has no
157157
# effect; it is provided only for POSIX compatibility.
158158
#
159-
# *blocking* is set to `true` by default because system event queues (e.g.
160-
# epoll, kqueue) will always report the file descriptor of regular disk files
161-
# as ready.
162-
#
163-
# *blocking* must be set to `false` on POSIX targets when the file to open
164-
# isn't a regular file but a character device (e.g. `/dev/tty`) or fifo. These
165-
# files depend on another process or thread to also be reading or writing, and
166-
# system event queues will properly report readiness.
167-
#
168-
# *blocking* may also be set to `nil` in which case the blocking or
169-
# non-blocking flag will be determined automatically, at the expense of an
170-
# additional syscall.
171-
def self.new(filename : Path | String, mode = "r", perm = DEFAULT_CREATE_PERMISSIONS, encoding = nil, invalid = nil, blocking = true)
159+
# NOTE: The *blocking* arg is deprecated since Crystal 1.17. It used to be
160+
# true by default to denote a regular disk file (always ready in system event
161+
# loops) and could be set to false when the file was known to be a fifo, pipe,
162+
# or character device (for example `/dev/tty`). Event loops now properly
163+
# handle this and there are no reasons to change the blocking mode anymore.
164+
def self.new(filename : Path | String, mode = "r", perm = DEFAULT_CREATE_PERMISSIONS, encoding = nil, invalid = nil, blocking = nil)
172165
filename = filename.to_s
173166
fd, blocking = Crystal::System::File.open(filename, mode, perm: perm, blocking: blocking)
174167
new(filename, fd, mode, blocking, encoding, invalid)
@@ -505,7 +498,7 @@ class File < IO::FileDescriptor
505498
# permissions may be set using the *perm* parameter.
506499
#
507500
# See `self.new` for what *mode* can be.
508-
def self.open(filename : Path | String, mode = "r", perm = DEFAULT_CREATE_PERMISSIONS, encoding = nil, invalid = nil, blocking = true) : self
501+
def self.open(filename : Path | String, mode = "r", perm = DEFAULT_CREATE_PERMISSIONS, encoding = nil, invalid = nil, blocking = nil) : self
509502
new filename, mode, perm, encoding, invalid, blocking
510503
end
511504

@@ -514,7 +507,7 @@ class File < IO::FileDescriptor
514507
# file as an argument, the file will be automatically closed when the block returns.
515508
#
516509
# See `self.new` for what *mode* can be.
517-
def self.open(filename : Path | String, mode = "r", perm = DEFAULT_CREATE_PERMISSIONS, encoding = nil, invalid = nil, blocking = true, &)
510+
def self.open(filename : Path | String, mode = "r", perm = DEFAULT_CREATE_PERMISSIONS, encoding = nil, invalid = nil, blocking = nil, &)
518511
file = new filename, mode, perm, encoding, invalid, blocking
519512
begin
520513
yield file
@@ -529,7 +522,7 @@ class File < IO::FileDescriptor
529522
# File.write("bar", "foo")
530523
# File.read("bar") # => "foo"
531524
# ```
532-
def self.read(filename : Path | String, encoding = nil, invalid = nil, blocking = true) : String
525+
def self.read(filename : Path | String, encoding = nil, invalid = nil, blocking = nil) : String
533526
open(filename, "r", blocking: blocking) do |file|
534527
if encoding
535528
file.set_encoding(encoding, invalid: invalid)
@@ -558,7 +551,7 @@ class File < IO::FileDescriptor
558551
# end
559552
# array # => ["foo", "bar"]
560553
# ```
561-
def self.each_line(filename : Path | String, encoding = nil, invalid = nil, chomp = true, blocking = true, &)
554+
def self.each_line(filename : Path | String, encoding = nil, invalid = nil, chomp = true, blocking = nil, &)
562555
open(filename, "r", encoding: encoding, invalid: invalid, blocking: blocking) do |file|
563556
file.each_line(chomp: chomp) do |line|
564557
yield line
@@ -572,7 +565,7 @@ class File < IO::FileDescriptor
572565
# File.write("foobar", "foo\nbar")
573566
# File.read_lines("foobar") # => ["foo", "bar"]
574567
# ```
575-
def self.read_lines(filename : Path | String, encoding = nil, invalid = nil, chomp = true, blocking = true) : Array(String)
568+
def self.read_lines(filename : Path | String, encoding = nil, invalid = nil, chomp = true, blocking = nil) : Array(String)
576569
lines = [] of String
577570
each_line(filename, encoding: encoding, invalid: invalid, chomp: chomp, blocking: blocking) do |line|
578571
lines << line
@@ -597,7 +590,7 @@ class File < IO::FileDescriptor
597590
# (the result of invoking `to_s` on *content*).
598591
#
599592
# See `self.new` for what *mode* can be.
600-
def self.write(filename : Path | String, content, perm = DEFAULT_CREATE_PERMISSIONS, encoding = nil, invalid = nil, mode = "w", blocking = true)
593+
def self.write(filename : Path | String, content, perm = DEFAULT_CREATE_PERMISSIONS, encoding = nil, invalid = nil, mode = "w", blocking = nil)
601594
open(filename, mode, perm, encoding: encoding, invalid: invalid, blocking: blocking) do |file|
602595
case content
603596
when Bytes

0 commit comments

Comments
 (0)