sqewer 5.0.0 → 5.0.1
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.
- checksums.yaml +4 -4
- data/.travis.yml +1 -0
- data/ACTIVE_JOB.md +64 -0
- data/FAQ.md +0 -4
- data/Gemfile +4 -0
- data/README.md +4 -0
- data/Rakefile +1 -0
- data/bin/sqewer +7 -0
- data/bin/sqewer_rails +10 -0
- data/lib/sqewer.rb +18 -1
- data/lib/sqewer/atomic_counter.rb +2 -2
- data/lib/sqewer/cli.rb +3 -3
- data/lib/sqewer/connection.rb +16 -16
- data/lib/sqewer/connection_messagebox.rb +4 -4
- data/lib/sqewer/execution_context.rb +5 -5
- data/lib/sqewer/extensions/active_job_adapter.rb +78 -0
- data/lib/sqewer/{contrib → extensions}/appsignal_wrapper.rb +4 -4
- data/lib/sqewer/extensions/railtie.rb +12 -0
- data/lib/sqewer/middleware_stack.rb +7 -7
- data/lib/sqewer/null_logger.rb +1 -1
- data/lib/sqewer/resubmit.rb +17 -0
- data/lib/sqewer/serializer.rb +25 -25
- data/lib/sqewer/simple_job.rb +11 -11
- data/lib/sqewer/state_lock.rb +2 -2
- data/lib/sqewer/submitter.rb +17 -3
- data/lib/sqewer/version.rb +1 -1
- data/lib/sqewer/worker.rb +47 -47
- data/spec/spec_helper.rb +8 -2
- data/spec/sqewer/active_job_spec.rb +113 -0
- data/spec/sqewer/cli_spec.rb +48 -31
- data/spec/sqewer/serializer_spec.rb +51 -56
- data/spec/sqewer/submitter_spec.rb +18 -0
- data/spec/sqewer/worker_spec.rb +11 -9
- data/sqewer.gemspec +21 -5
- metadata +55 -5
- data/lib/sqewer/contrib/performable.rb +0 -23
| @@ -7,19 +7,19 @@ module Sqewer | |
| 7 7 | 
             
                  # Unserialize the job
         | 
| 8 8 | 
             
                  def around_deserialization(serializer, msg_id, msg_payload)
         | 
| 9 9 | 
             
                    return yield unless (defined?(Appsignal) && Appsignal.active?)
         | 
| 10 | 
            -
             | 
| 10 | 
            +
             | 
| 11 11 | 
             
                    Appsignal.monitor_transaction('perform_job.demarshal', 
         | 
| 12 12 | 
             
                      :class => serializer.class.to_s, :params => {:recepit_handle => msg_id}, :method => 'deserialize') do
         | 
| 13 13 | 
             
                      yield
         | 
| 14 14 | 
             
                    end
         | 
| 15 15 | 
             
                  end
         | 
| 16 | 
            -
             | 
| 16 | 
            +
             | 
| 17 17 | 
             
                  # Run the job with Appsignal monitoring.
         | 
| 18 18 | 
             
                  def around_execution(job, context)
         | 
| 19 19 | 
             
                    return yield unless (defined?(Appsignal) && Appsignal.active?)
         | 
| 20 | 
            -
                    
         | 
| 20 | 
            +
                    job_params = job.respond_to?(:to_h) ? job.to_h : {}
         | 
| 21 21 | 
             
                    Appsignal.monitor_transaction('perform_job.sqewer', 
         | 
| 22 | 
            -
                      :class => job.class.to_s, :params =>  | 
| 22 | 
            +
                      :class => job.class.to_s, :params => job_params, :method => 'run') do |t|
         | 
| 23 23 | 
             
                        context['appsignal.transaction'] = t
         | 
| 24 24 | 
             
                      yield
         | 
| 25 25 | 
             
                    end
         | 
| @@ -0,0 +1,12 @@ | |
| 1 | 
            +
            module Sqewer
         | 
| 2 | 
            +
              require 'sqewer/extensions/active_job_adapter' if defined?(::ActiveJob)
         | 
| 3 | 
            +
              
         | 
| 4 | 
            +
              # Loads the Sqewer components that provide ActiveJob compatibility
         | 
| 5 | 
            +
              class Railtie < Rails::Railtie
         | 
| 6 | 
            +
                initializer "sqewer.load_active_job_adapter" do |app|
         | 
| 7 | 
            +
                  if defined?(::ActiveJob)
         | 
| 8 | 
            +
                    Rails.logger.warn "sqewer set as ActiveJob adapter. Make sure to call 'Rails.application.eager_load!` in your worker process"
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
              end
         | 
| 12 | 
            +
            end
         | 
| @@ -1,18 +1,18 @@ | |
| 1 1 | 
             
            # Allows arbitrary wrapping of the job deserialization and job execution procedures
         | 
| 2 2 | 
             
            class Sqewer::MiddlewareStack
         | 
| 3 | 
            -
             | 
| 3 | 
            +
             | 
