Class: Build::Graph::Walker

Inherits:
Object
  • Object
show all
Defined in:
lib/build/graph/walker.rb

Overview

A walker walks over a graph and applies a task to each node.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(logger: Console.logger, &block) ⇒ Walker

Returns a new instance of Walker.


42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# 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

Instance Attribute Details

#countObject (readonly)

Returns the value of attribute count


73
74
75
# File 'lib/build/graph/walker.rb', line 73

def count
  @count
end

#dirtyObject (readonly)

Returns the value of attribute dirty


74
75
76
# File 'lib/build/graph/walker.rb', line 74

def dirty
  @dirty
end

#failed_outputsObject (readonly)

Returns the value of attribute failed_outputs


71
72
73
# File 'lib/build/graph/walker.rb', line 71

def failed_outputs
  @failed_outputs
end

#failed_tasksObject (readonly)

Returns the value of attribute failed_tasks


70
71
72
# File 'lib/build/graph/walker.rb', line 70

def failed_tasks
  @failed_tasks
end

#loggerObject (readonly)

Primarily for debugging from within Task


62
63
64
# File 'lib/build/graph/walker.rb', line 62

def logger
  @logger
end

#monitorObject (readonly)

Returns the value of attribute monitor


78
79
80
# File 'lib/build/graph/walker.rb', line 78

def monitor
  @monitor
end

#outputsObject (readonly)

An Array of transient outputs which are currently being generated.


68
69
70
# File 'lib/build/graph/walker.rb', line 68

def outputs
  @outputs
end

#parentsObject (readonly)

Returns the value of attribute parents


76
77
78
# File 'lib/build/graph/walker.rb', line 76

def parents
  @parents
end

#tasksObject (readonly)

An Array of all instantiated tasks.


65
66
67
# File 'lib/build/graph/walker.rb', line 65

def tasks
  @tasks
end

Class Method Details

.for(task_class, *args, **options) ⇒ Object


32
33
34
35
36
37
38
39
40
# 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

Instance Method Details

#call(node, parent_task = nil) ⇒ Object


86
87
88
89
90
91
92
93
94
95
96
97
# 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_failedObject


224
225
226
227
228
229
230
231
# 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) ⇒ Object


216
217
218
219
220
221
222
# 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) ⇒ Object


162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# 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) ⇒ Object


184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# 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?Boolean

Returns:

  • (Boolean)

99
100
101
# File 'lib/build/graph/walker.rb', line 99

def failed?
	@failed_tasks.size > 0
end

#inspectObject


241
242
243
# 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) ⇒ Object


233
234
235
236
237
238
239
# File 'lib/build/graph/walker.rb', line 233

def run(**options)
	yield
	
	monitor.run(**options) do
		yield
	end
end

#update(nodes) ⇒ Object


80
81
82
83
84
# 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) ⇒ Object

A parent task only completes once all it's children are complete.


136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# 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) ⇒ Object


103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# 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