Skip to content

Commit fec255a

Browse files
authored
Robust list enumeration using intrusive iterator. (#209)
1 parent fd6e568 commit fec255a

File tree

4 files changed

+182
-32
lines changed

4 files changed

+182
-32
lines changed

lib/async/list.rb

Lines changed: 156 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,27 @@ def initialize
1515

1616
# Print a short summary of the list.
1717
def to_s
18-
"#<#{self.class.name} size=#{@size}>"
18+
sprintf("#<%s:0x%x size=%d>", self.class.name, object_id, @size)
1919
end
2020

2121
alias inspect to_s
2222

23+
# Fast, safe, unbounded accumulation of children.
24+
def to_a
25+
items = []
26+
current = self
27+
28+
while current.tail != self
29+
unless current.tail.is_a?(Iterator)
30+
items << current.tail
31+
end
32+
33+
current = current.tail
34+
end
35+
36+
return items
37+
end
38+
2339
# Points at the end of the list.
2440
attr_accessor :head
2541

@@ -118,31 +134,47 @@ def remove(node)
118134

119135
# @returns [Boolean] Returns true if the list is empty.
120136
def empty?
121-
@tail.equal?(self)
137+
@size == 0
122138
end
123139

124-
# Iterate over each node in the linked list. It is generally safe to remove the current node, any previous node or any future node during iteration.
125-
#
126-
# @yields {|node| ...} Yields each node in the list.
127-
# @returns [List] Returns self.
128-
def each
129-
return to_enum unless block_given?
130-
131-
current = self
140+
def validate!(node = nil)
141+
previous = self
142+
current = @tail
143+
found = node.equal?(self)
132144

133145
while true
134-
node = current.tail
135-
# binding.irb if node.nil? && !node.equal?(self)
136-
break if node.equal?(self)
146+
break if current.equal?(self)
147+
148+
if current.head != previous
149+
raise "Invalid previous linked list node!"
150+
end
137151

138-
yield node
152+
if current.is_a?(List) and !current.equal?(self)
153+
raise "Invalid list in list node!"
154+
end
139155

140-
# If the node has deleted itself or any subsequent node, it will no longer be the next node, so don't use it for continued traversal:
141-
if current.tail.equal?(node)
142-
current = node
156+
if node
157+
found ||= current.equal?(node)
143158
end
159+
160+
previous = current
161+
current = current.tail
144162
end
145163

164+
if node and !found
165+
raise "Node not found in list!"
166+
end
167+
end
168+
169+
# Iterate over each node in the linked list. It is generally safe to remove the current node, any previous node or any future node during iteration.
170+
#
171+
# @yields {|node| ...} Yields each node in the list.
172+
# @returns [List] Returns self.
173+
def each(&block)
174+
return to_enum unless block_given?
175+
176+
Iterator.each(self, &block)
177+
146178
return self
147179
end
148180

@@ -160,22 +192,119 @@ def include?(needle)
160192

161193
# @returns [Node] Returns the first node in the list, if it is not empty.
162194
def first
163-
unless @tail.equal?(self)
164-
@tail
195+
# validate!
196+
197+
current = @tail
198+
199+
while !current.equal?(self)
200+
if current.is_a?(Iterator)
201+
current = current.tail
202+
else
203+
return current
204+
end
165205
end
206+
207+
return nil
166208
end
167209

168210
# @returns [Node] Returns the last node in the list, if it is not empty.
169211
def last
170-
unless @head.equal?(self)
171-
@head
212+
# validate!
213+
214+
current = @head
215+
216+
while !current.equal?(self)
217+
if current.is_a?(Iterator)
218+
current = current.head
219+
else
220+
return current
221+
end
172222
end
223+
224+
return nil
173225
end
174-
end
175-
176-
# A linked list Node.
177-
class List::Node
178-
attr_accessor :head
179-
attr_accessor :tail
226+
227+
def shift
228+
if node = first
229+
remove!(node)
230+
end
231+
end
232+
233+
# A linked list Node.
234+
class Node
235+
attr_accessor :head
236+
attr_accessor :tail
237+
238+
alias inspect to_s
239+
end
240+
241+
class Iterator < Node
242+
def initialize(list)
243+
@list = list
244+
245+
# Insert the iterator as the first item in the list:
246+
@tail = list.tail
247+
@tail.head = self
248+
list.tail = self
249+
@head = list
250+
end
251+
252+
def remove!
253+
@head.tail = @tail
254+
@tail.head = @head
255+
@head = nil
256+
@tail = nil
257+
@list = nil
258+
end
259+
260+
def move_next
261+
# Move to the next item (which could be an iterator or the end):
262+
@tail.head = @head
263+
@head.tail = @tail
264+
@head = @tail
265+
@tail = @tail.tail
266+
@head.tail = self
267+
@tail.head = self
268+
end
269+
270+
def move_current
271+
while true
272+
# Are we at the end of the list?
273+
if @tail.equal?(@list)
274+
return nil
275+
end
276+
277+
if @tail.is_a?(Iterator)
278+
move_next
279+
else
280+
return @tail
281+
end
282+
end
283+
end
284+
285+
def each
286+
while current = move_current
287+
yield current
288+
289+
if current.equal?(@tail)
290+
move_next
291+
end
292+
end
293+
end
294+
295+
def self.each(list, &block)
296+
list.validate!
297+
298+
return if list.empty?
299+
300+
iterator = Iterator.new(list)
301+
302+
iterator.each(&block)
303+
ensure
304+
iterator&.remove!
305+
end
306+
end
307+
308+
private_constant :Iterator
180309
end
181310
end

lib/async/node.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,12 @@ def consume
187187
if parent = @parent and finished?
188188
parent.remove_child(self)
189189

190+
# If we have children, then we need to move them to our the parent if they are not finished:
190191
if @children
191-
@children.each do |child|
192+
while child = @children.shift
192193
if child.finished?
193-
remove_child(child)
194+
child.set_parent(nil)
194195
else
195-
# In theory we don't need to do this... because we are throwing away the list. However, if you don't correctly update the list when moving the child to the parent, it foobars the enumeration, and subsequent nodes will be skipped, or in the worst case you might start enumerating the parents nodes.
196-
remove_child(child)
197196
parent.add_child(child)
198197
end
199198
end

lib/async/task.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ def schedule(&block)
263263
self.root.resume(@fiber)
264264
end
265265

266-
# Finish the current task, and all bound bound IO objects.
266+
# Finish the current task, moving any children to the parent.
267267
def finish!
268268
# Allow the fiber to be recycled.
269269
@fiber = nil

test/async/task.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,28 @@
414414

415415
expect(items).to be == [1, 2]
416416
end
417+
418+
it "can stop a child task with transient children" do
419+
parent = child = transient = nil
420+
421+
reactor.run do |task|
422+
parent = task.async do |task|
423+
transient = task.async(transient: true) do
424+
sleep(1)
425+
end
426+
427+
child = task.async do
428+
sleep(1)
429+
end
430+
end
431+
432+
parent.wait
433+
expect(parent).to be(:complete?)
434+
parent.stop
435+
expect(parent).to be(:stopped?)
436+
expect(transient).to be(:running?)
437+
end.wait
438+
end
417439
end
418440

419441
with '#sleep' do

0 commit comments

Comments
 (0)