Skip to content

Commit

Permalink
Refactor Evented::Arena to allocate in blocks [fixup #14996] (#15205)
Browse files Browse the repository at this point in the history
Replaces the static `mmap` that must accommodate for as many file descriptors as allowed by ulimit/rlimit. Despite being virtual memory, not really allocated in practice, this led to out-of-memory errors in some situations.

The arena now dynamically allocates individual blocks as needed (no more virtual memory). For simplicity reasons it will only ever grow, and won't shrink (we may think of a solution for this later). The original safety guarantees still hold: once an entry has been allocated in the arena, its pointer won't change.

The event loop still limits the arena capacity to the hardware limit (ulimit: open files).

**Side effect:** the arena don't need to remember the maximum fd/index anymore; that was only needed for `fork`; we can simply iterate the allocated blocks now.

Co-authored-by: Johannes Müller <[email protected]>
  • Loading branch information
ysbaddaden and straight-shoota authored Nov 21, 2024
1 parent d57c9d8 commit 02df7ff
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 99 deletions.
56 changes: 42 additions & 14 deletions spec/std/crystal/evented/arena_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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)
arena = Crystal::Evented::Arena(Int32, 96).new(32)
pointer = nil
index = nil
called = 0
Expand All @@ -31,7 +31,7 @@ describe Crystal::Evented::Arena do
end

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

indexes = 32.times.map do |i|
Expand All @@ -49,15 +49,15 @@ describe Crystal::Evented::Arena do
end

it "checks bounds" do
arena = Crystal::Evented::Arena(Int32).new(32)
arena = Crystal::Evented::Arena(Int32, 96).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)
arena = Crystal::Evented::Arena(Int32, 96).new(32)
pointer = nil

index = arena.allocate_at(30) do |ptr|
Expand All @@ -77,15 +77,15 @@ describe Crystal::Evented::Arena do
end

it "can't access unallocated object" do
arena = Crystal::Evented::Arena(Int32).new(32)
arena = Crystal::Evented::Arena(Int32, 96).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)
arena = Crystal::Evented::Arena(Int32, 96).new(32)
called = 0

index1 = arena.allocate_at(2) { called += 1 }
Expand All @@ -102,15 +102,15 @@ describe Crystal::Evented::Arena do
end

it "checks out of bounds" do
arena = Crystal::Evented::Arena(Int32).new(32)
arena = Crystal::Evented::Arena(Int32, 96).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)
arena = Crystal::Evented::Arena(Int32, 96).new(32)
pointer = nil

index = arena.allocate_at(30) do |ptr|
Expand All @@ -131,7 +131,7 @@ describe Crystal::Evented::Arena do
end

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

called = 0
ret = arena.get?(Crystal::Evented::Arena::Index.new(10, 0)) { called += 1 }
Expand All @@ -140,7 +140,7 @@ describe Crystal::Evented::Arena do
end

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

old_index = arena.allocate_at(2) { }
Expand All @@ -166,7 +166,7 @@ describe Crystal::Evented::Arena do
end

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

arena.get?(Crystal::Evented::Arena::Index.new(-1, 0)) { called += 1 }.should be_false
Expand All @@ -178,7 +178,7 @@ describe Crystal::Evented::Arena do

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

index1 = arena.allocate_at(3) { |ptr| ptr.value = 123 }
arena.free(index1) { }
Expand All @@ -192,7 +192,7 @@ describe Crystal::Evented::Arena do
end

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

called = 0
old_index = arena.allocate_at(1) { }
Expand All @@ -214,7 +214,7 @@ describe Crystal::Evented::Arena do
end

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

arena.free(Crystal::Evented::Arena::Index.new(-1, 0)) { called += 1 }
Expand All @@ -223,4 +223,32 @@ describe Crystal::Evented::Arena do
called.should eq(0)
end
end

it "#each_index" do
arena = Crystal::Evented::Arena(Int32, 96).new(32)
indices = [] of {Int32, Crystal::Evented::Arena::Index}

arena.each_index { |i, index| indices << {i, index} }
indices.should be_empty

index5 = arena.allocate_at(5) { }

arena.each_index { |i, index| indices << {i, index} }
indices.should eq([{5, index5}])

index3 = arena.allocate_at(3) { }
index11 = arena.allocate_at(11) { }
index10 = arena.allocate_at(10) { }
index30 = arena.allocate_at(30) { }

indices.clear
arena.each_index { |i, index| indices << {i, index} }
indices.should eq([
{3, index3},
{5, index5},
{10, index10},
{11, index11},
{30, index30},
])
end
end
2 changes: 1 addition & 1 deletion src/crystal/system/unix/epoll/event_loop.cr
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class Crystal::Epoll::EventLoop < Crystal::Evented::EventLoop
system_set_timer(@timers.next_ready?)

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

Expand Down
Loading

0 comments on commit 02df7ff

Please sign in to comment.