1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 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 134 135 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 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 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
|
require "set" unless defined?(Set)
require_relative "errors"
require_relative "solver/serializer"
module Solve
class GecodeSolver
class << self
# The timeout (in seconds) to use when resolving graphs. Default is 10. This can be
# configured by setting the SOLVE_TIMEOUT environment variable.
#
# @return [Integer]
def timeout
seconds = 30 unless ( seconds = ENV["SOLVE_TIMEOUT"] )
seconds.to_i * 1_000
end
# Attemp to load the dep_selector gem which this solver engine requires.
def activate
require "dep_selector"
rescue LoadError => e
raise Errors::EngineNotAvailable, "dep_selector is not installed, GecodeSolver cannot be used (#{e})"
end
end
# Graph object with references to all known artifacts and dependency
# constraints.
#
# @return [Solve::Graph]
attr_reader :graph
# @example Demands are Arrays of Arrays with an artifact name and optional constraint:
# [['nginx', '= 1.0.0'], ['mysql']]
# @return [Array<String>, Array<Array<String, String>>] demands
attr_reader :demands_array
# @example Basic use:
# graph = Solve::Graph.new
# graph.artifacts("mysql", "1.2.0")
# demands = [["mysql"]]
# GecodeSolver.new(graph, demands)
# @param [Solve::Graph] graph
# @param [Array<String>, Array<Array<String, String>>] demands
# @param [Hash] options - unused, only present for API compatibility with RubySolver
def initialize(graph, demands, options = {})
@ds_graph = DepSelector::DependencyGraph.new
@graph = graph
@demands_array = demands
@timeout_ms = self.class.timeout
end
# The problem demands given as Demand model objects
# @return [Array<Solve::Demand>]
def demands
demands_array.map do |name, constraint|
Demand.new(self, name, constraint)
end
end
# @option options [Boolean] :sorted
# return the solution as a sorted list instead of a Hash
#
# @return [Hash, List] Returns a hash like { "Artifact Name" => "Version",... }
# unless the :sorted option is true, then it returns a list like [["Artifact Name", "Version],...]
# @raise [Errors::NoSolutionError] when the demands cannot be met for the
# given graph.
# @raise [Errors::UnsortableSolutionError] when the :sorted option is true
# and the demands have a solution, but the solution contains a cyclic
# dependency
def resolve(options = {})
solution = solve_demands(demands_as_constraints)
unsorted_solution = solution.inject({}) do |stringified_soln, (name, version)|
stringified_soln[name] = version.to_s
stringified_soln
end
if options[:sorted]
build_sorted_solution(unsorted_solution)
else
unsorted_solution
end
end
private
# DepSelector::DependencyGraph object representing the problem.
attr_reader :ds_graph
# Timeout in milliseconds. Hardcoded to 1s for now.
attr_reader :timeout_ms
# Runs the solver with the set of demands given. If any DepSelector
# exceptions are raised, they are rescued and re-raised
def solve_demands(demands_as_constraints)
selector = DepSelector::Selector.new(ds_graph, (timeout_ms / 1000.0))
selector.find_solution(demands_as_constraints, all_artifacts)
rescue DepSelector::Exceptions::InvalidSolutionConstraints => e
report_invalid_constraints_error(e)
rescue DepSelector::Exceptions::NoSolutionExists => e
report_no_solution_error(e)
rescue DepSelector::Exceptions::TimeBoundExceeded
# DepSelector timed out trying to find the solution. There may or may
# not be a solution.
raise Solve::Errors::NoSolutionError.new(
"The dependency constraints could not be solved in the time allotted."
)
rescue DepSelector::Exceptions::TimeBoundExceededNoSolution
# DepSelector determined there wasn't a solution to the problem, then
# timed out trying to determine which constraints cause the conflict.
raise Solve::Errors::NoSolutionCauseUnknown.new(
"There is a dependency conflict, but the solver could not determine the precise cause in the time allotted."
)
end
# Maps demands to corresponding DepSelector::SolutionConstraint objects.
def demands_as_constraints
@demands_as_constraints ||= demands_array.map do |demands_item|
item_name, constraint_with_operator = demands_item
version_constraint = Semverse::Constraint.new(constraint_with_operator)
DepSelector::SolutionConstraint.new(ds_graph.package(item_name), version_constraint)
end
end
# Maps all artifacts in the graph to DepSelector::Package objects. If not
# already done, artifacts are added to the ds_graph as a necessary side effect.
def all_artifacts
return @all_artifacts if @all_artifacts
populate_ds_graph!
@all_artifacts
end
# Converts artifacts to DepSelector::Package objects and adds them to the
# DepSelector graph. This should only be called once; use #all_artifacts
# to safely get the set of all artifacts.
def populate_ds_graph!
@all_artifacts = Set.new
graph.artifacts.each do |artifact|
add_artifact_to_ds_graph(artifact)
@all_artifacts << ds_graph.package(artifact.name)
end
end
def add_artifact_to_ds_graph(artifact)
package_version = ds_graph.package(artifact.name).add_version(artifact.version)
artifact.dependencies.each do |dependency|
dependency = DepSelector::Dependency.new(ds_graph.package(dependency.name), dependency.constraint)
package_version.dependencies << dependency
end
package_version
end
def report_invalid_constraints_error(e)
non_existent_cookbooks = e.non_existent_packages.inject([]) do |list, constraint|
list << constraint.package.name
end
constrained_to_no_versions = e.constrained_to_no_versions.inject([]) do |list, constraint|
list << [constraint.package.name, constraint.constraint.to_s]
end
raise Solve::Errors::NoSolutionError.new(
"Required artifacts do not exist at the desired version",
missing_artifacts: non_existent_cookbooks,
constraints_excluding_all_artifacts: constrained_to_no_versions
)
end
def report_no_solution_error(e)
most_constrained_cookbooks = e.disabled_most_constrained_packages.inject([]) do |list, package|
list << "#{package.name} = #{package.versions.first}"
end
non_existent_cookbooks = e.disabled_non_existent_packages.inject([]) do |list, package|
list << package.name
end
raise Solve::Errors::NoSolutionError.new(
e.message,
unsatisfiable_demand: e.unsatisfiable_solution_constraint.to_s,
missing_artifacts: non_existent_cookbooks,
artifacts_with_no_satisfactory_version: most_constrained_cookbooks
)
end
def build_sorted_solution(unsorted_solution)
nodes = {}
unsorted_solution.each do |name, version|
nodes[name] = @graph.artifact(name, version).dependencies.map(&:name)
end
# Modified from http://ruby-doc.org/stdlib-1.9.3/libdoc/tsort/rdoc/TSort.html
class << nodes
include TSort
alias tsort_each_node each_key
def tsort_each_child(node, &block)
fetch(node).each(&block)
end
end
begin
sorted_names = nodes.tsort
rescue TSort::Cyclic => e
raise Solve::Errors::UnsortableSolutionError.new(e, unsorted_solution)
end
sorted_names.map do |artifact|
[artifact, unsorted_solution[artifact]]
end
end
end
end
|