File: gecode_solver.rb

package info (click to toggle)
ruby-solve 4.0.4-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,708 kB
  • sloc: ruby: 36,626; makefile: 3
file content (212 lines) | stat: -rw-r--r-- 7,826 bytes parent folder | download
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