| 4 4 | 
             
              # Returns the default middleware stack, which is empty (an instance of None).
         | 
| 5 5 | 
             
              #
         | 
| 6 6 | 
             
              # @return [MiddlewareStack] the default empty stack
         | 
| 7 7 | 
             
              def self.default
         | 
| 8 8 | 
             
                @instance ||= new
         | 
| 9 9 | 
             
              end
         | 
| 10 | 
            -
             | 
| 10 | 
            +
             | 
| 11 11 | 
             
              # Creates a new MiddlewareStack. Once created, handlers can be added using `:<<`
         | 
| 12 12 | 
             
              def initialize
         | 
| 13 13 | 
             
                @handlers = []
         | 
| 14 14 | 
             
              end
         | 
| 15 | 
            -
             | 
| 15 | 
            +
             | 
| 16 16 | 
             
              # Adds a handler. The handler should respond to :around_deserialization and #around_execution.
         | 
| 17 17 | 
             
              #
         | 
| 18 18 | 
             
              # @param handler[#around_deserializarion, #around_execution] The middleware item to insert
         | 
| @@ -21,13 +21,13 @@ class Sqewer::MiddlewareStack | |
| 21 21 | 
             
                @handlers << handler
         | 
| 22 22 | 
             
                # TODO: cache the wrapping proc
         | 
| 23 23 | 
             
              end
         | 
| 24 | 
            -
             | 
| 24 | 
            +
             | 
| 25 25 | 
             
              def around_execution(job, context, &inner_block)
         | 
| 26 26 | 
             
                return yield if @handlers.empty?
         | 
| 27 | 
            -
             | 
| 27 | 
            +
             | 
| 28 28 | 
             
                responders = @handlers.select{|e| e.respond_to?(:around_execution) }
         | 
