job_boss 0.6.0 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.markdown +10 -0
- data/README.markdown +7 -5
- data/job_boss.gemspec +1 -1
- data/lib/job_boss.rb +2 -0
- data/lib/job_boss/batch.rb +44 -0
- data/lib/job_boss/boss.rb +3 -3
- data/lib/job_boss/job.rb +32 -8
- data/lib/job_boss/queuer.rb +6 -2
- data/lib/migrate.rb +3 -0
- data/test/unit/job_test.rb +62 -0
- metadata +11 -3
    
        data/CHANGELOG.markdown
    ADDED
    
    | @@ -0,0 +1,10 @@ | |
| 1 | 
            +
            ## 0.6.5
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            * Ability to create jobs in batches.  Batches allow for better management of jobs.  Batches for the same task will also run in parallel as opposed to serially
         | 
| 4 | 
            +
            * Can now call methods on a batch object that one can call as Job class methods.  Examples: wait_for_jobs, result_hash, time_taken, completed_percent
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            ## 0.6.0
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            * Concept of MIA jobs (boss process marks jobs as MIA when they are killed or otherwise die unhandled)
         | 
| 9 | 
            +
            * Job.wait_for_jobs method takes a block which allows updating of progress percentage
         | 
| 10 | 
            +
            * Output in jobs for MIA and cancelled jobs
         | 
    
        data/README.markdown
    CHANGED
    
    | @@ -67,26 +67,28 @@ But since you don't want to do that right now, it looks something like this: | |
| 67 67 | 
             
            From your Rails code or in a console:
         | 
| 68 68 |  | 
| 69 69 | 
             
                require 'job_boss'
         | 
| 70 | 
            +
                batch = Batch.new
         | 
| 70 71 | 
             
                jobs = (0..1000).collect do |i|
         | 
| 71 | 
            -
                     | 
| 72 | 
            +
                    batch.queue.math.is_prime?(i)
         | 
| 72 73 | 
             
                end
         | 
| 73 74 |  | 
| 74 75 | 
             
            Or:
         | 
| 75 76 |  | 
| 76 77 | 
             
                jobs = []
         | 
| 78 | 
            +
                batch = Batch.new
         | 
| 77 79 | 
             
                Article.select('id').find_in_batches(:batch_size => 10) do |articles|
         | 
| 78 | 
            -
                    jobs <<  | 
| 80 | 
            +
                    jobs << batch.queue.article.refresh_cache(articles.collect(&:id))
         | 
| 79 81 | 
             
                end
         | 
| 80 82 |  | 
| 81 83 | 
             
            job_boss also makes it easy to wait for the jobs to be done and to collect the results into a hash:
         | 
| 82 84 |  | 
| 83 | 
            -
                 | 
| 85 | 
            +
                batch.wait_for_jobs # Will sleep until the jobs are all complete
         | 
| 84 86 |  | 
| 85 | 
            -
                 | 
| 87 | 
            +
                batch.result_hash # => {[0]=>false, [1]=>false, [2]=>true, [3]=>true, [4]=>false, ... }
         | 
| 86 88 |  | 
| 87 89 | 
             
            You can even define a block to provide updates on progress (the value which is passed into the block is a float between 0.0 and 100.0):
         | 
| 88 90 |  | 
| 89 | 
            -
                 | 
| 91 | 
            +
                batch.wait_for_jobs do |progress|
         | 
| 90 92 | 
             
                    puts "We're now at #{progress}%"
         | 
| 91 93 | 
             
                end
         | 
| 92 94 |  | 
    
        data/job_boss.gemspec
    CHANGED
    
    
    
        data/lib/job_boss.rb
    CHANGED
    
    
| @@ -0,0 +1,44 @@ | |
| 1 | 
            +
            require 'active_support'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module JobBoss
         | 
| 4 | 
            +
              class Batch
         | 
| 5 | 
            +
                attr_accessor :batch_id
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                extend ActiveSupport::Memoizable
         | 
