Skip to content

Commit 9278f3f

Browse files
committed
Introduce Kernel#Barrier as a top level scheduling operation.
1 parent eb873e4 commit 9278f3f

3 files changed

Lines changed: 211 additions & 0 deletions

File tree

lib/async.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
require_relative "kernel/async"
1111
require_relative "kernel/sync"
12+
require_relative "kernel/barrier"
1213

1314
# Asynchronous programming framework.
1415
module Async

lib/kernel/barrier.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
require_relative "sync"
7+
require_relative "../async/barrier"
8+
9+
module Kernel
10+
# Create a barrier, yield it to the block, and then wait for all tasks to complete.
11+
#
12+
# If no scheduler is running, one will be created automatically for the duration of the block.
13+
#
14+
# @parameter parent [Task | Semaphore | Nil] The parent for holding any children tasks.
15+
# @parameter **options [Hash] Additional options passed to {Kernel::Sync}.
16+
# @public Since *Async v2.34*.
17+
def Barrier(parent: nil, **options)
18+
Sync(**options) do |task|
19+
parent ||= task
20+
21+
barrier = ::Async::Barrier.new(parent: parent)
22+
23+
yield barrier
24+
25+
barrier.wait
26+
ensure
27+
barrier&.stop
28+
end
29+
end
30+
end

test/kernel/barrier.rb

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
require "kernel/barrier"
7+
8+
describe Kernel do
9+
with "#Barrier" do
10+
it "should create a barrier, yield it, wait, and stop automatically" do
11+
finished = 0
12+
results = []
13+
14+
Barrier do |barrier|
15+
3.times do |i|
16+
barrier.async do |task|
17+
sleep(0.01 * i) # Different completion times
18+
results << "task_#{i}"
19+
finished += 1
20+
end
21+
end
22+
end
23+
24+
expect(finished).to be == 3
25+
expect(results.size).to be == 3
26+
end
27+
28+
it "should handle exceptions and still clean up properly" do
29+
exception_raised = false
30+
31+
expect do
32+
Barrier do |barrier|
33+
barrier.async do |task|
34+
raise "Test exception"
35+
end
36+
37+
barrier.async do |task|
38+
sleep(0.1) # This should be stopped
39+
end
40+
end
41+
end.to raise_exception(RuntimeError, message: be =~ /Test exception/)
42+
43+
# The barrier should have been cleaned up despite the exception.
44+
end
45+
46+
it "should support parent parameter" do
47+
parent_task = nil
48+
child_task = nil
49+
50+
Sync do |task|
51+
parent_task = task
52+
53+
Barrier(parent: task) do |barrier|
54+
barrier.async do |async_task|
55+
child_task = async_task
56+
# While the child task is running, parent should be set:
57+
expect(child_task.parent).to be == parent_task
58+
sleep(0.01)
59+
end
60+
end
61+
end
62+
63+
expect(child_task).not.to be_nil
64+
end
65+
66+
it "should wait for all tasks to complete before returning" do
67+
completion_order = []
68+
69+
Barrier do |barrier|
70+
3.times do |i|
71+
barrier.async do |task|
72+
sleep(0.01 * (3 - i)) # Reverse order completion
73+
completion_order << i
74+
end
75+
end
76+
end
77+
78+
# All tasks should have completed
79+
expect(completion_order.size).to be == 3
80+
expect(completion_order.sort).to be == [0, 1, 2]
81+
end
82+
83+
it "should stop remaining tasks when block exits early" do
84+
tasks = []
85+
86+
Sync do |parent|
87+
begin
88+
Barrier do |barrier|
89+
3.times do |i|
90+
tasks << barrier.async do |task|
91+
sleep(1) # Long running task
92+
end
93+
end
94+
95+
# Simulate early exit due to some condition
96+
raise "Early exit"
97+
end
98+
rescue => e
99+
# Expected exception
100+
end
101+
102+
# Wait for tasks to finish/stopped deterministically
103+
tasks.each do |t|
104+
begin
105+
t.wait
106+
rescue => e
107+
# ignore errors from waiting on stopped tasks
108+
end
109+
end
110+
end
111+
112+
# All three tasks should have been stopped
113+
expect(tasks.map(&:stopped?).all?).to be == true
114+
end
115+
116+
it "should handle empty barriers gracefully" do
117+
result = nil
118+
119+
expect do
120+
result = Async::Barrier() do |barrier|
121+
# No tasks added
122+
end
123+
end.not.to raise_exception
124+
125+
# Should complete successfully
126+
end
127+
end
128+
129+
with "Kernel::Barrier" do
130+
it "should create a barrier, yield it, wait, and stop automatically" do
131+
finished = 0
132+
results = []
133+
Barrier() do |barrier|
134+
3.times do |i|
135+
barrier.async do |task|
136+
sleep(0.01 * i)
137+
results << "task_#{i}"
138+
finished += 1
139+
end
140+
end
141+
end
142+
143+
expect(finished).to be == 3
144+
expect(results.size).to be == 3
145+
end
146+
147+
it "should handle exceptions and still clean up properly" do
148+
expect do
149+
Barrier() do |barrier|
150+
barrier.async do |task|
151+
raise "Kernel helper exception"
152+
end
153+
barrier.async do |task|
154+
sleep(0.1)
155+
end
156+
end
157+
end.to raise_exception(RuntimeError, message: be =~ /Kernel helper exception/)
158+
end
159+
160+
it "should support parent parameter" do
161+
parent_task = nil
162+
child_task = nil
163+
164+
Sync do |task|
165+
parent_task = task
166+
167+
Barrier(parent: task) do |barrier|
168+
barrier.async do |async_task|
169+
child_task = async_task
170+
# While the child task is running, parent should be set:
171+
expect(child_task.parent).to be == parent_task
172+
sleep(0.01)
173+
end
174+
end
175+
end
176+
177+
expect(child_task).not.to be_nil
178+
end
179+
end
180+
end

0 commit comments

Comments
 (0)