| 29 29 | 
             
                responders.reverse.inject(inner_block) {|outer_block, middleware_object|
         | 
| 30 | 
            -
                  ->{ | 
| 30 | 
            +
                  ->{
         | 
| 31 31 | 
             
                    middleware_object.public_send(:around_execution, job, context, &outer_block)
         | 
| 32 32 | 
             
                  }
         | 
| 33 33 | 
             
                }.call
         | 
| @@ -35,7 +35,7 @@ class Sqewer::MiddlewareStack | |
| 35 35 |  | 
| 36 36 | 
             
              def around_deserialization(serializer, message_id, message_body, &inner_block)
         | 
| 37 37 | 
             
                return yield if @handlers.empty?
         | 
| 38 | 
            -
             | 
| 38 | 
            +
             | 
| 39 39 | 
             
                responders = @handlers.select{|e| e.respond_to?(:around_deserialization) }
         | 
| 40 40 | 
             
                responders.reverse.inject(inner_block) {|outer_block, middleware_object|
         | 
| 41 41 | 
             
                  ->{ middleware_object.public_send(:around_deserialization, serializer, message_id, message_body, &outer_block) }
         | 
    
        data/lib/sqewer/null_logger.rb
    CHANGED
    
    
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            module Sqewer
         | 
| 2 | 
            +
              class Resubmit
         | 
| 3 | 
            +
                attr_reader :job
         | 
| 4 | 
            +
                attr_reader :execute_after
         | 
| 5 | 
            +
                
         | 
| 6 | 
            +
                def initialize(job_to_resubmit, execute_after_timestamp)
         | 
| 7 | 
            +
                  @job = job_to_resubmit
         | 
| 8 | 
            +
                  @execute_after = execute_after_timestamp
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
                
         | 
| 11 | 
            +
                def run(ctx)
         | 
| 12 | 
            +
                  # Take the maximum delay period SQS allows
         | 
| 13 | 
            +
                  required_delay = (@execute_after - Time.now.to_i)
         | 
| 14 | 
            +
                  ctx.submit!(@job, delay_seconds: required_delay)
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
    
        data/lib/sqewer/serializer.rb
    CHANGED
    
    | @@ -4,7 +4,7 @@ | |
| 4 4 | 
             
            # custom job objects from S3 bucket notifications, you might want to override this
         | 
| 5 5 | 
             
            # class and feed the overridden instance to {Sqewer::Worker}.
         | 
| 6 6 | 
             
            class Sqewer::Serializer
         | 
| 7 | 
            -
             | 
| 7 | 
            +
             | 
| 8 8 | 
             
              # Returns the default Serializer, of which we store one instance
         | 
| 9 9 | 
             
              # (because the serializer is stateless).
         | 
| 10 10 | 
             
              #
         | 
| @@ -12,10 +12,9 @@ class Sqewer::Serializer | |
| 12 12 | 
             
              def self.default
         | 
| 13 13 | 
             
                @instance ||= new
         | 
| 14 14 | 
             
              end
         | 
| 15 | 
            -
             | 
| 15 | 
            +
             | 
| 16 16 | 
             
              AnonymousJobClass = Class.new(StandardError)
         | 
| 17 | 
            -
             | 
| 18 | 
            -
              
         | 
| 17 | 
            +
             | 
| 19 18 | 
             
              # Instantiate a Job object from a message body string. If the
         | 
| 20 19 | 
             
              # returned result is `nil`, the job will be skipped.
         | 
| 21 20 | 
             
              #
         | 
| @@ -24,48 +23,49 @@ class Sqewer::Serializer | |
| 24 23 | 
             
              def unserialize(message_body)
         | 
| 25 24 | 
             
                job_ticket_hash = JSON.parse(message_body, symbolize_names: true)
         | 
| 26 25 | 
             
                raise "Job ticket must unmarshal into a Hash" unless job_ticket_hash.is_a?(Hash)
         | 
| 27 | 
            -
             | 
| 28 | 
            -
                job_ticket_hash = convert_old_ticket_format(job_ticket_hash) if job_ticket_hash[:job_class]
         | 
| 29 | 
            -
                
         | 
| 26 | 
            +
             | 
| 30 27 | 
             
                # Use fetch() to raise a descriptive KeyError if none
         | 
| 31 28 | 
             
                job_class_name = job_ticket_hash.delete(:_job_class)
         | 
| 32 29 | 
             
                raise ":_job_class not set in the ticket" unless job_class_name
         | 
| 33 30 | 
             
                job_class = Kernel.const_get(job_class_name)
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                # Grab the parameter that is responsible for executing the job later. If it is not set,
         | 
| 33 | 
            +
                # use a default that will put us ahead of that execution deadline from the start.
         | 
| 34 | 
            +
                t = Time.now.to_i
         | 
| 35 | 
            +
                execute_after = job_ticket_hash.fetch(:_execute_after) { t - 5 }
         | 
| 34 36 |  | 
| 35 37 | 
             
                job_params = job_ticket_hash.delete(:_job_params)
         | 
| 36 | 
            -
                if job_params.nil? || job_params.empty?
         | 
| 38 | 
            +
                job = if job_params.nil? || job_params.empty?
         | 
| 37 39 | 
             
                  job_class.new # no args
         | 
| 38 40 | 
             
                else
         | 
| 39 | 
            -
                   | 
| 40 | 
            -
                    job_class.new(**job_params) # The rest of the message are keyword arguments for the job
         | 
| 41 | 
            -
                  rescue ArgumentError => e
         | 
| 42 | 
            -
                    raise ArityMismatch, "Could not instantiate #{job_class} because it did not accept the arguments #{job_params.inspect}"
         | 
| 43 | 
            -
                  end
         | 
| 41 | 
            +
                  job_class.new(**job_params) # The rest of the message are keyword arguments for the job
         | 
| 44 42 | 
             
                end
         | 
| 43 | 
            +
                
         | 
| 44 | 
            +
                # If the job is not up for execution now, wrap it with something that will 
         | 
| 45 | 
            +
                # re-submit it for later execution when the run() method is called
         | 
| 46 | 
            +
                return ::Sqewer::Resubmit.new(job, execute_after) if execute_after > t
         | 
| 47 | 
            +
                
         | 
| 48 | 
            +
                job
         | 
| 45 49 | 
             
              end
         | 
| 46 | 
            -
             | 
| 50 | 
            +
             | 
| 47 51 | 
             
              # Converts the given Job into a string, which can be submitted to the queue
         | 
| 48 52 | 
             
              #
         | 
| 49 53 | 
             
              # @param job[#to_h] an object that supports `to_h`
         | 
| 54 | 
            +
              # @param execute_after_timestamp[#to_i, nil] the Unix timestamp after which the job may be executed
         | 
| 50 55 | 
             
              # @return [String] serialized string ready to be put into the queue
         | 
| 51 | 
            -
              def serialize(job)
         | 
| 56 | 
            +
              def serialize(job, execute_after_timestamp = nil)
         | 
| 52 57 | 
             
                job_class_name = job.class.to_s
         | 
| 53 | 
            -
             | 
| 58 | 
            +
             | 
| 54 59 | 
             
                begin
         | 
| 55 60 | 
             
                  Kernel.const_get(job_class_name)
         | 
| 56 61 | 
             
                rescue NameError
         | 
| 57 62 | 
             
                  raise AnonymousJobClass, "The class of #{job.inspect} could not be resolved and will not restore to a Job"
         | 
| 58 63 | 
             
                end
         | 
| 59 | 
            -
             | 
| 64 | 
            +
             | 
| 60 65 | 
             
                job_params = job.respond_to?(:to_h) ? job.to_h : nil
         | 
| 61 66 | 
             
                job_ticket_hash = {_job_class: job_class_name, _job_params: job_params}
         | 
| 67 | 
            +
                job_ticket_hash[:_execute_after] = execute_after_timestamp.to_i if execute_after_timestamp
         | 
| 68 | 
            +
                
         | 
| 62 69 | 
             
                JSON.dump(job_ticket_hash)
         | 
| 63 70 | 
             
              end
         | 
| 64 | 
            -
             | 
| 65 | 
            -
              private
         | 
| 66 | 
            -
              
         | 
| 67 | 
            -
              def convert_old_ticket_format(hash_of_properties)
         | 
| 68 | 
            -
                job_class = hash_of_properties.delete(:job_class)
         | 
| 69 | 
            -
                {_job_class: job_class, _job_params: hash_of_properties}
         | 
| 70 | 
            -
              end
         | 
| 71 | 
            -
            end
         | 
| 71 | 
            +
            end
         | 
    
        data/lib/sqewer/simple_job.rb
    CHANGED
    
    | @@ -7,19 +7,19 @@ | |
| 7 7 | 
             
            module Sqewer::SimpleJob
         | 
| 8 8 | 
             
              UnknownJobAttribute = Class.new(StandardError)
         | 
| 9 9 | 
             
              MissingAttribute = Class.new(StandardError)
         | 
| 10 | 
            -
             | 
| 10 | 
            +
             | 
| 11 11 | 
             
              EQ_END = /(\w+)(\=)$/
         | 
| 12 | 
            -
             | 
| 12 | 
            +
             | 
| 13 13 | 
             
              # Returns the list of methods on the object that have corresponding accessors.
         | 
| 14 14 | 
             
              # This is then used by #inspect to compose a list of the job parameters, formatted
         | 
| 15 15 | 
             
              # as an inspected Hash.
         | 
| 16 16 | 
             
              #
         | 
| 17 | 
            -
              # @return [Array<Symbol>] the array of attributes to show via inspect | 
| 17 | 
            +
              # @return [Array<Symbol>] the array of attributes to show via inspect
         | 
| 18 18 | 
             
              def inspectable_attributes
         | 
| 19 19 | 
             
                # All the attributes that have accessors
         | 
| 20 20 | 
             
                methods.grep(EQ_END).map{|e| e.to_s.gsub(EQ_END, '\1')}.map(&:to_sym)
         | 
| 21 21 | 
             
              end
         | 
| 22 | 
            -
             | 
| 22 | 
            +
             | 
| 23 23 | 
             
              # Returns the inspection string with the job and all of it's instantiation keyword attributes.
         | 
| 24 24 | 
             
              # If `inspectable_attributes` has been overridden, the attributes returned by that method will be the
         | 
| 25 25 | 
             
              # ones returned in the inspection string.
         | 
| @@ -36,7 +36,7 @@ module Sqewer::SimpleJob | |
| 36 36 | 
             
                end
         | 
| 37 37 | 
             
                "<#{self.class}:#{h.inspect}>"
         | 
| 38 38 | 
             
              end
         | 
| 39 | 
            -
             | 
| 39 | 
            +
             | 
| 40 40 | 
             
              # Initializes a new Job with the given job args. Will check for presence of
         | 
| 41 41 | 
             
              # accessor methods for each of the arguments, and call them with the arguments given.
         | 
| 42 42 | 
             
              #
         | 
| @@ -49,30 +49,30 @@ module Sqewer::SimpleJob | |
| 49 49 | 
             
                @simple_job_args = jobargs.keys
         | 
| 50 50 | 
             
                touched_attributes = Set.new
         | 
| 51 51 | 
             
                jobargs.each do |(k,v)|
         | 
| 52 | 
            -
             | 
| 52 | 
            +
             | 
| 53 53 | 
             
                  accessor = "#{k}="
         | 
| 54 54 | 
             
                  touched_attributes << k
         | 
| 55 55 | 
             
                  unless respond_to?(accessor)
         | 
| 56 56 | 
             
                    raise UnknownJobAttribute, "Unknown attribute #{k.inspect} for #{self.class}" 
         | 
| 57 57 | 
             
                  end
         | 
| 58 | 
            -
             | 
| 58 | 
            +
             | 
| 59 59 | 
             
                  send("#{k}=", v)
         | 
| 60 60 | 
             
                end
         | 
| 61 | 
            -
             | 
| 61 | 
            +
             | 
| 62 62 | 
             
                accessors = methods.grep(EQ_END).map{|method_name| method_name.to_s.gsub(EQ_END, '\1').to_sym }
         | 
| 63 63 | 
             
                settable_attributes = Set.new(accessors)
         | 
| 64 64 | 
             
                missing_attributes = settable_attributes - touched_attributes
         | 
| 65 | 
            -
             | 
| 65 | 
            +
             | 
| 66 66 | 
             
                missing_attributes.each do | attr |
         | 
| 67 67 | 
             
                  raise MissingAttribute, "Missing job attribute #{attr.inspect}"
         | 
| 68 68 | 
             
                end
         | 
| 69 69 | 
             
              end
         | 
| 70 | 
            -
             | 
| 70 | 
            +
             | 
| 71 71 | 
             
              def to_h
         | 
| 72 72 | 
             
                keys_and_values = @simple_job_args.each_with_object({}) do |k, h|
         | 
| 73 73 | 
             
                  h[k] = send(k)
         | 
| 74 74 | 
             
                end
         | 
| 75 | 
            -
             | 
| 75 | 
            +
             | 
| 76 76 | 
             
                keys_and_values
         | 
| 77 77 | 
             
              end
         | 
| 78 78 | 
             
            end
         | 
    
        data/lib/sqewer/state_lock.rb
    CHANGED
    
    | @@ -13,11 +13,11 @@ class Sqewer::StateLock < SimpleDelegator | |
| 13 13 | 
             
                m.permit_transition :starting => :failed # Failed to start
         | 
| 14 14 | 
             
                __setobj__(m)
         | 
| 15 15 | 
             
              end
         | 
| 16 | 
            -
             | 
| 16 | 
            +
             | 
| 17 17 | 
             
              def in_state?(some_state)
         | 
| 18 18 | 
             
                @m.synchronize { __getobj__.in_state?(some_state) }
         | 
| 19 19 | 
             
              end
         | 
| 20 | 
            -
             | 
| 20 | 
            +
             | 
| 21 21 | 
             
              def transition!(to_state)
         | 
| 22 22 | 
             
                @m.synchronize { __getobj__.transition!(to_state) }
         | 
| 23 23 | 
             
              end
         | 
    
        data/lib/sqewer/submitter.rb
    CHANGED
    
    | @@ -3,14 +3,28 @@ | |
| 3 3 | 
             
            # and the serializer (something that responds to `#serialize`) to
         | 
| 4 4 | 
             
            # convert the job into the string that will be put in the queue.
         | 
| 5 5 | 
             
            class Sqewer::Submitter < Struct.new(:connection, :serializer)
         | 
| 6 | 
            -
             | 
| 6 | 
            +
             | 
| 7 7 | 
             
              # Returns a default Submitter, configured with the default connection
         | 
| 8 8 | 
             
              # and the default serializer.
         | 
| 9 9 | 
             
              def self.default
         | 
| 10 10 | 
             
                new(Sqewer::Connection.default, Sqewer::Serializer.default)
         | 
| 11 11 | 
             
              end
         | 
| 12 | 
            -
             | 
| 12 | 
            +
             | 
| 13 13 | 
             
              def submit!(job, **kwargs_for_send)
         | 
| 14 | 
            -
                 | 
| 14 | 
            +
                message_body = if delay_by_seconds = kwargs_for_send[:delay_seconds]
         | 
| 15 | 
            +
                  clamped_delay = clamp_delay(delay_by_seconds)
         | 
| 16 | 
            +
                  kwargs_for_send[:delay_seconds] = clamped_delay
         | 
| 17 | 
            +
                  # Pass the actual delay value to the serializer, to be stored in executed_at
         | 
| 18 | 
            +
                  serializer.serialize(job, Time.now.to_i + delay_by_seconds)
         | 
| 19 | 
            +
                else
         | 
| 20 | 
            +
                  serializer.serialize(job)
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
                connection.send_message(message_body, **kwargs_for_send)
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
              
         | 
| 25 | 
            +
              private
         | 
| 26 | 
            +
              
         | 
| 27 | 
            +
              def clamp_delay(delay)
         | 
| 28 | 
            +
                [1, 899, delay].sort[1]
         | 
| 15 29 | 
             
              end
         | 
| 16 30 | 
             
            end
         | 
    
        data/lib/sqewer/version.rb
    CHANGED
    
    
    
        data/lib/sqewer/worker.rb
    CHANGED
    
    | @@ -8,38 +8,38 @@ class Sqewer::Worker | |
| 8 8 | 
             
              DEFAULT_NUM_THREADS = 4
         | 
| 9 9 | 
             
              SLEEP_SECONDS_ON_EMPTY_QUEUE = 1
         | 
| 10 10 | 
             
              THROTTLE_FACTOR = 2
         | 
| 11 | 
            -
             | 
| 11 | 
            +
             | 
| 12 12 | 
             
              # @return [Logger] The logger used for job execution
         | 
| 13 13 | 
             
              attr_reader :logger
         | 
| 14 | 
            -
             | 
| 14 | 
            +
             | 
| 15 15 | 
             
              # @return [Sqewer::Connection] The connection for sending and receiving messages
         | 
| 16 16 | 
             
              attr_reader :connection
         | 
| 17 | 
            -
             | 
| 17 | 
            +
             | 
| 18 18 | 
             
              # @return [Sqewer::Serializer] The serializer for unmarshalling and marshalling
         | 
| 19 19 | 
             
              attr_reader :serializer
         | 
| 20 | 
            -
             | 
| 20 | 
            +
             | 
| 21 21 | 
             
              # @return [Sqewer::MiddlewareStack] The stack used when executing the job
         | 
| 22 22 | 
             
              attr_reader :middleware_stack
         | 
| 23 | 
            -
             | 
| 23 | 
            +
             | 
| 24 24 | 
             
              # @return [Class] The class to use when instantiating the execution context
         | 
| 25 25 | 
             
              attr_reader :execution_context_class
         | 
| 26 | 
            -
             | 
| 26 | 
            +
             | 
| 27 27 | 
             
              # @return [Class] The class used to create the Submitter used by jobs to spawn other jobs
         | 
| 28 28 | 
             
              attr_reader :submitter_class
         | 
| 29 | 
            -
             | 
| 29 | 
            +
             | 
| 30 30 | 
             
              # @return [Array<Thread>] all the currently running threads of the Worker
         | 
| 31 31 | 
             
              attr_reader :threads
         | 
| 32 | 
            -
             | 
| 32 | 
            +
             | 
| 33 33 | 
             
              # @return [Fixnum] the number of worker threads set up for this Worker
         | 
| 34 34 | 
             
              attr_reader :num_threads
         | 
| 35 | 
            -
             | 
| 36 | 
            -
              # Returns  | 
| 35 | 
            +
             | 
| 36 | 
            +
              # Returns a Worker instance, configured based on the default components
         | 
| 37 37 | 
             
              #
         | 
| 38 38 | 
             
              # @return [Sqewer::Worker]
         | 
| 39 39 | 
             
              def self.default
         | 
| 40 | 
            -
                 | 
| 40 | 
            +
                new
         | 
| 41 41 | 
             
              end
         | 
| 42 | 
            -
             | 
| 42 | 
            +
             | 
| 43 43 | 
             
              # Creates a new Worker. The Worker, unlike it is in the Rails tradition, is only responsible for
         | 
| 44 44 | 
             
              # the actual processing of jobs, and not for the job arguments.
         | 
| 45 45 | 
             
              #
         | 
| @@ -58,7 +58,7 @@ class Sqewer::Worker | |
| 58 58 | 
             
                  middleware_stack: Sqewer::MiddlewareStack.default,
         | 
| 59 59 | 
             
                  logger: Logger.new($stderr),
         | 
| 60 60 | 
             
                  num_threads: DEFAULT_NUM_THREADS)
         | 
| 61 | 
            -
             | 
| 61 | 
            +
             | 
| 62 62 | 
             
                @logger = logger
         | 
| 63 63 | 
             
                @connection = connection
         | 
| 64 64 | 
             
                @serializer = serializer
         | 
| @@ -66,38 +66,38 @@ class Sqewer::Worker | |
| 66 66 | 
             
                @execution_context_class = execution_context_class
         | 
| 67 67 | 
             
                @submitter_class = submitter_class
         | 
| 68 68 | 
             
                @num_threads = num_threads
         | 
| 69 | 
            -
             | 
| 69 | 
            +
             | 
| 70 70 | 
             
                @threads = []
         | 
| 71 | 
            -
             | 
| 71 | 
            +
             | 
| 72 72 | 
             
                raise ArgumentError, "num_threads must be > 0" unless num_threads > 0
         | 
| 73 | 
            -
             | 
| 73 | 
            +
             | 
| 74 74 | 
             
                @execution_counter = Sqewer::AtomicCounter.new
         | 
| 75 | 
            -
             | 
| 75 | 
            +
             | 
| 76 76 | 
             
                @state = Sqewer::StateLock.new
         | 
| 77 77 | 
             
              end
         | 
| 78 | 
            -
             | 
| 78 | 
            +
             | 
| 79 79 | 
             
              # Start listening on the queue, spin up a number of consumer threads that will execute the jobs.
         | 
| 80 80 | 
             
              #
         | 
| 81 81 | 
             
              # @param num_threads[Fixnum] the number of consumer/executor threads to spin up
         | 
| 82 82 | 
             
              # @return [void]
         | 
| 83 83 | 
             
              def start
         | 
| 84 84 | 
             
                @state.transition! :starting
         | 
| 85 | 
            -
             | 
| 85 | 
            +
             | 
| 86 86 | 
             
                @logger.info { '[worker] Starting with %d consumer threads' % @num_threads }
         | 
| 87 87 | 
             
                @execution_queue = Queue.new
         | 
| 88 | 
            -
             | 
| 88 | 
            +
             | 
| 89 89 | 
             
                consumers = (1..@num_threads).map do
         | 
| 90 90 | 
             
                  Thread.new do
         | 
| 91 91 | 
             
                    catch(:goodbye) { loop {take_and_execute} }
         | 
| 92 92 | 
             
                  end
         | 
| 93 93 | 
             
                end
         | 
| 94 | 
            -
             | 
| 94 | 
            +
             | 
| 95 95 | 
             
                # Create the provider thread. When the execution queue is exhausted,
         | 
| 96 96 | 
             
                # grab new messages and place them on the local queue.
         | 
| 97 97 | 
             
                provider = Thread.new do
         | 
| 98 98 | 
             
                  loop do
         | 
| 99 99 | 
             
                    break if stopping?
         | 
| 100 | 
            -
             | 
| 100 | 
            +
             | 
| 101 101 | 
             
                    if queue_has_capacity?
         | 
| 102 102 | 
             
                      messages = @connection.receive_messages
         | 
| 103 103 | 
             
                      if messages.any?
         | 
| @@ -110,12 +110,12 @@ class Sqewer::Worker | |
| 110 110 | 
             
                    else
         | 
| 111 111 | 
             
                      @logger.debug { "[worker] Cache is full (%d items), postponing receive" % @execution_queue.length }
         | 
| 112 112 | 
             
                      sleep SLEEP_SECONDS_ON_EMPTY_QUEUE
         | 
| 113 | 
            -
                    end | 
| 113 | 
            +
                    end
         | 
| 114 114 | 
             
                  end
         | 
| 115 115 | 
             
                end
         | 
| 116 | 
            -
             | 
| 116 | 
            +
             | 
| 117 117 | 
             
                @threads = consumers + [provider]
         | 
| 118 | 
            -
             | 
| 118 | 
            +
             | 
| 119 119 | 
             
                # If any of our threads are already dead, it means there is some misconfiguration and startup failed
         | 
| 120 120 | 
             
                if @threads.any?{|t| !t.alive? }
         | 
| 121 121 | 
             
                  @threads.map(&:kill)
         | 
| @@ -126,7 +126,7 @@ class Sqewer::Worker | |
| 126 126 | 
             
                  @logger.info { '[worker] Started, %d consumer threads' % consumers.length }
         | 
| 127 127 | 
             
                end
         | 
| 128 128 | 
             
              end
         | 
| 129 | 
            -
             | 
| 129 | 
            +
             | 
| 130 130 | 
             
              # Attempts to softly stop the running consumers and the producer. Once the call is made,
         | 
| 131 131 | 
             
              # all the threads will stop after the local cache of messages is emptied. This is to ensure that
         | 
| 132 132 | 
             
              # message drops do not happen just because the worker is about to be terminated.
         | 
| @@ -140,20 +140,20 @@ class Sqewer::Worker | |
| 140 140 | 
             
                loop do
         | 
| 141 141 | 
             
                  n_live = @threads.select(&:alive?).length
         | 
| 142 142 | 
             
                  break if n_live.zero?
         | 
| 143 | 
            -
             | 
| 143 | 
            +
             | 
| 144 144 | 
             
                  n_dead = @threads.length - n_live
         | 
| 145 145 | 
             
                  @logger.info { '[worker] Staged shutdown, %d threads alive, %d have quit, %d jobs in local cache' %
         | 
| 146 146 | 
             
                    [n_live, n_dead, @execution_queue.length] }
         | 
| 147 | 
            -
             | 
| 147 | 
            +
             | 
| 148 148 | 
             
                  sleep 2
         | 
| 149 149 | 
             
                end
         | 
| 150 | 
            -
             | 
| 150 | 
            +
             | 
| 151 151 | 
             
                @threads.map(&:join)
         | 
| 152 152 | 
             
                @logger.info { '[worker] Stopped'}
         | 
| 153 153 | 
             
                @state.transition! :stopped
         | 
| 154 154 | 
             
                true
         | 
| 155 155 | 
             
              end
         | 
| 156 | 
            -
             | 
| 156 | 
            +
             | 
| 157 157 | 
             
              # Peforms a hard shutdown by killing all the threads
         | 
| 158 158 | 
             
              def kill
         | 
| 159 159 | 
             
                @state.transition! :stopping
         | 
| @@ -162,7 +162,7 @@ class Sqewer::Worker | |
| 162 162 | 
             
                @logger.info { '[worker] Stopped'}
         | 
| 163 163 | 
             
                @state.transition! :stopped
         | 
| 164 164 | 
             
              end
         | 
| 165 | 
            -
             | 
| 165 | 
            +
             | 
| 166 166 | 
             
              # Prints the status and the backtraces of all controlled threads to the logger
         | 
| 167 167 | 
             
              def debug_thread_information!
         | 
| 168 168 | 
             
                @threads.each do | t |
         | 
| @@ -170,48 +170,48 @@ class Sqewer::Worker | |
| 170 170 | 
             
                  @logger.debug { t.backtrace }
         | 
| 171 171 | 
             
                end
         | 
| 172 172 | 
             
              end
         | 
| 173 | 
            -
             | 
| 173 | 
            +
             | 
| 174 174 | 
             
              private
         | 
| 175 | 
            -
             | 
| 175 | 
            +
             | 
| 176 176 | 
             
              def stopping?
         | 
| 177 177 | 
             
                @state.in_state?(:stopping)
         | 
| 178 178 | 
             
              end
         | 
| 179 | 
            -
             | 
| 179 | 
            +
             | 
| 180 180 | 
             
              def queue_has_capacity?
         | 
| 181 181 | 
             
                @execution_queue.length < (@num_threads * THROTTLE_FACTOR)
         | 
| 182 182 | 
             
              end
         | 
| 183 | 
            -
             | 
| 183 | 
            +
             | 
| 184 184 | 
             
              def handle_message(message)
         | 
| 185 185 | 
             
                return unless message.receipt_handle
         | 
| 186 | 
            -
             | 
| 186 | 
            +
             | 
| 187 187 | 
             
                # Create a messagebox that buffers all the calls to Connection, so that
         | 
| 188 188 | 
             
                # we can send out those commands in one go (without interfering with senders
         | 
| 189 189 | 
             
                # on other threads, as it seems the Aws::SQS::Client is not entirely
         | 
| 190 190 | 
             
                # thread-safe - or at least not it's HTTP client part).
         | 
| 191 191 | 
             
                box = Sqewer::ConnectionMessagebox.new(connection)
         | 
| 192 192 | 
             
                return box.delete_message(message.receipt_handle) unless message.has_body?
         | 
| 193 | 
            -
             | 
| 193 | 
            +
             | 
| 194 194 | 
             
                job = middleware_stack.around_deserialization(serializer, message.receipt_handle, message.body) do
         | 
| 195 195 | 
             
                  serializer.unserialize(message.body)
         | 
| 196 196 | 
             
                end
         | 
| 197 197 | 
             
                return unless job
         | 
| 198 | 
            -
             | 
| 198 | 
            +
             | 
| 199 199 | 
             
                submitter = submitter_class.new(box, serializer)
         | 
| 200 200 | 
             
                context = execution_context_class.new(submitter, {'logger' => logger})
         | 
| 201 | 
            -
             | 
| 201 | 
            +
             | 
| 202 202 | 
             
                t = Time.now
         | 
| 203 203 | 
             
                middleware_stack.around_execution(job, context) do
         | 
| 204 204 | 
             
                  job.method(:run).arity.zero? ? job.run : job.run(context)
         | 
| 205 205 | 
             
                end
         | 
| 206 206 | 
             
                box.delete_message(message.receipt_handle)
         | 
| 207 | 
            -
             | 
| 207 | 
            +
             | 
| 208 208 | 
             
                delta = Time.now - t
         | 
| 209 209 | 
             
                logger.info { "[worker] Finished %s in %0.2fs" % [job.inspect, delta] }
         | 
| 210 210 | 
             
              ensure
         | 
| 211 211 | 
             
                n_flushed = box.flush!
         | 
| 212 212 | 
             
                logger.debug { "[worker] Flushed %d connection commands" % n_flushed } if n_flushed.nonzero?
         | 
| 213 213 | 
             
              end
         | 
| 214 | 
            -
             | 
| 214 | 
            +
             | 
| 215 215 | 
             
              def take_and_execute
         | 
| 216 216 | 
             
                message = @execution_queue.pop(nonblock=true)
         | 
| 217 217 | 
             
                handle_message(message)
         | 
| @@ -222,31 +222,31 @@ class Sqewer::Worker | |
| 222 222 | 
             
                @logger.error { '[worker] Failed "%s..." with %s: %s' % [message.inspect[0..32], e.class, e.message] }
         | 
| 223 223 | 
             
                e.backtrace.each { |s| @logger.error{"\t#{s}"} }
         | 
| 224 224 | 
             
              end
         | 
| 225 | 
            -
             | 
| 225 | 
            +
             | 
| 226 226 | 
             
              def perform(message)
         | 
| 227 227 | 
             
                # Create a messagebox that buffers all the calls to Connection, so that
         | 
| 228 228 | 
             
                # we can send out those commands in one go (without interfering with senders
         | 
| 229 229 | 
             
                # on other threads, as it seems the Aws::SQS::Client is not entirely
         | 
| 230 230 | 
             
                # thread-safe - or at least not it's HTTP client part).
         | 
| 231 231 | 
             
                box = Sqewer::ConnectionMessagebox.new(connection)
         | 
| 232 | 
            -
             | 
| 232 | 
            +
             | 
| 233 233 | 
             
                job = middleware_stack.around_deserialization(serializer, message.receipt_handle, message.body) do
         | 
| 234 234 | 
             
                  serializer.unserialize(message.body)
         | 
| 235 235 | 
             
                end
         | 
| 236 236 | 
             
                return unless job
         | 
| 237 | 
            -
             | 
| 237 | 
            +
             | 
| 238 238 | 
             
                submitter = submitter_class.new(box, serializer)
         | 
| 239 239 | 
             
                context = execution_context_class.new(submitter, {'logger' => logger})
         | 
| 240 | 
            -
             | 
| 240 | 
            +
             | 
| 241 241 | 
             
                t = Time.now
         | 
| 242 242 | 
             
                middleware_stack.around_execution(job, context) do
         | 
| 243 243 | 
             
                  job.method(:run).arity.zero? ? job.run : job.run(context)
         | 
| 244 244 | 
             
                end
         | 
| 245 | 
            -
             | 
| 245 | 
            +
             | 
| 246 246 | 
             
                # Perform two flushes, one for any possible jobs the job has spawned,
         | 
| 247 247 | 
             
                # and one for the job delete afterwards
         | 
| 248 248 | 
             
                box.delete_message(message.receipt_handle)
         | 
| 249 | 
            -
             | 
| 249 | 
            +
             | 
| 250 250 | 
             
                delta = Time.now - t
         | 
| 251 251 | 
             
                logger.info { "[worker] Finished %s in %0.2fs" % [job.inspect, delta] }
         | 
| 252 252 | 
             
              ensure
         |