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

Refactor Lifetime Event Loop #14996

Merged
Merged
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
657dbcb
Add :evloop to Crystal::Tracing
ysbaddaden Sep 10, 2024
abd0ce4
Add C bindings for epoll
ysbaddaden Sep 6, 2024
e6c722d
Add C bindings for eventfd
ysbaddaden Sep 6, 2024
ceb7a6f
Add C bindings for timerfd + itimerspec
ysbaddaden Sep 6, 2024
74f58d8
Add C bindings for kqueue
ysbaddaden Sep 6, 2024
2421008
Add C bindings for getrlimit(RLIMIT_NOFILE)
ysbaddaden Sep 9, 2024
3f34458
Add Event and related FiberChannel objects
ysbaddaden Sep 10, 2024
95b64d9
Add PollDescriptor and Waiters objects
ysbaddaden Sep 10, 2024
dfa56a5
Add Timers object
ysbaddaden Sep 10, 2024
7a12a86
Add generational arena
ysbaddaden Sep 10, 2024
2363de1
Add polling EventLoop (abstract base)
ysbaddaden Sep 10, 2024
ff85b3a
Add epoll EventLoop (Linux, Android)
ysbaddaden Sep 10, 2024
20adb88
Add kqueue event loop (BSD)
ysbaddaden Sep 10, 2024
73412be
Conditionnaly load IO::Evented
ysbaddaden Sep 10, 2024
dceb184
Enable the epoll/kqueue event loop
ysbaddaden Sep 10, 2024
cf6873b
Fix: cleanup evloop resources before closing pipe/reopen io
ysbaddaden Sep 12, 2024
a9b48c0
Fix: always close the kqueue after fork
ysbaddaden Sep 12, 2024
eac515b
Fix: remember maximum index in arena to avoid OOM
ysbaddaden Sep 12, 2024
a970696
fixup! Add epoll EventLoop (Linux, Android)
ysbaddaden Sep 12, 2024
c9e8b4a
fixup! Add kqueue event loop (BSD)
ysbaddaden Sep 12, 2024
3c3a801
Fix: rename Arena#allocate as Arena#lazy_allocate
ysbaddaden Sep 13, 2024
db6fe20
Fix: rename PollDescriptor#release to #remove
ysbaddaden Sep 13, 2024
c78c556
Fix: errno handling on add/del of fd from epoll/kqueue
ysbaddaden Sep 13, 2024
fd73eb2
Fix: fd transfer on kqueue (BSD, Darwin)
ysbaddaden Sep 13, 2024
5a0d09b
Improve documentation a bit
ysbaddaden Sep 13, 2024
6c45d09
Fix: arena doesn't free the object when the block raises
ysbaddaden Sep 14, 2024
a58e8c1
Add Arena::Index to abstract the generation index
ysbaddaden Sep 17, 2024
0393b01
Fix: invalid check for waiters in PollDescriptor#take_ownership
ysbaddaden Sep 17, 2024
cc6c33a
Fix: race condition with #evented_close in parallel to #run
ysbaddaden Sep 19, 2024
fa6a935
Retry epoll_wait/kevent syscall on EINTR with infinite timeout
ysbaddaden Sep 24, 2024
f42ea86
Remove #try_run? and #try_lock?
ysbaddaden Sep 24, 2024
2a84f73
Fix: use 'open files' soft limit to preallocate arena
ysbaddaden Sep 30, 2024
589993b
Fix: race condition in Waiters#add vs Waiters#consume_each
ysbaddaden Oct 3, 2024
23eb8e9
fixup! Add C bindings for getrlimit(RLIMIT_NOFILE)
ysbaddaden Oct 3, 2024
eb35831
Arena: tests + fixes + use IndexError
ysbaddaden Oct 10, 2024
7e94097
EventLoop: follow suggestions by @straight-shoota
ysbaddaden Oct 10, 2024
08214a8
EventLoop: get rid of the macro
ysbaddaden Oct 10, 2024
762f9b4
Waiters: tests + fix segfault on delete + mt safe by default
ysbaddaden Oct 11, 2024
a5b8ae0
PollDescriptor: tests
ysbaddaden Oct 11, 2024
1c56ca3
Timers: tests + fix #delete + add #each
ysbaddaden Oct 11, 2024
d20e2cf
Fix: crystal tool format
ysbaddaden Oct 11, 2024
a01398d
Fix: simplify Evented::EventLoop#delete_timer
ysbaddaden Oct 14, 2024
5e7f1aa
Fix: simplify skip_file for loading io/evented
ysbaddaden Oct 14, 2024
375253c
Fix: EventLoop#interrupt for kqueue evloop
ysbaddaden Oct 15, 2024
6985080
Fix: prohibit parallel access to arena objects
ysbaddaden Oct 17, 2024
f2a1efe
NetBSD: fix libc binding for kevent
ysbaddaden Oct 22, 2024
b14fa3c
Fix: prefer System::Time.monotonic over Time.monotonic
ysbaddaden Oct 22, 2024
b4c192e
Add :evloop_epoll and :evloop_kqueue flags + opt-in on some targets
ysbaddaden Nov 4, 2024
36ef33b
Fix: compilation with -Devloop_libevent
ysbaddaden Nov 4, 2024
3777d73
fixup! Fix: compilation with -Devloop_libevent
ysbaddaden Nov 4, 2024
cf6f508
Use -Devloop=[libevent|epoll|kqueue] flag(s)
ysbaddaden Nov 4, 2024
406d7d6
Format + avoid formatter bug (#15112)
ysbaddaden Nov 4, 2024
4a66600
fixup! Format + avoid formatter bug (#15112)
ysbaddaden Nov 4, 2024
f782784
fixup! Use -Devloop=[libevent|epoll|kqueue] flag(s)
ysbaddaden Nov 4, 2024
a3a320c
Fix: crystal tool format
ysbaddaden Nov 5, 2024
79bc334
Fix: update todo
ysbaddaden Nov 7, 2024
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
226 changes: 226 additions & 0 deletions spec/std/crystal/evented/arena_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
{% skip_file unless Crystal.has_constant?(:Evented) %}

require "spec"

describe Crystal::Evented::Arena do
describe "#allocate_at?" do
it "yields block when not allocated" do
arena = Crystal::Evented::Arena(Int32).new(32)
pointer = nil
index = nil
called = 0

ret = arena.allocate_at?(0) do |ptr, idx|
pointer = ptr
index = idx
called += 1
end
ret.should eq(index)
called.should eq(1)

ret = arena.allocate_at?(0) { called += 1 }
ret.should be_nil
called.should eq(1)

pointer.should_not be_nil
index.should_not be_nil

arena.get(index.not_nil!) do |ptr|
ptr.should eq(pointer)
end
end

it "allocates up to capacity" do
arena = Crystal::Evented::Arena(Int32).new(32)
indexes = [] of Crystal::Evented::Arena::Index

indexes = 32.times.map do |i|
arena.allocate_at?(i) { |ptr, _| ptr.value = i }
end.to_a

indexes.size.should eq(32)

indexes.each do |index|
arena.get(index.not_nil!) do |pointer|
pointer.should eq(pointer)
pointer.value.should eq(index.not_nil!.index)
end
end
end

it "checks bounds" do
arena = Crystal::Evented::Arena(Int32).new(32)
expect_raises(IndexError) { arena.allocate_at?(-1) { } }
expect_raises(IndexError) { arena.allocate_at?(33) { } }
end
end

describe "#get" do
it "returns previously allocated object" do
arena = Crystal::Evented::Arena(Int32).new(32)
pointer = nil

index = arena.allocate_at(30) do |ptr|
pointer = ptr
ptr.value = 654321
end
called = 0

2.times do
arena.get(index.not_nil!) do |ptr|
ptr.should eq(pointer)
ptr.value.should eq(654321)
called += 1
end
end
called.should eq(2)
end

it "can't access unallocated object" do
arena = Crystal::Evented::Arena(Int32).new(32)

expect_raises(RuntimeError) do
arena.get(Crystal::Evented::Arena::Index.new(10, 0)) { }
end
end

it "checks generation" do
arena = Crystal::Evented::Arena(Int32).new(32)
called = 0

index1 = arena.allocate_at(2) { called += 1 }
called.should eq(1)

arena.free(index1) { }
expect_raises(RuntimeError) { arena.get(index1) { } }

index2 = arena.allocate_at(2) { called += 1 }
called.should eq(2)
expect_raises(RuntimeError) { arena.get(index1) { } }

arena.get(index2) { }
end

it "checks out of bounds" do
arena = Crystal::Evented::Arena(Int32).new(32)
expect_raises(IndexError) { arena.get(Crystal::Evented::Arena::Index.new(-1, 0)) { } }
expect_raises(IndexError) { arena.get(Crystal::Evented::Arena::Index.new(33, 0)) { } }
end
end

describe "#get?" do
it "returns previously allocated object" do
arena = Crystal::Evented::Arena(Int32).new(32)
pointer = nil

index = arena.allocate_at(30) do |ptr|
pointer = ptr
ptr.value = 654321
end

called = 0
2.times do
ret = arena.get?(index) do |ptr|
ptr.should eq(pointer)
ptr.not_nil!.value.should eq(654321)
called += 1
end
ret.should be_true
end
called.should eq(2)
end

it "can't access unallocated index" do
arena = Crystal::Evented::Arena(Int32).new(32)

called = 0
ret = arena.get?(Crystal::Evented::Arena::Index.new(10, 0)) { called += 1 }
ret.should be_false
called.should eq(0)
end

it "checks generation" do
arena = Crystal::Evented::Arena(Int32).new(32)
called = 0

old_index = arena.allocate_at(2) { }
arena.free(old_index) { }

# not accessible after free:
ret = arena.get?(old_index) { called += 1 }
ret.should be_false
called.should eq(0)

# can be reallocated:
new_index = arena.allocate_at(2) { }

# still not accessible after reallocate:
ret = arena.get?(old_index) { called += 1 }
ret.should be_false
called.should eq(0)

# accessible after reallocate (new index):
ret = arena.get?(new_index) { called += 1 }
ret.should be_true
called.should eq(1)
end

it "checks out of bounds" do
arena = Crystal::Evented::Arena(Int32).new(32)
called = 0

arena.get?(Crystal::Evented::Arena::Index.new(-1, 0)) { called += 1 }.should be_false
arena.get?(Crystal::Evented::Arena::Index.new(33, 0)) { called += 1 }.should be_false

called.should eq(0)
end
end

describe "#free" do
it "deallocates the object" do
arena = Crystal::Evented::Arena(Int32).new(32)

index1 = arena.allocate_at(3) { |ptr| ptr.value = 123 }
arena.free(index1) { }

index2 = arena.allocate_at(3) { }
index2.should_not eq(index1)

value = nil
arena.get(index2) { |ptr| value = ptr.value }
value.should eq(0)
end

it "checks generation" do
arena = Crystal::Evented::Arena(Int32).new(32)

called = 0
old_index = arena.allocate_at(1) { }

# can free:
arena.free(old_index) { called += 1 }
called.should eq(1)

# can reallocate:
new_index = arena.allocate_at(1) { }

# can't free with invalid index:
arena.free(old_index) { called += 1 }
called.should eq(1)

# but new index can:
arena.free(new_index) { called += 1 }
called.should eq(2)
end

it "checks out of bounds" do
arena = Crystal::Evented::Arena(Int32).new(32)
called = 0

arena.free(Crystal::Evented::Arena::Index.new(-1, 0)) { called += 1 }
arena.free(Crystal::Evented::Arena::Index.new(33, 0)) { called += 1 }

called.should eq(0)
end
end
end
100 changes: 100 additions & 0 deletions spec/std/crystal/evented/poll_descriptor_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
{% skip_file unless Crystal.has_constant?(:Evented) %}

require "spec"

class Crystal::Evented::FakeLoop < Crystal::Evented::EventLoop
getter operations = [] of {Symbol, Int32, Crystal::Evented::Arena::Index | Bool}

private def system_run(blocking : Bool) : Nil
end

private def interrupt : Nil
end

protected def system_add(fd : Int32, index : Arena::Index) : Nil
operations << {:add, fd, index}
end

protected def system_del(fd : Int32, closing = true) : Nil
operations << {:del, fd, closing}
end

protected def system_del(fd : Int32, closing = true, &) : Nil
operations << {:del, fd, closing}
end

private def system_set_timer(time : Time::Span?) : Nil
end
end

describe Crystal::Evented::Waiters do
describe "#take_ownership" do
it "associates a poll descriptor to an evloop instance" do
fd = Int32::MAX
pd = Crystal::Evented::PollDescriptor.new
index = Crystal::Evented::Arena::Index.new(fd, 0)
evloop = Crystal::Evented::FakeLoop.new

pd.take_ownership(evloop, fd, index)
pd.@event_loop.should be(evloop)

evloop.operations.should eq([
{:add, fd, index},
])
end

it "moves a poll descriptor to another evloop instance" do
fd = Int32::MAX
pd = Crystal::Evented::PollDescriptor.new
index = Crystal::Evented::Arena::Index.new(fd, 0)

evloop1 = Crystal::Evented::FakeLoop.new
evloop2 = Crystal::Evented::FakeLoop.new

pd.take_ownership(evloop1, fd, index)
pd.take_ownership(evloop2, fd, index)

pd.@event_loop.should be(evloop2)

evloop1.operations.should eq([
{:add, fd, index},
{:del, fd, false},
])
evloop2.operations.should eq([
{:add, fd, index},
])
end

it "can't move to the current evloop" do
fd = Int32::MAX
pd = Crystal::Evented::PollDescriptor.new
index = Crystal::Evented::Arena::Index.new(fd, 0)

evloop = Crystal::Evented::FakeLoop.new

pd.take_ownership(evloop, fd, index)
expect_raises(Exception) { pd.take_ownership(evloop, fd, index) }
end

it "can't move with pending waiters" do
fd = Int32::MAX
pd = Crystal::Evented::PollDescriptor.new
index = Crystal::Evented::Arena::Index.new(fd, 0)
event = Crystal::Evented::Event.new(:io_read, Fiber.current)

evloop1 = Crystal::Evented::FakeLoop.new
pd.take_ownership(evloop1, fd, index)
pd.@readers.add(pointerof(event))

evloop2 = Crystal::Evented::FakeLoop.new
expect_raises(RuntimeError) { pd.take_ownership(evloop2, fd, index) }

pd.@event_loop.should be(evloop1)

evloop1.operations.should eq([
{:add, fd, index},
])
evloop2.operations.should be_empty
end
end
end
Loading
Loading