class Build::Graph::Walker
A walker walks over a graph and applies a task to each node.
Attributes
count[R]
dirty[R]
failed_outputs[R]
failed_tasks[R]
logger[R]
Primarily for debugging from within Task
monitor[R]
outputs[R]
An Array of transient outputs which are currently being generated.
parents[R]
tasks[R]
An Array of all instantiated tasks.
Public Class Methods
for(task_class, *args, **options)
click to toggle source
# File lib/build/graph/walker.rb, line 32 def self.for(task_class, *args, **options) self.new(**options) do |walker, node, parent_task = nil| task = task_class.new(walker, node, *args) task.visit do task.update end end end
new(logger: Console.logger, &block)
click to toggle source
# File lib/build/graph/walker.rb, line 42 def initialize(logger: Console.logger, &block) # Node -> Task mapping. @tasks = {} @update = block # A list of paths which are currently being generated by tasks: @outputs = {} @parents = Hash.new{|h,k| h[k] = []} # Failed output paths: @failed_tasks = [] @failed_outputs = Set.new @logger = logger @monitor = Files::Monitor.new(logger: @logger) end
Public Instance Methods
call(node, parent_task = nil)
click to toggle source
# File lib/build/graph/walker.rb, line 86 def call(node, parent_task = nil) # We try to fetch the task if it has already been invoked, otherwise we create a new task. @tasks.fetch(node) do @logger&.debug(self) {"Update: #{node} #{parent_task.class}"} # This method should add the node @update.call(self, node, parent_task) # This should now be defined: return @tasks[node] end end
clear_failed()
click to toggle source
# File lib/build/graph/walker.rb, line 224 def clear_failed @failed_tasks.each do |task| self.delete(task.node) end if @failed_tasks @failed_tasks = [] @failed_outputs = Set.new end
delete(node)
click to toggle source
# File lib/build/graph/walker.rb, line 216 def delete(node) @logger&.debug(self) {"Delete #{node}"} if task = @tasks.delete(node) @monitor.delete(task) end end
enter(task)
click to toggle source
# File lib/build/graph/walker.rb, line 162 def enter(task) @logger&.debug(self) {"Walker entering: #{task.node}"} @tasks[task.node] = task # In order to wait on outputs, they must be known before entering the task. This might seem odd, but unless we know outputs are being generated, waiting for them to complete is impossible - unless this was somehow specified ahead of time. The implications of this logic is that all tasks must be sequential in terms of output -> input chaning. This is by design and is not a problem in practice. if outputs = task.outputs @logger&.debug(self) do |buffer| buffer.puts "Task will generate outputs:" Array(outputs).each do |output| buffer.puts output.inspect end end outputs.each do |path| # Tasks which have children tasks may list the same output twice. This is not a bug. @outputs[path.to_s] ||= [] end end end
exit(task)
click to toggle source
# File lib/build/graph/walker.rb, line 184 def exit(task) @logger&.debug(self) {"Walker exiting: #{task.node}, task #{task.failed? ? 'failed' : 'succeeded'}"} # Fail outputs if the node failed: if task.failed? @failed_tasks << task if task.outputs @failed_outputs += task.outputs.collect{|path| path.to_s} end end # Clean the node's outputs: task.outputs.each do |path| path = path.to_s @logger&.debug(self) {"File #{task.failed? ? 'failed' : 'available'}: #{path}"} if edges = @outputs.delete(path) # @logger&.debug "\tUpdating #{edges.count} edges..." edges.each{|edge| edge.traverse(task)} end end # Notify the parent nodes that the child is done: if parents = @parents.delete(task.node) parents.each{|edge| edge.traverse(task)} end @monitor.add(task) end
failed?()
click to toggle source
# File lib/build/graph/walker.rb, line 99 def failed? @failed_tasks.size > 0 end
inspect()
click to toggle source
# File lib/build/graph/walker.rb, line 241 def inspect "\#<#{self.class}:0x#{self.object_id.to_s(16)} #{@tasks.count} tasks, #{@failed_tasks.count} failed>" end
run(**options) { || ... }
click to toggle source
# File lib/build/graph/walker.rb, line 233 def run(**options) yield monitor.run(**options) do yield end end
update(nodes)
click to toggle source
# File lib/build/graph/walker.rb, line 80 def update(nodes) Array(nodes).each do |node| self.call(node) end end
wait_for_children(parent, children)
click to toggle source
A parent task only completes once all it's children are complete.
# File lib/build/graph/walker.rb, line 136 def wait_for_children(parent, children) # Consider only incomplete/failed children: children = children.select{|child| !child.complete?} # If there are no children like this, then done: return true if children.size == 0 @logger&.debug(self) {"Task #{parent} is waiting on #{children.count} children"} # Otherwise, construct an edge to track state changes: edge = Edge.new children.each do |child| if child.failed? edge.skip!(child) else # We are waiting for this child to finish: edge.increment! @parents[child.node] << edge end end return edge.wait end
wait_on_paths(task, paths)
click to toggle source
# File lib/build/graph/walker.rb, line 103 def wait_on_paths(task, paths) # If there are no paths, we are done: return true if paths.count == 0 # We create a new directed hyper-graph edge which waits for all paths to be ready (or failed): edge = Edge.new paths = paths.collect(&:to_s) paths.each do |path| # Is there a task generating this output? if outputs = @outputs[path] @logger&.debug(self) {"Task #{task} is waiting on path #{path}"} # When the output is ready, trigger this edge: outputs << edge edge.increment! elsif !File.exist?(path) @logger&.warn(self) {"Task #{task} is waiting on paths which don't exist and are not being generated!"} raise RuntimeError, "File #{path} is not being generated by any active task!" # What should we do about paths which haven't been registered as outputs? # Either they exist - or they don't. # If they exist, it means they are probably static inputs of the build graph. # If they don't, it might be an error, or it might be deliberate. end end failed = paths.any?{|path| @failed_outputs.include?(path)} return edge.wait && !failed end