Add name based re-weight helper. Add documentation/comments.
This commit is contained in:
parent
f9e55a8f9d
commit
f220ac2f94
@ -38,12 +38,22 @@ module VagrantPlugins
|
||||
@m.synchronize do
|
||||
logger.debug { "finding path #{src} -> #{dst}" }
|
||||
|
||||
# Mark the root of our graph which will be used later
|
||||
@root = src
|
||||
|
||||
# We know the root is our final destination, so flop the graph before searching
|
||||
# When searching for a path in the graph, we will be looking
|
||||
# for the shortest path. Since we know the desired final
|
||||
# destination, we want to use that as our starting point
|
||||
# and attempt to find a path from that destination vertex
|
||||
# to the source vertex. So we start by reversing the graph
|
||||
graph.reverse!
|
||||
|
||||
# Perform an initial DFS to prune the graph down
|
||||
# Perform an initial DFS to prune the graph down. This will remove any
|
||||
# vertices which are not connected to the root of the graph (which will
|
||||
# be the destination since we reversed the graph). The reason we do this
|
||||
# is to reduce the overall size of the graph before generating our path,
|
||||
# and to ensure that the both ends of the graph are still available after
|
||||
# the pruning.
|
||||
vertices = []
|
||||
descender = lambda { |v|
|
||||
graph.depth_first_visit(v) { |vrt|
|
||||
@ -61,6 +71,9 @@ module VagrantPlugins
|
||||
end
|
||||
}
|
||||
|
||||
# Since the destination is now the "root" of our reversed graph,
|
||||
# we want to start our search from there to generate the list
|
||||
# of verticies it can connect to.
|
||||
descender.call(dst)
|
||||
|
||||
# Remove excess vertices from the graph
|
||||
@ -71,11 +84,15 @@ module VagrantPlugins
|
||||
logger.trace { "graph after DFS reduction:\n#{graph.reverse.inspect}" }
|
||||
logger.trace { "generating list of required vertices for path #{src} -> #{dst}" }
|
||||
|
||||
# If the graph no longer contains the source vertex (this is the root of the
|
||||
# non-reversed graph) then it is impossible to generate a valid path.
|
||||
if !graph.vertices.include?(src)
|
||||
raise NoPathError,
|
||||
"Graph no longer includes source vertex #{src} (#{src} -> #{dst})"
|
||||
end
|
||||
|
||||
# This should not happen since we start our DFS from the destination vertex
|
||||
# but we keep this sanity check regardless.
|
||||
if !graph.vertices.include?(dst)
|
||||
raise NoPathError,
|
||||
"Graph no longer includes destination vertex #{dst} (#{src} -> #{dst})"
|
||||
@ -83,7 +100,7 @@ module VagrantPlugins
|
||||
# Generate list of required vertices from the graph
|
||||
required_vertices = generate_path(dst, src)
|
||||
|
||||
# If not vertices were returned, then the path generation failed
|
||||
# If no vertices were returned, then the path generation failed
|
||||
if required_vertices.nil?
|
||||
raise NoPathError,
|
||||
"Path generation failed to reach destination (#{src} -> #{dst&.type&.inspect})"
|
||||
@ -91,18 +108,26 @@ module VagrantPlugins
|
||||
|
||||
logger.trace { "required vertices list generation complete for path #{src} -> #{dst}" }
|
||||
|
||||
# Remove all extraneous vertices
|
||||
# Now that we have a path, remove all extraneous vertices
|
||||
# from the graph. We're going to use the graph to provide
|
||||
# us a proper ordering of the vertices below
|
||||
(graph.vertices - required_vertices).each do |vrt|
|
||||
graph.remove_vertex(vrt)
|
||||
end
|
||||
|
||||
# Reverse the graph so the direction of the graph is in its
|
||||
# original state.
|
||||
graph.reverse!
|
||||
|
||||
# Check if the graph is acyclic. If it is not, break any cycles found in the graph.
|
||||
graph.break_cycles!(src) if !graph.acyclic?
|
||||
|
||||
logger.debug { "graph after acyclic breakage:\n#{graph.reverse.inspect}" }
|
||||
|
||||
# Apply topological sort to the graph so we have
|
||||
# a proper order for execution
|
||||
# a proper order for execution. This is required
|
||||
# to ensure that all inputs for a given vertex are
|
||||
# available before executing it.
|
||||
result = Array.new.tap do |path|
|
||||
t = graph.topsort_iterator
|
||||
until t.at_end?
|
||||
@ -110,6 +135,12 @@ module VagrantPlugins
|
||||
end
|
||||
end
|
||||
|
||||
# If the first vertex of the path is not the expected source,
|
||||
# or the last vertex is not the expected destination, then
|
||||
# a valid path was not found. Even though we generated our
|
||||
# list of required vertices, this can still occur if the
|
||||
# resultant graph included any cycles, and breaking the cycle(s)
|
||||
# resulted in orphaned vertices.
|
||||
if result.first != src
|
||||
raise NoPathError,
|
||||
"Initial vertex is not source #{src} != #{result.first}"
|
||||
@ -119,6 +150,7 @@ module VagrantPlugins
|
||||
raise NoPathError,
|
||||
"Final vertex is not destination #{dst} != #{result.last}"
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
rescue NoPathError
|
||||
@ -135,8 +167,19 @@ module VagrantPlugins
|
||||
|
||||
protected
|
||||
|
||||
# Generate a list of required vertices for making a
|
||||
# path from the given source vertex to the given
|
||||
# destination vertex.
|
||||
#
|
||||
# @param src [Vertex] source vertex
|
||||
# @param dst [Vertex] destination vertex
|
||||
# @return [Array<Vertex>, NilClass]
|
||||
def generate_path(src, dst)
|
||||
begin
|
||||
# Find the shortest path based edge weights. Since
|
||||
# we are generating a path against the reversed graph,
|
||||
# the source is our desired destination vertex and the
|
||||
# destination is the source vertex.
|
||||
path = graph.shortest_path(src, dst)
|
||||
logger.trace {
|
||||
o = Array(path).reverse.map { |v|
|
||||
@ -148,38 +191,124 @@ module VagrantPlugins
|
||||
raise NoPathError,
|
||||
"Path generation failed to reach destination (#{dst} -> #{src})"
|
||||
end
|
||||
# Once we have the path, we need to expand the
|
||||
# path to ensure that all vertices in the path
|
||||
# are fully reachable, and if any vertices are
|
||||
# missing that they are added
|
||||
expand_path(path, dst, graph)
|
||||
rescue InvalidVertex => err
|
||||
# An invalid vertex will be flagged when path expansion exposes
|
||||
# a vertex which requires all incoming edges and all the edges
|
||||
# cannot reach the destination vertex. When this happens, we
|
||||
# remove that vertex from the graph and then retry the path
|
||||
# generation.
|
||||
logger.trace { "invalid vertex in path, removing (#{err.vertex})" }
|
||||
graph.remove_vertex(err.vertex)
|
||||
retry
|
||||
end
|
||||
end
|
||||
|
||||
# Expand a given path of vertices by iterating through
|
||||
# the provided path and validating all required vertices
|
||||
# are in the path for any vertices which require all incoming
|
||||
# edges. The result will be the list of original vertices
|
||||
# plus all additional vertices found to be missing.
|
||||
#
|
||||
# @param path [Array<Vertex>] current path
|
||||
# @param dst [Vertex]
|
||||
def expand_path(path, dst, graph)
|
||||
new_path = []
|
||||
new_path = path.dup
|
||||
path.each do |v|
|
||||
new_path << v
|
||||
# Only inspect vertices that require all
|
||||
# incoming edges
|
||||
next if !v.incoming_edges_required
|
||||
logger.trace { "validating incoming edges for vertex #{v}" }
|
||||
# Since we are using a reversed graph, the incoming edges are now
|
||||
# outgoing edges, so we graph the out vertices
|
||||
outs = graph.out_vertices(v)
|
||||
# Make a clone of the graph so we can modify it without affecting
|
||||
# the original
|
||||
g = graph.clone
|
||||
# Remove the original vertex we are currently inspecting from
|
||||
# the cloned graph. This is done to prevent generating a cycle
|
||||
# where the vetex is required in the path to reach the destination
|
||||
g.remove_vertex(v)
|
||||
# Now we find the path from each vertex to the destination
|
||||
outs.each do |src|
|
||||
ipath = g.shortest_path(src, dst)
|
||||
# Since inputs can support named arguments, temporarily update
|
||||
# weights of value vertices.
|
||||
ipath = reweight_for(src, g) do |ig|
|
||||
ig.shortest_path(src, dst)
|
||||
end
|
||||
# Remove any other incoming edges to this input
|
||||
# (graph.out_vertices(src) - [ipath.first]).each do |dv|
|
||||
# graph.remove_edge(src, dv)
|
||||
# end
|
||||
|
||||
# If no path was found an exception is raised that the vertex
|
||||
# in the path (this is the original vertex from the first loop)
|
||||
# is not valid within the path.
|
||||
if ipath.nil? || ipath.empty?
|
||||
logger.trace { "failed to find validating path from #{dst} -> #{src}" }
|
||||
raise InvalidVertex.new(v)
|
||||
else
|
||||
logger.trace { "found validating path from #{dst} -> #{src}" }
|
||||
end
|
||||
ipath = expand_path(ipath, dst, g)
|
||||
new_path += ipath
|
||||
# Remove any vertices that already exist in our final collection
|
||||
# so we don't duplicate the inspection on them
|
||||
ipath = ipath - new_path
|
||||
if !ipath.empty?
|
||||
# Since we have new vertices we need to expand them to
|
||||
# ensure that they all have valid paths to the destination.
|
||||
# The graph provided here is our modified clone to prevent
|
||||
# a cycle where the original vertex is required
|
||||
ipath = expand_path(ipath, dst, g)
|
||||
# And now we add any new vertices which were discovered to
|
||||
# be required
|
||||
new_path |= ipath
|
||||
end
|
||||
end
|
||||
logger.trace { "incoming edge validation complete for vertex #{v}" }
|
||||
end
|
||||
new_path
|
||||
end
|
||||
|
||||
# Modify the weights of value type vertices when the given
|
||||
# vertex is a Vertex::Input and has a name defined. All
|
||||
# value vertices will have their weight reset to the default
|
||||
# value weight, and if a Vertex::NamedValue exists with a
|
||||
# matching name, it will have the NAMED_VALUE_WEIGHT
|
||||
# applied. After the given block is executed, the value
|
||||
# vertices will have their original weights re-applied.
|
||||
#
|
||||
# @param vertex [Vertex] source vertex
|
||||
# @param graph [Graph] graph to modify
|
||||
# @yieldparam [Graph] modified graph (passed instance, not a copy)
|
||||
# @yield block to execute
|
||||
# @return [Object] result of block
|
||||
def reweight_for(vertex, graph)
|
||||
original_weights = {}
|
||||
begin
|
||||
if vertex.is_a?(Vertex::Input) && vertex.name
|
||||
graph.each_vertex do |v|
|
||||
next if !v.is_a?(Vertex::Value)
|
||||
original_weights[v] = v.weight
|
||||
if v.name.to_s == vertex.name.to_s
|
||||
v.weight = Mappers::NAMED_VALUE_WEIGHT
|
||||
else
|
||||
v.weight = Mappers::VALUE_WEIGHT
|
||||
end
|
||||
end
|
||||
end
|
||||
yield graph
|
||||
ensure
|
||||
# Set the weights of the value vertices back to
|
||||
# their original values
|
||||
original_weights.each do |v, w|
|
||||
v.weight = w
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user