Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: some specs rely on Fiber.yield behavior #6953

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Fix: some specs rely on Fiber.yield behavior
Some specs are relying on the `Fiber.yield` behavior is actually the
event loop behavior (libevent2). They start failing whenever the
`Fiber.yield` algorithm changes —for example to push the current
fiber to the runnables queue, instead of adding a resume event.

This patch proposes changes to make the fiber synchronization
expectations explicit. Either by looping until somethig is ready, or
using Channel::Unbuffered's sync ability, or with an explicit
enqueue/resume for specs testing Channel itself.
  • Loading branch information
ysbaddaden committed Oct 17, 2018
commit d623176dd3c5ed48a4c94113a0486e31edbe6c23
109 changes: 76 additions & 33 deletions spec/std/channel_spec.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
require "spec"

private def yield_to(fiber)
Crystal::Scheduler.enqueue(Fiber.current)
Crystal::Scheduler.resume(fiber)
end

describe Channel do
it "creates unbuffered with no arguments" do
Channel(Int32).new.should be_a(Channel::Unbuffered(Int32))
Expand Down Expand Up @@ -40,17 +45,22 @@ describe Channel::Unbuffered do
it "blocks if there is no receiver" do
ch = Channel::Unbuffered(Int32).new
state = 0
spawn do
main = Fiber.current

sender = Fiber.new do
state = 1
ch.send 123
state = 2
ensure
yield_to(main)
end

Fiber.yield
yield_to(sender)
state.should eq(1)
ch.receive.should eq(123)
state.should eq(1)
Fiber.yield

sleep
state.should eq(2)
end

Expand All @@ -67,8 +77,8 @@ describe Channel::Unbuffered do
ch = Channel::Unbuffered(Int32).new
ch.full?.should be_true
ch.empty?.should be_true
spawn { ch.send 123 }
Fiber.yield
sender = Fiber.new { ch.send 123 }
yield_to(sender)
ch.empty?.should be_false
ch.full?.should be_true
ch.receive.should eq(123)
Expand All @@ -88,8 +98,8 @@ describe Channel::Unbuffered do

it "can send and receive nil" do
ch = Channel::Unbuffered(Nil).new
spawn { ch.send nil }
Fiber.yield
sender = Fiber.new { ch.send nil }
yield_to(sender)
ch.empty?.should be_false
ch.receive.should be_nil
ch.empty?.should be_true
Expand All @@ -113,10 +123,19 @@ describe Channel::Unbuffered do
it "can be closed from different fiber" do
ch = Channel::Unbuffered(Int32).new
received = false
spawn { expect_raises(Channel::ClosedError) { ch.receive }; received = true }
Fiber.yield
main = Fiber.current

receiver = Fiber.new do
expect_raises(Channel::ClosedError) { ch.receive }
received = true
ensure
yield_to(main)
end

yield_to(receiver)
ch.close
Fiber.yield

sleep
received.should be_true
end

Expand All @@ -138,27 +157,44 @@ describe Channel::Unbuffered do
ch.receive?.should eq(123)
end

it "wakes up the sender fiber when channel is closed" do
it "wakes up sender fiber when channel is closed" do
ch = Channel::Unbuffered(Nil).new
sender_closed = false
spawn do
ch.send nil
ch.send nil
rescue Channel::ClosedError
sender_closed = true
closed = false
main = Fiber.current

sender = Fiber.new do
ch.send(nil)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to #6948 : I'd expect ch.send to raise here since it couldn't deliver the value.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented in the issue. I think raising only in the second send is the right thing.

I would leave the rescue block to catch upon the second send.

closed = ch.closed?
yield_to(main)
end
receiver_closed = false
spawn do
Fiber.yield

yield_to(sender)

ch.close
sleep

closed.should be_true
end

it "wakes up receiver fibers when channel is closed" do
ch = Channel::Unbuffered(Nil).new
closed = false
main = Fiber.current