| 8 | 
            +
                # Used to queue jobs in a batch
         | 
| 9 | 
            +
                # Usage:
         | 
| 10 | 
            +
                #   batch.queue.math.is_prime?(42)
         | 
| 11 | 
            +
                def queue
         | 
| 12 | 
            +
                  require 'job_boss/queuer'
         | 
| 13 | 
            +
                  Queuer.new(:batch_id => @batch_id)
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
                memoize :queue
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def initialize(batch_id = nil)
         | 
| 18 | 
            +
                  @batch_id = batch_id || Batch.generate_batch_id
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                # Returns ActiveRecord::Relation representing query for jobs in batch
         | 
| 22 | 
            +
                def jobs
         | 
| 23 | 
            +
                  Job.where('batch_id = ?', @batch_id)
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                # Allow calling of Job class methods from a batch which will be called
         | 
| 27 | 
            +
                # on in the scope of the jobs for the batch
         | 
| 28 | 
            +
                # Examples: wait_for_jobs, result_hash, time_taken, completed_percent
         | 
| 29 | 
            +
                def method_missing(sym, *args, &block)
         | 
| 30 | 
            +
                  jobs.send(sym, *args, &block)
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
              private
         | 
| 34 | 
            +
                class << self
         | 
| 35 | 
            +
                  def generate_batch_id(size = 32)
         | 
| 36 | 
            +
                    characters = (0..9).to_a + ('a'..'f').to_a
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    (1..size).collect do |i|
         | 
| 39 | 
            +
                      characters[rand(characters.size)]
         | 
| 40 | 
            +
                    end.join
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
            end
         | 
    
        data/lib/job_boss/boss.rb
    CHANGED
    
    | @@ -92,10 +92,10 @@ module JobBoss | |
| 92 92 | 
             
                      next
         | 
| 93 93 | 
             
                    end
         | 
| 94 94 |  | 
| 95 | 
            -
                    # Go through each pending path so that we don't get stuck just processing
         | 
| 95 | 
            +
                    # Go through each pending path / batch so that we don't get stuck just processing
         | 
| 96 96 | 
             
                    # long running jobs which would leave quicker jobs to suffocate
         | 
| 97 | 
            -
                    Job. | 
| 98 | 
            -
                      job = Job.pending. | 
| 97 | 
            +
                    Job.pending.select('DISTINCT path, batch_id').each do |distinct_job|
         | 
| 98 | 
            +
                      job = Job.pending.order('id').find_by_path_and_batch_id(distinct_job.path, distinct_job.batch_id)
         | 
| 99 99 | 
             
                      next if job.nil?
         | 
| 100 100 |  | 
| 101 101 | 
             
                      job.dispatch(self)
         | 
    
        data/lib/job_boss/job.rb
    CHANGED
    
    | @@ -13,7 +13,7 @@ module JobBoss | |
| 13 13 | 
             
                scope :mia, where("completed_at IS NOT NULL AND status = 'mia'")
         | 
| 14 14 |  | 
| 15 15 | 
             
                def prototype
         | 
| 16 | 
            -
                  self.path + "(#{self.args.join(', ')})"
         | 
| 16 | 
            +
                  self.path + "(#{self.args.collect(&:inspect).join(', ')})"
         | 
| 17 17 | 
             
                end
         | 
| 18 18 |  | 
| 19 19 | 
             
                # Method used by the boss to dispatch an employee
         | 
| @@ -54,6 +54,10 @@ module JobBoss | |
| 54 54 | 
             
                  write_attribute(:result, [value])
         | 
| 55 55 | 
             
                end
         | 
| 56 56 |  | 
| 57 | 
            +
                def batch
         | 
| 58 | 
            +
                  self.batch_id && Batch.new(self.batch_id)
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 57 61 | 
             
                def result
         | 
| 58 62 | 
             
                  # If the result is being called for but the job hasn't been completed, reload
         | 
