Skip to content

Commit 9788c3e

Browse files
Introduce Task.wait_all.
1 parent 1819433 commit 9788c3e

File tree

5 files changed

+202
-1
lines changed

5 files changed

+202
-1
lines changed

lib/async/node.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,14 @@ def stop(later = false)
295295
end
296296
end
297297

298+
# Wait for this node to complete. By default, nodes cannot be waited on.
299+
# Subclasses like Task override this method to provide waiting functionality.
300+
#
301+
# @returns [self] Returns self for method chaining.
302+
def wait
303+
self
304+
end
305+
298306
# Whether the node has been stopped.
299307
def stopped?
300308
@children.nil?

lib/async/task.rb

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,11 +284,40 @@ def wait
284284
begin
285285
@promise.wait
286286
rescue Promise::Cancel
287-
# For backward compatibility, stopped tasks return nil
287+
# For backward compatibility, stopped tasks return nil:
288288
return nil
289289
end
290290
end
291291

292+
# Wait on all non-transient children to complete, recursively, then wait on the task itself, if it is not the current task.
293+
#
294+
# If any child task fails with an exception, that exception will be raised immediately, and remaining children may not be waited on.
295+
#
296+
# @example Waiting on all children.
297+
# Async do |task|
298+
# child = task.async do
299+
# sleep(0.01)
300+
# end
301+
# task.wait_all # Will wait on the child task.
302+
# end
303+
#
304+
# @raises [StandardError] If any child task failed with an exception, that exception will be raised.
305+
# @returns [Object | Nil] The final expression/result of the task's block, or nil if called from within the task.
306+
# @asynchronous This method is thread-safe.
307+
def wait_all
308+
@children&.each do |child|
309+
# Skip transient tasks
310+
next if child.transient?
311+
312+
child.wait_all
313+
end
314+
315+
# Only wait on the task if we're not waiting on ourselves:
316+
unless self.current?
317+
return self.wait
318+
end
319+
end
320+
292321
# Access the result of the task without waiting. May be nil if the task is not completed. Does not raise exceptions.
293322
def result
294323
value = @promise.value

releases.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Releases
22

3+
## Unreleased
4+
5+
- Introduce `Task#wait_all` which recursively waits for all children and self, excepting the current task.
6+
37
## v2.35.3
48

59
- `Async::Clock` now implements `#as_json` and `#to_json` for nicer log formatting.

test/async/node.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,4 +311,11 @@
311311
expect(node.traverse.to_a).to be == [[node, 0], [middle, 1], [child1, 2], [child2, 2]]
312312
end
313313
end
314+
315+
with "#wait" do
316+
it "returns self for a plain node" do
317+
result = node.wait
318+
expect(result).to be_equal(node)
319+
end
320+
end
314321
end

test/async/task.rb

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,159 @@ def sleep_forever
870870
end
871871
end
872872

873+
with "#wait_all" do
874+
it "will wait on all child tasks to complete" do
875+
results = []
876+
877+
reactor.run do |parent|
878+
child1 = parent.async do |child|
879+
child.yield
880+
results << :child1
881+
end
882+
883+
child2 = parent.async do |child|
884+
child.yield
885+
results << :child2
886+
end
887+
888+
parent.wait_all
889+
results << :parent
890+
end
891+
892+
expect(results).to be == [:child1, :child2, :parent]
893+
end
894+
895+
it "will wait recursively on nested child tasks" do
896+
results = []
897+
898+
reactor.run do |parent|
899+
child = parent.async do |child|
900+
grandchild = child.async do |grandchild|
901+
grandchild.yield
902+
results << :grandchild
903+
end
904+
905+
child.yield
906+
results << :child
907+
end
908+
909+
parent.wait_all
910+
results << :parent
911+
end
912+
913+
expect(results).to be == [:grandchild, :child, :parent]
914+
end
915+
916+
it "will skip transient tasks" do
917+
results = []
918+
919+
reactor.run do |parent|
920+
child = parent.async do |child|
921+
child.yield
922+
results << :child
923+
end
924+
925+
transient = parent.async(transient: true) do
926+
sleep(0.1)
927+
results << :transient
928+
end
929+
930+
parent.wait_all
931+
results << :parent
932+
end
933+
934+
# Transient task should not have completed
935+
expect(results).to be == [:child, :parent]
936+
end
937+
938+
it "will handle tasks with no children" do
939+
reactor.run do |parent|
940+
result = parent.wait_all
941+
expect(result).to be_nil
942+
end
943+
end
944+
945+
it "will wait on multiple levels of nesting" do
946+
results = []
947+
948+
reactor.run do |parent|
949+
child1 = parent.async do |child|
950+
grandchild1 = child.async do |grandchild|
951+
grandchild.yield
952+
results << :grandchild1
953+
end
954+
child.yield
955+
results << :child1
956+
end
957+
958+
child2 = parent.async do |child|
959+
grandchild2 = child.async do |grandchild|
960+
grandchild.yield
961+
results << :grandchild2
962+
end
963+
child.yield
964+
results << :child2
965+
end
966+
967+
parent.wait_all
968+
results << :parent
969+
end
970+
971+
# All tasks should complete in order
972+
expect(results).to be(:include?, :grandchild1)
973+
expect(results).to be(:include?, :grandchild2)
974+
expect(results).to be(:include?, :child1)
975+
expect(results).to be(:include?, :child2)
976+
expect(results.last).to be == :parent
977+
end
978+
979+
it "returns nil when called from within the task" do
980+
reactor.run do |parent|
981+
child = parent.async do |child|
982+
child.yield
983+
end
984+
985+
result = parent.wait_all
986+
expect(result).to be_nil
987+
end
988+
end
989+
990+
it "returns the task result when called from outside" do
991+
parent = reactor.async do |parent|
992+
child = parent.async do |child|
993+
child.yield
994+
end
995+
:result
996+
end
997+
998+
reactor.run
999+
1000+
result = parent.wait_all
1001+
expect(result).to be == :result
1002+
end
1003+
1004+
it "will propagate exceptions from child tasks" do
1005+
failed_child = nil
1006+
1007+
reactor.run do |parent|
1008+
failed_child = parent.async(finished: false) do |child|
1009+
child.yield
1010+
raise RuntimeError, "child task failed"
1011+
end
1012+
1013+
# Wait for the failed child to fail first
1014+
reactor.run
1015+
1016+
expect(failed_child).to be(:finished?)
1017+
1018+
# wait_all should propagate the exception when it calls wait on the failed child
1019+
expect do
1020+
parent.wait_all
1021+
end.to raise_exception(RuntimeError, message: be =~ /child task failed/)
1022+
end
1023+
end
1024+
end
1025+
8731026
with "#result" do
8741027
it "does not raise exception" do
8751028
task = reactor.async do |task|

0 commit comments

Comments
 (0)