receiver = Fiber.new do
ch.receive
rescue Channel::ClosedError
receiver_closed = true
closed = ch.closed?
ensure
yield_to(main)
end
Fiber.yield

yield_to(receiver)

ch.close
Fiber.yield
sender_closed.should be_true
receiver_closed.should be_true
sleep

closed.should be_true
end
end

Expand Down Expand Up @@ -191,9 +227,8 @@ describe Channel::Buffered do
it "doesn't block when not full" do
ch = Channel::Buffered(Int32).new
done = false
spawn { ch.send 123; done = true }
done.should be_false
Fiber.yield
sender = Fiber.new { ch.send 123; done = true }
yield_to(sender)
done.should be_true
end

Expand All @@ -213,8 +248,8 @@ describe Channel::Buffered do

it "can send and receive nil" do
ch = Channel::Buffered(Nil).new
spawn { ch.send nil }
Fiber.yield
sender = Fiber.new { ch.send nil }
yield_to(sender)
ch.empty?.should be_false
ch.receive.should be_nil
ch.empty?.should be_true
Expand All @@ -238,10 +273,18 @@ describe Channel::Buffered do
it "can be closed from different fiber" do
ch = Channel::Buffered(Int32).new
received = false
spawn { expect_raises(Channel::ClosedError) { ch.receive }; received = true }
Fiber.yield
main = Fiber.current

receiver = Fiber.new do
expect_raises(Channel::ClosedError) { ch.receive }
received = true
ensure
yield_to(main)
end

yield_to(receiver)
ch.close
Fiber.yield
sleep
received.should be_true
end

Expand Down
13 changes: 9 additions & 4 deletions spec/std/concurrent/future_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,12 @@ describe Concurrent::Future do

describe "future" do
it "computes a value" do
chan = Channel(Int32).new(1)
chan = Channel::Unbuffered(Int32).new
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is changing the spec to run with buffered channel to an unbuffered.

Is there any reason for that change other than buffered channel of size 1 seems to be an unbuffered channel?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An unbuffered channel is synchronous, so it forces main/fiber to sync as we expect it. Unlike Buffered that are async.

Unrelated to this PR, but I think both buffered/unbuffered should have both (a)synchronous modes —in fact, I would have just buffered channels with a blocking argument to select sync/async.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, it's true the behavior is different. And that change does not affect the spec.

I think is more clear to have the async/sync nature in the type and not in the type argument.


f = future { chan.receive }
f.running?.should be_true

chan.send 42
Fiber.yield
f.completed?.should be_true

f.get.should eq(42)
Expand All @@ -59,10 +58,16 @@ describe Concurrent::Future do
end

it "raises" do
f = future { raise IndexError.new("test error") }
# we rely on the channel to sync fibers:
chan = Channel::Unbuffered(Int32).new

f = future do
chan.receive
raise IndexError.new("test error")
end
f.running?.should be_true

Fiber.yield
chan.send(0)
f.completed?.should be_true

expect_raises(IndexError) { f.get }
Expand Down
11 changes: 9 additions & 2 deletions spec/std/concurrent/select_spec.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
require "spec"

private def yield_to(fiber)
Crystal::Scheduler.enqueue(Fiber.current)
Crystal::Scheduler.resume(fiber)
end

describe "select" do
it "select many receviers" do
ch1 = Channel(Int32).new
Expand Down Expand Up @@ -70,6 +75,7 @@ describe "select" do
it "select should work with send which started before receive, fixed #3862" do
ch1 = Channel(Int32).new
ch2 = Channel(Int32).new
main = Fiber.current

spawn do
select
Expand All @@ -87,10 +93,11 @@ describe "select" do
when b = ch2.receive
x = b
end
ensure
yield_to(main)
end

Fiber.yield

sleep
x.should eq 1
end
end
12 changes: 8 additions & 4 deletions spec/std/concurrent_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ private def method_with_named_args(chan, x = 1, y = 2)
chan.send(x + y)
end