| 59 63 | 
             
                  # to check to see if there was a result
         | 
| @@ -118,6 +122,15 @@ module JobBoss | |
| 118 122 | 
             
                  employee_pid && employee_host
         | 
| 119 123 | 
             
                end
         | 
| 120 124 |  | 
| 125 | 
            +
                # Is the job running?
         | 
| 126 | 
            +
                def running?
         | 
| 127 | 
            +
                  # If the #running? method is being called for but the job hasn't started, reload
         | 
| 128 | 
            +
                  # to check to see if it has been assigned
         | 
| 129 | 
            +
                  self.reload if started_at.nil? || completed_at.nil?
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                  started_at && !completed_at
         | 
| 132 | 
            +
                end
         | 
| 133 | 
            +
             | 
| 121 134 | 
             
                # How long did the job take?
         | 
| 122 135 | 
             
                def time_taken
         | 
| 123 136 | 
             
                  # If the #time_taken method is being called for but the job doesn't seem to have started/completed
         | 
| @@ -127,6 +140,19 @@ module JobBoss | |
| 127 140 | 
             
                  completed_at - started_at if completed_at && started_at
         | 
| 128 141 | 
             
                end
         | 
| 129 142 |  | 
| 143 | 
            +
                # How long did have set of jobs taken?
         | 
| 144 | 
            +
                # Returns nil if not all jobs are complete
         | 
| 145 | 
            +
                def self.time_taken
         | 
| 146 | 
            +
                  return nil if self.completed.count != self.count
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                  self.maximum(:completed_at) - self.minimum(:started_at)
         | 
| 149 | 
            +
                end
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                # Returns the completion percentage of a set of jobs
         | 
| 152 | 
            +
                def self.completed_percent
         | 
| 153 | 
            +
                  self.completed.count.to_f / self.count.to_f
         | 
| 154 | 
            +
                end
         | 
| 155 | 
            +
             | 
| 130 156 | 
             
                # If the job raised an exception, this method will return the instance of that exception
         | 
| 131 157 | 
             
                # with the message and backtrace
         | 
| 132 158 | 
             
                def error
         | 
| @@ -145,8 +171,9 @@ module JobBoss | |
| 145 171 | 
             
                  # Given a job or an array of jobs
         | 
| 146 172 | 
             
                  # Will cause the process to sleep until all specified jobs have completed
         | 
| 147 173 | 
             
                  # sleep_interval specifies polling period
         | 
| 148 | 
            -
                  def wait_for_jobs(jobs, sleep_interval = 0.5)
         | 
| 174 | 
            +
                  def wait_for_jobs(jobs = nil, sleep_interval = 0.5)
         | 
| 149 175 | 
             
                    jobs = [jobs] if jobs.is_a?(Job)
         | 
| 176 | 
            +
                    jobs = self.scoped if jobs.nil?
         | 
| 150 177 |  | 
| 151 178 | 
             
                    ids = jobs.collect(&:id)
         | 
| 152 179 | 
             
                    Job.uncached do
         | 
| @@ -154,7 +181,7 @@ module JobBoss | |
| 154 181 | 
             
                        sleep(sleep_interval)
         | 
| 155 182 |  | 
| 156 183 | 
             
                        if block_given?
         | 
| 157 | 
            -
                          yield | 
| 184 | 
            +
                          yield((Job.where('id in (?)', ids).completed.count.to_f / jobs.size.to_f) * 100.0)
         | 
| 158 185 | 
             
                        end
         | 
| 159 186 | 
             
                      end
         | 
| 160 187 | 
             
                    end
         | 
| @@ -165,8 +192,9 @@ module JobBoss | |
| 165 192 | 
             
                  # Given a job or an array of jobs
         | 
| 166 193 | 
             
                  # Returns a hash where the keys are the job method arguments and the values are the
         | 
| 167 194 | 
             
                  # results of the job processing
         | 
| 168 | 
            -
                  def result_hash(jobs)
         | 
