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
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add epoll EventLoop (Linux, Android)
Specific to Linux and Android. It might be working on Solaris too
through their Linux compatibility layer.
  • Loading branch information
ysbaddaden committed Oct 3, 2024
commit ff85b3a2e2a1e756ce4351c4ead1634adb45b132
151 changes: 151 additions & 0 deletions src/crystal/system/unix/epoll/event_loop.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
require "../evented/event_loop"
require "../epoll"
require "../eventfd"
require "../timerfd"

class Crystal::Epoll::EventLoop < Crystal::Evented::EventLoop
def initialize
super

# the epoll instance
@epoll = System::Epoll.new

# notification to interrupt a run
@interrupted = Atomic::Flag.new
@eventfd = System::EventFD.new
@epoll.add(@eventfd.fd, LibC::EPOLLIN, u64: @eventfd.fd.to_u64!)

# we use timerfd to go below the millisecond precision of epoll_wait; it
# also allows to avoid locking timers before every epoll_wait call
@timerfd = System::TimerFD.new
@epoll.add(@timerfd.fd, LibC::EPOLLIN, u64: @timerfd.fd.to_u64!)
end

def after_fork_before_exec : Nil
super

# O_CLOEXEC would close these automatically, but we don't want to mess with
# the parent process fds (it would mess the parent evloop)
@epoll.close
@eventfd.close
@timerfd.close
end

{% unless flag?(:preview_mt) %}
def after_fork : Nil
super

# close inherited fds
@epoll.close
@eventfd.close
@timerfd.close

# create new fds
@epoll = System::Epoll.new

@interrupted.clear
@eventfd = System::EventFD.new
@epoll.add(@eventfd.fd, LibC::EPOLLIN, u64: @eventfd.fd.to_u64!)

@timerfd = System::TimerFD.new
@epoll.add(@timerfd.fd, LibC::EPOLLIN, u64: @timerfd.fd.to_u64!)
system_set_timer(@timers.next_ready?)

# re-add all registered fds
Evented.arena.each { |fd, gen_index| system_add(fd, gen_index) }
end
{% end %}

private def system_run(blocking : Bool) : Nil
Crystal.trace :evloop, "wait", blocking: blocking ? 1 : 0

# wait for events (indefinitely when blocking)
buffer = uninitialized LibC::EpollEvent[128]
epoll_events = @epoll.wait(buffer.to_slice, timeout: blocking ? -1 : 0)

timer_triggered = false

# process events
epoll_events.size.times do |i|
epoll_event = epoll_events.to_unsafe + i

case epoll_event.value.data.u64
when @eventfd.fd
# TODO: panic if epoll_event.value.events != LibC::EPOLLIN (could be EPOLLERR or EPLLHUP)
Crystal.trace :evloop, "interrupted"
@eventfd.read
# OPTIMIZE: only reset interrupted before a blocking wait
@interrupted.clear
when @timerfd.fd
# TODO: panic if epoll_event.value.events != LibC::EPOLLIN (could be EPOLLERR or EPLLHUP)
Crystal.trace :evloop, "timer"
timer_triggered = true
else
process(epoll_event)
end
end

process_timers(timer_triggered)
end

private def process(epoll_event : LibC::EpollEvent*) : Nil
gen_index = epoll_event.value.data.u64.unsafe_as(Int64)
events = epoll_event.value.events

{% if flag?(:tracing) %}
fd = (gen_index >> 32).to_i32!
Crystal.trace :evloop, "event", fd: fd, gen_index: gen_index, events: events
{% end %}

pd = Evented.arena.get(gen_index)

if (events & (LibC::EPOLLERR | LibC::EPOLLHUP)) != 0
pd.value.@readers.consume_each { |event| resume_io(event) }
pd.value.@writers.consume_each { |event| resume_io(event) }
return
end

if (events & LibC::EPOLLRDHUP) == LibC::EPOLLRDHUP
pd.value.@readers.consume_each { |event| resume_io(event) }
elsif (events & LibC::EPOLLIN) == LibC::EPOLLIN
if event = pd.value.@readers.ready!
resume_io(event)
end
end

if (events & LibC::EPOLLOUT) == LibC::EPOLLOUT
if event = pd.value.@writers.ready!
resume_io(event)
end
end
end

def interrupt : Nil
# the atomic makes sure we only write once
@eventfd.write(1) if @interrupted.test_and_set
end

protected def system_add(fd : Int32, gen_index : Int64) : Nil
Crystal.trace :evloop, "epoll_ctl", op: "add", fd: fd, gen_index: gen_index
events = LibC::EPOLLIN | LibC::EPOLLOUT | LibC::EPOLLRDHUP | LibC::EPOLLET
@epoll.add(fd, events, u64: gen_index.unsafe_as(UInt64))
end

protected def system_del(fd : Int32) : Nil
Crystal.trace :evloop, "epoll_ctl", op: "del", fd: fd
@epoll.delete(fd)
end

protected def system_del(fd : Int32, &) : Nil
Crystal.trace :evloop, "epoll_ctl", op: "del", fd: fd
@epoll.delete(fd) { yield }
end

private def system_set_timer(time : Time::Span?) : Nil
if time
@timerfd.set(time)
else
@timerfd.cancel
end
end
end