private def method_named(expected_named)
private def method_named(expected_named, chan)
Fiber.current.name.should eq(expected_named)
chan.close
end

class SomeParallelJobException < Exception
Expand Down Expand Up @@ -57,15 +58,18 @@ describe "concurrent" do
end

it "spawns named" do
chan = Channel::Unbuffered(Nil).new
spawn(name: "sub") do
Fiber.current.name.should eq("sub")
chan.close
end
Fiber.yield
chan.receive?
end

it "spawns named with macro" do
spawn method_named("foo"), name: "foo"
Fiber.yield
chan = Channel::Unbuffered(Nil).new
spawn method_named("foo", chan), name: "foo"
chan.receive?
end
end

Expand Down
33 changes: 18 additions & 15 deletions spec/std/http/server/server_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ require "http/server"
require "http/client/response"
require "../../../support/ssl"

private def wait_for(timeout = 5.seconds)
now = Time.monotonic

until yield
Fiber.yield

if (Time.monotonic - now) > timeout
raise "server failed to start within 5 seconds"
end
end
end

private class RaiseErrno < IO
def initialize(@value : Int32)
end
Expand Down Expand Up @@ -268,8 +280,6 @@ module HTTP

spawn { server.listen }

Fiber.yield

HTTP::Client.get("http://#{address2}/").body.should eq "Test Server (#{address2})"
HTTP::Client.get("http://#{address1}/").body.should eq "Test Server (#{address1})"
HTTP::Client.get("http://#{address1}/").body.should eq "Test Server (#{address1})"
Expand All @@ -290,7 +300,7 @@ module HTTP
server = Server.new { }
server.bind_unused_port
spawn { server.listen }
Fiber.yield
wait_for { server.listening? }
expect_raises(Exception, "Can't add socket to running server") do
server.bind_unused_port
end
Expand All @@ -301,7 +311,7 @@ module HTTP
server = Server.new { }
server.bind_unused_port
spawn { server.listen }
Fiber.yield
wait_for { server.listening? }
server.close
expect_raises(Exception, "Can't add socket to closed server") do
server.bind_unused_port
Expand Down Expand Up @@ -413,7 +423,6 @@ module HTTP
ip_address2 = socket.local_address

spawn server.listen
Fiber.yield

HTTP::Client.get("https://#{ip_address1}", tls: client_context).body.should eq "Test Server (#{ip_address1})\n"
HTTP::Client.get("https://#{ip_address2}", tls: client_context).body.should eq "Test Server (#{ip_address2})\n"
Expand All @@ -427,8 +436,7 @@ module HTTP
server = Server.new { }
server.bind_unused_port
spawn { server.listen }
Fiber.yield
server.listening?.should be_true
wait_for { server.listening? }
expect_raises(Exception, "Can't start running server") do
server.listen
end
Expand All @@ -439,8 +447,7 @@ module HTTP
server = Server.new { }
server.bind_unused_port
spawn { server.listen }
Fiber.yield
server.listening?.should be_true
wait_for { server.listening? }
server.close
server.listening?.should be_false
expect_raises(Exception, "Can't re-start closed server") do
Expand All @@ -467,8 +474,7 @@ module HTTP
socket2 = server.bind_unix path2

spawn server.listen

Fiber.yield
wait_for { server.listening? }

unix_request(path1).should eq "Test Server (#{path1})"
unix_request(path2).should eq "Test Server (#{path2})"
Expand Down Expand Up @@ -497,6 +503,7 @@ module HTTP
server_done = false
spawn do
server.listen
ensure
server_done = true
end

Expand All @@ -513,8 +520,6 @@ module HTTP
HTTP::Client.get("https://#{address}/", tls: client_context).body.should eq "ok"
end

Fiber.yield

server_done.should be_false
end

Expand All @@ -525,8 +530,6 @@ module HTTP
context.response.puts "foo"
context.response.flush

Fiber.yield

context.response.puts "bar"
end

Expand Down
Loading