| 195 | 
            +
                  def result_hash(jobs = nil)
         | 
| 169 196 | 
             
                    jobs = [jobs] if jobs.is_a?(Job)
         | 
| 197 | 
            +
                    jobs = self.scoped if jobs.nil?
         | 
| 170 198 |  | 
| 171 199 | 
             
                    # the #result method automatically reloads the result here if needed but this will
         | 
| 172 200 | 
             
                    # do it in one SQL call
         | 
| @@ -237,10 +265,6 @@ private | |
| 237 265 |  | 
| 238 266 | 
             
                    controller_object.send(action, *args)
         | 
| 239 267 | 
             
                  end
         | 
| 240 | 
            -
             | 
| 241 | 
            -
                  def pending_paths
         | 
| 242 | 
            -
                    self.pending.except(:order).select('DISTINCT path').collect(&:path)
         | 
| 243 | 
            -
                  end
         | 
| 244 268 | 
             
                end
         | 
| 245 269 | 
             
              end
         | 
| 246 270 | 
             
            end
         | 
    
        data/lib/job_boss/queuer.rb
    CHANGED
    
    | @@ -1,5 +1,9 @@ | |
| 1 1 | 
             
            module JobBoss
         | 
| 2 2 | 
             
              class Queuer
         | 
| 3 | 
            +
                def initialize(attributes = nil)
         | 
| 4 | 
            +
                  @attributes = attributes || {}
         | 
| 5 | 
            +
                end
         | 
| 6 | 
            +
              
         | 
| 3 7 | 
             
                def method_missing(method_id, *args)
         | 
| 4 8 | 
             
                  require 'active_support'
         | 
| 5 9 | 
             
                  require 'job_boss/job'
         | 
| @@ -17,8 +21,8 @@ module JobBoss | |
| 17 21 | 
             
                      @class = nil
         | 
| 18 22 | 
             
                      @controller = nil
         | 
| 19 23 |  | 
| 20 | 
            -
                      Job.create(:path => path,
         | 
| 21 | 
            -
             | 
| 24 | 
            +
                      Job.create(@attributes.merge(:path => path,
         | 
| 25 | 
            +
                                                    :args => args))
         | 
| 22 26 | 
             
                    else
         | 
| 23 27 | 
             
                      raise ArgumentError, "Invalid action"
         | 
| 24 28 | 
             
                    end
         | 
    
        data/lib/migrate.rb
    CHANGED
    
    | @@ -2,6 +2,8 @@ class CreateJobs < ActiveRecord::Migration | |
| 2 2 | 
             
              def self.up
         | 
| 3 3 | 
             
                create_table :jobs do |t|
         | 
| 4 4 | 
             
                  t.string :path
         | 
| 5 | 
            +
                  t.string :batch_id
         | 
| 6 | 
            +
             | 
| 5 7 | 
             
                  t.text :args
         | 
| 6 8 | 
             
                  t.text :result
         | 
| 7 9 | 
             
                  t.datetime :started_at
         | 
| @@ -20,6 +22,7 @@ class CreateJobs < ActiveRecord::Migration | |
| 20 22 | 
             
                end
         | 
| 21 23 |  | 
| 22 24 | 
             
                add_index :jobs, :path
         | 
| 25 | 
            +
                add_index :jobs, :batch_id
         | 
| 23 26 | 
             
                add_index :jobs, :status
         | 
| 24 27 |  | 
| 25 28 | 
             
                postgres = (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL')
         | 
    
        data/test/unit/job_test.rb
    CHANGED
    
    | @@ -138,6 +138,68 @@ class DaemonTest < ActiveSupport::TestCase | |
| 138 138 | 
             
                assert_equal 7, job.result
         | 
| 139 139 |  | 
| 140 140 |  | 
| 141 | 
            +
                # Test Batch class
         | 
| 142 | 
            +
                batch = Batch.new
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                jobs = (0..10).collect do |i|
         | 
| 145 | 
            +
                  batch.queue.math.is_prime?(i)
         | 
| 146 | 
            +
                end
         | 
| 147 | 
            +
                assert_equal 0.0, batch.completed_percent
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                batch.wait_for_jobs
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                batch.result_hash.each do |args, result|
         | 
| 152 | 
            +
                  assert_equal MathJobs.new.is_prime?(args.first), result
         | 
| 153 | 
            +
                end
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                assert_equal Job.result_hash(jobs), batch.result_hash
         | 
| 156 | 
            +
                assert_equal 1.0, batch.completed_percent
         | 
| 157 | 
            +
                assert batch.time_taken > 0.5
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                # Test to make sure that different batches run in parallel where non-batched jobs don't
         | 
| 160 | 
            +
             | 
| 161 | 
            +
                batch1 = Batch.new
         | 
| 162 | 
            +
             | 
| 163 | 
            +
                first_jobs = (0..3).collect do
         | 
| 164 | 
            +
                  batch1.queue.sleep.sleep_for(3)
         | 
| 165 | 
            +
                end
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                batch2 = Batch.new
         | 
| 168 | 
            +
                job2 = batch2.queue.sleep.sleep_for(3)
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                sleep(1)
         | 
| 171 | 
            +
             | 
| 172 | 
            +
                assert first_jobs[0,3].all? {|job| job.running? }
         | 
| 173 | 
            +
                assert !first_jobs.last.running?
         | 
| 174 | 
            +
                assert job2.running?
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                sleep(3)
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                assert first_jobs[0,3].all? {|job| !job.running? }
         | 
| 179 | 
            +
                assert first_jobs.last.running?
         | 
| 180 | 
            +
                assert !job2.running?
         | 
| 181 | 
            +
             | 
| 182 | 
            +
                sleep(3)
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                assert !first_jobs.last.running?
         | 
| 185 | 
            +
             | 
| 186 | 
            +
             | 
| 187 | 
            +
                first_jobs = (0..3).collect do
         | 
| 188 | 
            +
                  Boss.queue.sleep.sleep_for(3)
         | 
| 189 | 
            +
                end
         | 
| 190 | 
            +
                job2 = Boss.queue.sleep.sleep_for(3)
         | 
| 191 | 
            +
             | 
| 192 | 
            +
                sleep(1)
         | 
| 193 | 
            +
             | 
| 194 | 
            +
                assert first_jobs.all? {|job| job.running? }
         | 
| 195 | 
            +
                assert !job2.running?
         | 
| 196 | 
            +
                sleep(3)
         | 
| 197 | 
            +
                assert first_jobs.all? {|job| !job.running? }
         | 
| 198 | 
            +
                assert job2.running?
         | 
| 199 | 
            +
                sleep(3)
         | 
| 200 | 
            +
                assert first_jobs.all? {|job| !job.running? }
         | 
| 201 | 
            +
                assert !job2.running?
         | 
| 202 | 
            +
             | 
| 141 203 | 
             
                stop_daemon
         | 
| 142 204 | 
             
              end
         | 
| 143 205 | 
             
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,12 +1,13 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification 
         | 
| 2 2 | 
             
            name: job_boss
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version 
         | 
| 4 | 
            +
              hash: 13
         | 
| 4 5 | 
             
              prerelease: false
         | 
| 5 6 | 
             
              segments: 
         | 
| 6 7 | 
             
              - 0
         | 
| 7 8 | 
             
              - 6
         | 
| 8 | 
            -
              -  | 
| 9 | 
            -
              version: 0.6. | 
| 9 | 
            +
              - 5
         | 
| 10 | 
            +
              version: 0.6.5
         | 
| 10 11 | 
             
            platform: ruby
         | 
| 11 12 | 
             
            authors: 
         | 
| 12 13 | 
             
            - Brian Underwood
         | 
| @@ -14,7 +15,7 @@ autorequire: | |
| 14 15 | 
             
            bindir: bin
         | 
| 15 16 | 
             
            cert_chain: []
         | 
| 16 17 |  | 
| 17 | 
            -
            date:  | 
| 18 | 
            +
            date: 2011-01-06 00:00:00 -05:00
         | 
| 18 19 | 
             
            default_executable: job_boss
         | 
| 19 20 | 
             
            dependencies: 
         | 
| 20 21 | 
             
            - !ruby/object:Gem::Dependency 
         | 
| @@ -25,6 +26,7 @@ dependencies: | |
| 25 26 | 
             
                requirements: 
         | 
| 26 27 | 
             
                - - ">="
         | 
| 27 28 | 
             
                  - !ruby/object:Gem::Version 
         | 
| 29 | 
            +
                    hash: 3
         | 
| 28 30 | 
             
                    segments: 
         | 
| 29 31 | 
             
                    - 0
         | 
| 30 32 | 
             
                    version: "0"
         | 
| @@ -38,6 +40,7 @@ dependencies: | |
| 38 40 | 
             
                requirements: 
         | 
| 39 41 | 
             
                - - ">="
         | 
| 40 42 | 
             
                  - !ruby/object:Gem::Version 
         | 
| 43 | 
            +
                    hash: 3
         | 
| 41 44 | 
             
                    segments: 
         | 
| 42 45 | 
             
                    - 0
         | 
| 43 46 | 
             
                    version: "0"
         | 
| @@ -51,6 +54,7 @@ dependencies: | |
| 51 54 | 
             
                requirements: 
         | 
| 52 55 | 
             
                - - ">="
         | 
| 53 56 | 
             
                  - !ruby/object:Gem::Version 
         | 
| 57 | 
            +
                    hash: 3
         | 
| 54 58 | 
             
                    segments: 
         | 
| 55 59 | 
             
                    - 0
         | 
| 56 60 | 
             
                    version: "0"
         | 
| @@ -66,6 +70,7 @@ extensions: [] | |
| 66 70 | 
             
            extra_rdoc_files: []
         | 
| 67 71 |  | 
| 68 72 | 
             
            files: 
         | 
| 73 | 
            +
            - CHANGELOG.markdown
         | 
| 69 74 | 
             
            - Gemfile
         | 
| 70 75 | 
             
            - MIT-LICENSE
         | 
| 71 76 | 
             
            - README.markdown
         | 
| @@ -134,6 +139,7 @@ files: | |
| 134 139 | 
             
            - init.rb
         | 
| 135 140 | 
             
            - job_boss.gemspec
         | 
| 136 141 | 
             
            - lib/job_boss.rb
         | 
| 142 | 
            +
            - lib/job_boss/batch.rb
         | 
| 137 143 | 
             
            - lib/job_boss/boss.rb
         | 
| 138 144 | 
             
            - lib/job_boss/capistrano.rb
         | 
| 139 145 | 
             
            - lib/job_boss/config.rb
         | 
| @@ -166,6 +172,7 @@ required_ruby_version: !ruby/object:Gem::Requirement | |
| 166 172 | 
             
              requirements: 
         | 
| 167 173 | 
             
              - - ">="
         | 
| 168 174 | 
             
                - !ruby/object:Gem::Version 
         | 
| 175 | 
            +
                  hash: 3
         | 
| 169 176 | 
             
                  segments: 
         | 
| 170 177 | 
             
                  - 0
         | 
| 171 178 | 
             
                  version: "0"
         | 
| @@ -174,6 +181,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 174 181 | 
             
              requirements: 
         | 
| 175 182 | 
             
              - - ">="
         | 
| 176 183 | 
             
                - !ruby/object:Gem::Version 
         | 
| 184 | 
            +
                  hash: 23
         | 
| 177 185 | 
             
                  segments: 
         | 
| 178 186 | 
             
                  - 1
         | 
| 179 187 | 
             
                  - 3
         |