job_dispatch 0.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 +7 -0
 - data/.gitignore +19 -0
 - data/.rspec +1 -0
 - data/.travis.yml +13 -0
 - data/Gemfile +20 -0
 - data/Guardfile +13 -0
 - data/LICENSE.txt +22 -0
 - data/README.md +85 -0
 - data/Rakefile +10 -0
 - data/bin/job-dispatcher +34 -0
 - data/bin/job-status +69 -0
 - data/bin/job-worker +40 -0
 - data/examples/mongoid-job.rb +43 -0
 - data/job_dispatch.gemspec +33 -0
 - data/lib/job_dispatch/broker/command.rb +45 -0
 - data/lib/job_dispatch/broker/internal_job.rb +32 -0
 - data/lib/job_dispatch/broker/socket.rb +85 -0
 - data/lib/job_dispatch/broker.rb +523 -0
 - data/lib/job_dispatch/client/proxy.rb +34 -0
 - data/lib/job_dispatch/client/proxy_error.rb +18 -0
 - data/lib/job_dispatch/client/synchronous_proxy.rb +29 -0
 - data/lib/job_dispatch/client.rb +49 -0
 - data/lib/job_dispatch/configuration.rb +7 -0
 - data/lib/job_dispatch/identity.rb +54 -0
 - data/lib/job_dispatch/job.rb +44 -0
 - data/lib/job_dispatch/signaller.rb +30 -0
 - data/lib/job_dispatch/sockets/enqueue.rb +18 -0
 - data/lib/job_dispatch/status.rb +79 -0
 - data/lib/job_dispatch/version.rb +3 -0
 - data/lib/job_dispatch/worker/item.rb +43 -0
 - data/lib/job_dispatch/worker/socket.rb +96 -0
 - data/lib/job_dispatch/worker.rb +120 -0
 - data/lib/job_dispatch.rb +97 -0
 - data/spec/factories/jobs.rb +19 -0
 - data/spec/job_dispatch/broker/socket_spec.rb +53 -0
 - data/spec/job_dispatch/broker_spec.rb +737 -0
 - data/spec/job_dispatch/identity_spec.rb +88 -0
 - data/spec/job_dispatch/job_spec.rb +77 -0
 - data/spec/job_dispatch/worker/socket_spec.rb +32 -0
 - data/spec/job_dispatch/worker_spec.rb +24 -0
 - data/spec/job_dispatch_spec.rb +0 -0
 - data/spec/spec_helper.rb +23 -0
 - data/spec/support/test_job.rb +30 -0
 - metadata +255 -0
 
| 
         @@ -0,0 +1,523 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # encoding: UTF-8
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'set'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module JobDispatch
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
              # The broker is the central communications service of JobDispatch. Clients and Workers both connect
         
     | 
| 
      
 8 
     | 
    
         
            +
              # to a ZeroMQ ROUTER socket. Clients and workers use a REQ socket, and send a request. The Broker
         
     | 
| 
      
 9 
     | 
    
         
            +
              # sends a reply immediately or at some point in the future when it is appropriate (eg: when there)
         
     | 
| 
      
 10 
     | 
    
         
            +
              # is a job to do for a worker, or when a job is completed for a client waiting on job notification).
         
     | 
| 
      
 11 
     | 
    
         
            +
              class Broker
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                WORKER_IDLE_TIME = 10.123
         
     | 
| 
      
 14 
     | 
    
         
            +
                POLL_TIME = 5.123
         
     | 
| 
      
 15 
     | 
    
         
            +
                STOP_SIGNALS = %w[INT TERM KILL]
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                IdleWorker = Struct.new :worker_id, :idle_since, :queue, :worker_name
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                # any object that will respond to `next_job_for_queue`, which should return a job, or nil if there
         
     | 
| 
      
 21 
     | 
    
         
            +
                # are no jobs for that queue. The returned job should be a JSONable object that will be sent to the worker.
         
     | 
| 
      
 22 
     | 
    
         
            +
                # This should include `target`, `action` and `parameters` keys.
         
     | 
| 
      
 23 
     | 
    
         
            +
                attr :socket
         
     | 
| 
      
 24 
     | 
    
         
            +
                attr :workers_waiting_for_reply # Array of Identity
         
     | 
| 
      
 25 
     | 
    
         
            +
                attr :workers_waiting_for_jobs # Hash of key: Identity, value: IdleWorker
         
     | 
| 
      
 26 
     | 
    
         
            +
                attr :worker_names # Hash of key: Identity actual ZMQ identity, value: String claimed identity
         
     | 
| 
      
 27 
     | 
    
         
            +
                attr :jobs_in_progress
         
     | 
| 
      
 28 
     | 
    
         
            +
                attr :jobs_in_progress_workers
         
     | 
| 
      
 29 
     | 
    
         
            +
                attr :queues
         
     | 
| 
      
 30 
     | 
    
         
            +
                attr_accessor :verbose
         
     | 
| 
      
 31 
     | 
    
         
            +
                attr :status
         
     | 
| 
      
 32 
     | 
    
         
            +
                attr :job_subscribers # Key: job_id, value: list of Socket Identities waiting for job completion notifications.
         
     | 
| 
      
 33 
     | 
    
         
            +
                attr :pub_socket
         
     | 
| 
      
 34 
     | 
    
         
            +
                attr_accessor :reply_exceptions
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                def initialize(worker_bind_address, wakeup_bind_address, publish_bind_address=nil)
         
     | 
| 
      
 37 
     | 
    
         
            +
                  @worker_bind_address = worker_bind_address
         
     | 
| 
      
 38 
     | 
    
         
            +
                  @wakeup_bind_address = wakeup_bind_address
         
     | 
| 
      
 39 
     | 
    
         
            +
                  @publish_bind_address = publish_bind_address
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                  # to track REQ-REP state:
         
     | 
| 
      
 42 
     | 
    
         
            +
                  @workers_waiting_for_reply = [] # array of Symbol (worker id = zmq identity of worker)
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                  # to track jobs:
         
     | 
| 
      
 45 
     | 
    
         
            +
                  @workers_waiting_for_jobs = {} # Hash of key: Identity(worker_id) value: IdleWorker
         
     | 
| 
      
 46 
     | 
    
         
            +
                  @queues = Hash.new { |hash, key| hash[key] = Set.new } # key:queue name, value: Array of Identity of worker id
         
     | 
| 
      
 47 
     | 
    
         
            +
                  @jobs_in_progress = {} # key: job_id, value: Job model object
         
     | 
| 
      
 48 
     | 
    
         
            +
                  @jobs_in_progress_workers = {} #key: job_id, value: worker_id
         
     | 
| 
      
 49 
     | 
    
         
            +
                  @worker_names = {} # Key: Symbol socket identity, value: String claimed name of worker
         
     | 
| 
      
 50 
     | 
    
         
            +
                  @job_subscribers = {} # Key: job_id, value: list of Socket Identities waiting for job completion notifications.
         
     | 
| 
      
 51 
     | 
    
         
            +
                  @status = "OK"
         
     | 
| 
      
 52 
     | 
    
         
            +
                  @reply_exceptions = true
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                  queues[:default] # ensure the default queue exists.
         
     | 
| 
      
 55 
     | 
    
         
            +
                end
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                def running?
         
     | 
| 
      
 58 
     | 
    
         
            +
                  @running
         
     | 
| 
      
 59 
     | 
    
         
            +
                end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                def verbose?
         
     | 
| 
      
 62 
     | 
    
         
            +
                  verbose
         
     | 
| 
      
 63 
     | 
    
         
            +
                end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                def run
         
     | 
| 
      
 66 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 67 
     | 
    
         
            +
                    puts "JobDispatch::Broker running in process #{Process.pid}"
         
     | 
| 
      
 68 
     | 
    
         
            +
                    JobDispatch.logger.info("JobDispatch::Broker running in process #{Process.pid}")
         
     | 
| 
      
 69 
     | 
    
         
            +
                    @running = true
         
     | 
| 
      
 70 
     | 
    
         
            +
                    poller = ZMQ::Poller.new
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                    @socket = JobDispatch::Broker::Socket.new(@worker_bind_address)
         
     | 
| 
      
 73 
     | 
    
         
            +
                    @socket.connect
         
     | 
| 
      
 74 
     | 
    
         
            +
                    poller.register(@socket.poll_item)
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
                    if @publish_bind_address
         
     | 
| 
      
 77 
     | 
    
         
            +
                      @pub_socket = JobDispatch.context.socket(ZMQ::PUB)
         
     | 
| 
      
 78 
     | 
    
         
            +
                      @pub_socket.bind(@publish_bind_address)
         
     | 
| 
      
 79 
     | 
    
         
            +
                    end
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                    if @wakeup_bind_address
         
     | 
| 
      
 82 
     | 
    
         
            +
                      JobDispatch.logger.info("JobDispatch::Broker signaller SUB socket bound to #{@wakeup_bind_address}")
         
     | 
| 
      
 83 
     | 
    
         
            +
                      @wake_socket = JobDispatch.context.socket(ZMQ::SUB)
         
     | 
| 
      
 84 
     | 
    
         
            +
                      @wake_socket.subscribe('')
         
     | 
| 
      
 85 
     | 
    
         
            +
                      @wake_socket.bind(@wakeup_bind_address)
         
     | 
| 
      
 86 
     | 
    
         
            +
                      poller.register(@wake_socket)
         
     | 
| 
      
 87 
     | 
    
         
            +
                    end
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
                    while running?
         
     | 
| 
      
 90 
     | 
    
         
            +
                      begin
         
     | 
| 
      
 91 
     | 
    
         
            +
                        process_messages(poller)
         
     | 
| 
      
 92 
     | 
    
         
            +
                        dispatch_jobs_to_workers
         
     | 
| 
      
 93 
     | 
    
         
            +
                        expire_timed_out_jobs
         
     | 
| 
      
 94 
     | 
    
         
            +
                        send_idle_commands
         
     | 
| 
      
 95 
     | 
    
         
            +
                      rescue SignalException => e
         
     | 
| 
      
 96 
     | 
    
         
            +
                        signal_name = Signal.signame(e.signo)
         
     | 
| 
      
 97 
     | 
    
         
            +
                        if STOP_SIGNALS.include?(signal_name)
         
     | 
| 
      
 98 
     | 
    
         
            +
                          JobDispatch.logger.info("JobDispatch::Broker shutting down, due to #{signal_name} signal")
         
     | 
| 
      
 99 
     | 
    
         
            +
                          puts "JobDispatch::Broker shutting down, due to #{signal_name} signal"
         
     | 
| 
      
 100 
     | 
    
         
            +
                          @running = false
         
     | 
| 
      
 101 
     | 
    
         
            +
                          @status = "SHUTDOWN"
         
     | 
| 
      
 102 
     | 
    
         
            +
                          sleep 1
         
     | 
| 
      
 103 
     | 
    
         
            +
                          process_quit
         
     | 
| 
      
 104 
     | 
    
         
            +
                          sleep 1
         
     | 
| 
      
 105 
     | 
    
         
            +
                        end
         
     | 
| 
      
 106 
     | 
    
         
            +
                      end
         
     | 
| 
      
 107 
     | 
    
         
            +
                    end
         
     | 
| 
      
 108 
     | 
    
         
            +
                  ensure
         
     | 
| 
      
 109 
     | 
    
         
            +
                    @socket.disconnect if @socket
         
     | 
| 
      
 110 
     | 
    
         
            +
                    @socket = nil
         
     | 
| 
      
 111 
     | 
    
         
            +
                  end
         
     | 
| 
      
 112 
     | 
    
         
            +
                end
         
     | 
| 
      
 113 
     | 
    
         
            +
             
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
                def process_messages(poller)
         
     | 
| 
      
 116 
     | 
    
         
            +
                  # TODO: calculate the amount of time to sleep to wake up such that a scheduled event happens as close
         
     | 
| 
      
 117 
     | 
    
         
            +
                  # as possible to the time it was supposed to happen. This could additionally mean that the POLL_TIME
         
     | 
| 
      
 118 
     | 
    
         
            +
                  # could be arbitrarily large. As any communication with the broker will wake it immediately.
         
     | 
| 
      
 119 
     | 
    
         
            +
                  poll_time = POLL_TIME
         
     | 
| 
      
 120 
     | 
    
         
            +
                  poller.poll(poll_time)
         
     | 
| 
      
 121 
     | 
    
         
            +
             
     | 
| 
      
 122 
     | 
    
         
            +
                  if @wake_socket && poller.readables.include?(@wake_socket)
         
     | 
| 
      
 123 
     | 
    
         
            +
                    @wake_socket.recv # no message to process, just consume messages in order to wake the poller
         
     | 
| 
      
 124 
     | 
    
         
            +
                  end
         
     | 
| 
      
 125 
     | 
    
         
            +
             
     | 
| 
      
 126 
     | 
    
         
            +
                  if poller.readables.include?(socket.socket)
         
     | 
| 
      
 127 
     | 
    
         
            +
                    command = read_command
         
     | 
| 
      
 128 
     | 
    
         
            +
                    reply = process_command(command)
         
     | 
| 
      
 129 
     | 
    
         
            +
                    send_command(reply) if reply
         
     | 
| 
      
 130 
     | 
    
         
            +
                  end
         
     | 
| 
      
 131 
     | 
    
         
            +
                end
         
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
      
 133 
     | 
    
         
            +
             
     | 
| 
      
 134 
     | 
    
         
            +
                # read a command from a worker. We will keep a 1:1 REQ-REP model with each worker so we need to track the
         
     | 
| 
      
 135 
     | 
    
         
            +
                # state of the worker.
         
     | 
| 
      
 136 
     | 
    
         
            +
                def read_command
         
     | 
| 
      
 137 
     | 
    
         
            +
                  command = socket.read_command
         
     | 
| 
      
 138 
     | 
    
         
            +
                  @workers_waiting_for_reply << command.worker_id
         
     | 
| 
      
 139 
     | 
    
         
            +
                  command
         
     | 
| 
      
 140 
     | 
    
         
            +
                end
         
     | 
| 
      
 141 
     | 
    
         
            +
             
     | 
| 
      
 142 
     | 
    
         
            +
             
     | 
| 
      
 143 
     | 
    
         
            +
                # send a command out the socket. Also maintains the state of the list of workers so that we can keep the
         
     | 
| 
      
 144 
     | 
    
         
            +
                # REQ-REP contract.
         
     | 
| 
      
 145 
     | 
    
         
            +
                def send_command(command)
         
     | 
| 
      
 146 
     | 
    
         
            +
                  raise "Worker not waiting for reply" unless workers_waiting_for_reply.include?(command.worker_id)
         
     | 
| 
      
 147 
     | 
    
         
            +
                  workers_waiting_for_reply.delete(command.worker_id)
         
     | 
| 
      
 148 
     | 
    
         
            +
                  JobDispatch.logger.debug("JobDispatch::Broker sending command: #{command.inspect}")
         
     | 
| 
      
 149 
     | 
    
         
            +
                  socket.send_command command
         
     | 
| 
      
 150 
     | 
    
         
            +
                end
         
     | 
| 
      
 151 
     | 
    
         
            +
             
     | 
| 
      
 152 
     | 
    
         
            +
             
     | 
| 
      
 153 
     | 
    
         
            +
                def process_command(command)
         
     | 
| 
      
 154 
     | 
    
         
            +
                  # prepare for immediate reply
         
     | 
| 
      
 155 
     | 
    
         
            +
                  reply = Broker::Command.new(command.worker_id)
         
     | 
| 
      
 156 
     | 
    
         
            +
             
     | 
| 
      
 157 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 158 
     | 
    
         
            +
                    case command.command
         
     | 
| 
      
 159 
     | 
    
         
            +
                      when "ready"
         
     | 
| 
      
 160 
     | 
    
         
            +
                        # add to list of workers who are ready for work
         
     | 
| 
      
 161 
     | 
    
         
            +
                        add_available_worker(command)
         
     | 
| 
      
 162 
     | 
    
         
            +
             
     | 
| 
      
 163 
     | 
    
         
            +
                        # don't reply, leaves worker blocked waiting for a job to do.
         
     | 
| 
      
 164 
     | 
    
         
            +
                        reply = nil
         
     | 
| 
      
 165 
     | 
    
         
            +
             
     | 
| 
      
 166 
     | 
    
         
            +
                      when "goodbye"
         
     | 
| 
      
 167 
     | 
    
         
            +
                        reply.parameters = remove_available_worker(command)
         
     | 
| 
      
 168 
     | 
    
         
            +
             
     | 
| 
      
 169 
     | 
    
         
            +
                      when "completed"
         
     | 
| 
      
 170 
     | 
    
         
            +
                        #  process completed job.
         
     | 
| 
      
 171 
     | 
    
         
            +
                        handle_completed_job(command)
         
     | 
| 
      
 172 
     | 
    
         
            +
             
     | 
| 
      
 173 
     | 
    
         
            +
                        if command.worker_ready?
         
     | 
| 
      
 174 
     | 
    
         
            +
                          # a completed job also means the worker is available for more work.
         
     | 
| 
      
 175 
     | 
    
         
            +
                          add_available_worker(command)
         
     | 
| 
      
 176 
     | 
    
         
            +
                          reply = nil
         
     | 
| 
      
 177 
     | 
    
         
            +
                        else
         
     | 
| 
      
 178 
     | 
    
         
            +
                          reply.parameters = {:status => 'thanks'}
         
     | 
| 
      
 179 
     | 
    
         
            +
                        end
         
     | 
| 
      
 180 
     | 
    
         
            +
             
     | 
| 
      
 181 
     | 
    
         
            +
                      when "notify"
         
     | 
| 
      
 182 
     | 
    
         
            +
                        # synchronous notification of job status.
         
     | 
| 
      
 183 
     | 
    
         
            +
             
     | 
| 
      
 184 
     | 
    
         
            +
                        job_id = command.parameters[:job_id]
         
     | 
| 
      
 185 
     | 
    
         
            +
                        raise MissingParameterError, "Missing 'job_id' parameter" unless job_id
         
     | 
| 
      
 186 
     | 
    
         
            +
             
     | 
| 
      
 187 
     | 
    
         
            +
                        if jobs_in_progress[job_id]
         
     | 
| 
      
 188 
     | 
    
         
            +
                          workers_waiting_for_reply << command.worker_id
         
     | 
| 
      
 189 
     | 
    
         
            +
                          job_subscribers[job_id.to_s] ||= []
         
     | 
| 
      
 190 
     | 
    
         
            +
                          job_subscribers[job_id.to_s] << command.worker_id
         
     | 
| 
      
 191 
     | 
    
         
            +
                          reply = nil
         
     | 
| 
      
 192 
     | 
    
         
            +
                        else
         
     | 
| 
      
 193 
     | 
    
         
            +
                          job = job_source.find(job_id) # load job from storage and return to requester.
         
     | 
| 
      
 194 
     | 
    
         
            +
                          reply.parameters = job_status_parameters(job)
         
     | 
| 
      
 195 
     | 
    
         
            +
                        end
         
     | 
| 
      
 196 
     | 
    
         
            +
             
     | 
| 
      
 197 
     | 
    
         
            +
             
     | 
| 
      
 198 
     | 
    
         
            +
                      when "touch"
         
     | 
| 
      
 199 
     | 
    
         
            +
                        # perhaps this could also be processed of a PUB/SUB socket so that it doesn't require a synchronous
         
     | 
| 
      
 200 
     | 
    
         
            +
                        # response to the worker...
         
     | 
| 
      
 201 
     | 
    
         
            +
                        reply.parameters = touch_job(command)
         
     | 
| 
      
 202 
     | 
    
         
            +
             
     | 
| 
      
 203 
     | 
    
         
            +
                      when "status"
         
     | 
| 
      
 204 
     | 
    
         
            +
                        reply.parameters = status_response
         
     | 
| 
      
 205 
     | 
    
         
            +
             
     | 
| 
      
 206 
     | 
    
         
            +
                      when "enqueue"
         
     | 
| 
      
 207 
     | 
    
         
            +
                        reply.parameters = create_job(command)
         
     | 
| 
      
 208 
     | 
    
         
            +
             
     | 
| 
      
 209 
     | 
    
         
            +
                      when "quit"
         
     | 
| 
      
 210 
     | 
    
         
            +
                        process_quit
         
     | 
| 
      
 211 
     | 
    
         
            +
                        reply.parameters = {:status => 'bye'}
         
     | 
| 
      
 212 
     | 
    
         
            +
                        @running = false
         
     | 
| 
      
 213 
     | 
    
         
            +
             
     | 
| 
      
 214 
     | 
    
         
            +
                      else
         
     | 
| 
      
 215 
     | 
    
         
            +
                        # unknown command, reply with error immediately to fulfil REQ-REP state machine contract.
         
     | 
| 
      
 216 
     | 
    
         
            +
                        reply.parameters = {:status => 'unknown command!'}
         
     | 
| 
      
 217 
     | 
    
         
            +
                    end
         
     | 
| 
      
 218 
     | 
    
         
            +
             
     | 
| 
      
 219 
     | 
    
         
            +
                  rescue RSpec::Expectations::ExpectationNotMetError
         
     | 
| 
      
 220 
     | 
    
         
            +
                    raise # allow test exceptions through.
         
     | 
| 
      
 221 
     | 
    
         
            +
                  rescue StandardError => e
         
     | 
| 
      
 222 
     | 
    
         
            +
                    if reply_exceptions
         
     | 
| 
      
 223 
     | 
    
         
            +
                      # all others reply over socket.
         
     | 
| 
      
 224 
     | 
    
         
            +
                      JobDispatch.logger.error("JobDispatch::Broker #{e}")
         
     | 
| 
      
 225 
     | 
    
         
            +
                      reply.parameters = {:status => 'error', :message => e.to_s}
         
     | 
| 
      
 226 
     | 
    
         
            +
                    else
         
     | 
| 
      
 227 
     | 
    
         
            +
                      # used during testing to raise errors so that Rspec can catch them as a test failure.
         
     | 
| 
      
 228 
     | 
    
         
            +
                      raise
         
     | 
| 
      
 229 
     | 
    
         
            +
                    end
         
     | 
| 
      
 230 
     | 
    
         
            +
                  end
         
     | 
| 
      
 231 
     | 
    
         
            +
             
     | 
| 
      
 232 
     | 
    
         
            +
                  reply
         
     | 
| 
      
 233 
     | 
    
         
            +
                end
         
     | 
| 
      
 234 
     | 
    
         
            +
             
     | 
| 
      
 235 
     | 
    
         
            +
                def send_idle_commands(idle_time=nil)
         
     | 
| 
      
 236 
     | 
    
         
            +
                  idle_time ||= Time.now
         
     | 
| 
      
 237 
     | 
    
         
            +
                  idle_time -= WORKER_IDLE_TIME
         
     | 
| 
      
 238 
     | 
    
         
            +
                  idle_workers = @workers_waiting_for_jobs.select { |worker_id, worker| worker.idle_since < idle_time }
         
     | 
| 
      
 239 
     | 
    
         
            +
                  idle_workers.each do |worker_id, worker|
         
     | 
| 
      
 240 
     | 
    
         
            +
                    send_job_to_worker(InternalJob.new('idle', worker.queue), worker_id)
         
     | 
| 
      
 241 
     | 
    
         
            +
                  end
         
     | 
| 
      
 242 
     | 
    
         
            +
                end
         
     | 
| 
      
 243 
     | 
    
         
            +
             
     | 
| 
      
 244 
     | 
    
         
            +
             
     | 
| 
      
 245 
     | 
    
         
            +
                def send_job_to_worker(job, worker_id)
         
     | 
| 
      
 246 
     | 
    
         
            +
                  # remove from queue and idle workers lists.
         
     | 
| 
      
 247 
     | 
    
         
            +
                  idle_worker = workers_waiting_for_jobs.delete(worker_id)
         
     | 
| 
      
 248 
     | 
    
         
            +
                  queues[idle_worker.queue].delete(worker_id)
         
     | 
| 
      
 249 
     | 
    
         
            +
             
     | 
| 
      
 250 
     | 
    
         
            +
                  # serialise job for json message
         
     | 
| 
      
 251 
     | 
    
         
            +
                  hash = json_for_job(job)
         
     | 
| 
      
 252 
     | 
    
         
            +
             
     | 
| 
      
 253 
     | 
    
         
            +
                  # use the job record id or assign a uuid as the job id
         
     | 
| 
      
 254 
     | 
    
         
            +
                  job_id = job.id ? job.id.to_s : SecureRandom.uuid
         
     | 
| 
      
 255 
     | 
    
         
            +
                  hash[:job_id] = job_id
         
     | 
| 
      
 256 
     | 
    
         
            +
                  hash[:command] = 'job' unless job.is_a?(InternalJob)
         
     | 
| 
      
 257 
     | 
    
         
            +
                  job_id = hash[:job_id] ||= SecureRandom.uuid
         
     | 
| 
      
 258 
     | 
    
         
            +
             
     | 
| 
      
 259 
     | 
    
         
            +
                  # add to working lists
         
     | 
| 
      
 260 
     | 
    
         
            +
                  jobs_in_progress[job_id] = job
         
     | 
| 
      
 261 
     | 
    
         
            +
                  jobs_in_progress_workers[job_id] = worker_id
         
     | 
| 
      
 262 
     | 
    
         
            +
             
     | 
| 
      
 263 
     | 
    
         
            +
                  # send the command.
         
     | 
| 
      
 264 
     | 
    
         
            +
                  command = Broker::Command.new(worker_id, hash)
         
     | 
| 
      
 265 
     | 
    
         
            +
                  JobDispatch.logger.info("JobDispatch::Broker Sending command '#{hash[:command]}' to worker: #{worker_id.to_json}")
         
     | 
| 
      
 266 
     | 
    
         
            +
                  send_command(command)
         
     | 
| 
      
 267 
     | 
    
         
            +
                end
         
     | 
| 
      
 268 
     | 
    
         
            +
             
     | 
| 
      
 269 
     | 
    
         
            +
             
     | 
| 
      
 270 
     | 
    
         
            +
                # add a worker to the list of workers available for jobs.
         
     | 
| 
      
 271 
     | 
    
         
            +
                def add_available_worker(command)
         
     | 
| 
      
 272 
     | 
    
         
            +
                  JobDispatch.logger.info("JobDispatch::Broker Worker '#{command.worker_id.to_json}' available for work on queue '#{command.queue}'")
         
     | 
| 
      
 273 
     | 
    
         
            +
                  queue = command.queue
         
     | 
| 
      
 274 
     | 
    
         
            +
                  idle_worker = IdleWorker.new(command.worker_id, Time.now, queue, command.worker_name)
         
     | 
| 
      
 275 
     | 
    
         
            +
                  workers_waiting_for_jobs[command.worker_id] = idle_worker
         
     | 
| 
      
 276 
     | 
    
         
            +
                  queues[queue] << command.worker_id
         
     | 
| 
      
 277 
     | 
    
         
            +
                  if command.worker_name # this is only sent on initial requests.
         
     | 
| 
      
 278 
     | 
    
         
            +
                    worker_names[command.worker_id] = command.worker_name
         
     | 
| 
      
 279 
     | 
    
         
            +
                  end
         
     | 
| 
      
 280 
     | 
    
         
            +
                end
         
     | 
| 
      
 281 
     | 
    
         
            +
             
     | 
| 
      
 282 
     | 
    
         
            +
                # remove a worker from available list. Worker is shutting down or indicating that it will no longer
         
     | 
| 
      
 283 
     | 
    
         
            +
                # be available for doing work.
         
     | 
| 
      
 284 
     | 
    
         
            +
                def remove_available_worker(command)
         
     | 
| 
      
 285 
     | 
    
         
            +
                  JobDispatch.logger.info("JobDispatch::Broker Worker '#{command.worker_id.to_json}' available for work on queue '#{command.queue}'")
         
     | 
| 
      
 286 
     | 
    
         
            +
             
     | 
| 
      
 287 
     | 
    
         
            +
                  # the goodbye command is sent by another socket connection, so the worker_id (socket identity) will
         
     | 
| 
      
 288 
     | 
    
         
            +
                  # not match the socket actually waiting for work.
         
     | 
| 
      
 289 
     | 
    
         
            +
             
     | 
| 
      
 290 
     | 
    
         
            +
                  keys = worker_names.select { |id, name| name == command.worker_name }.keys
         
     | 
| 
      
 291 
     | 
    
         
            +
                  keys.each do |worker_id|
         
     | 
| 
      
 292 
     | 
    
         
            +
                    workers_waiting_for_reply.delete(worker_id) # socket will be closing, no need to send it anything.
         
     | 
| 
      
 293 
     | 
    
         
            +
                    worker = workers_waiting_for_jobs.delete(worker_id)
         
     | 
| 
      
 294 
     | 
    
         
            +
                    queues[worker.queue].delete(worker_id) if worker
         
     | 
| 
      
 295 
     | 
    
         
            +
                    worker_names.delete(worker_id)
         
     | 
| 
      
 296 
     | 
    
         
            +
                  end
         
     | 
| 
      
 297 
     | 
    
         
            +
             
     | 
| 
      
 298 
     | 
    
         
            +
                  {status: "see ya later"}
         
     | 
| 
      
 299 
     | 
    
         
            +
                end
         
     | 
| 
      
 300 
     | 
    
         
            +
             
     | 
| 
      
 301 
     | 
    
         
            +
                def dispatch_jobs_to_workers
         
     | 
| 
      
 302 
     | 
    
         
            +
                  # dequeue jobs from database for each queue
         
     | 
| 
      
 303 
     | 
    
         
            +
                  @queues.each_pair do |queue, worker_ids|
         
     | 
| 
      
 304 
     | 
    
         
            +
                    # we only need to check the database if there are available workers in that queue
         
     | 
| 
      
 305 
     | 
    
         
            +
                    if worker_ids.count > 0
         
     | 
| 
      
 306 
     | 
    
         
            +
                      worker_id = worker_ids.first
         
     | 
| 
      
 307 
     | 
    
         
            +
             
     | 
| 
      
 308 
     | 
    
         
            +
                      job = begin
         
     | 
| 
      
 309 
     | 
    
         
            +
                        job_source.dequeue_job_for_queue(queue.to_s)
         
     | 
| 
      
 310 
     | 
    
         
            +
                      rescue StandardError => e
         
     | 
| 
      
 311 
     | 
    
         
            +
                        # Log any errors reported dequeuing jobs, and treat it as no jobs available. This could
         
     | 
| 
      
 312 
     | 
    
         
            +
                        # be, for example, that the database is not contactable at this point in time.
         
     | 
| 
      
 313 
     | 
    
         
            +
                        JobDispatch.logger.error "JobDispatch::Broker#dispatch_jobs_to_workers: #{e}"
         
     | 
| 
      
 314 
     | 
    
         
            +
                        nil
         
     | 
| 
      
 315 
     | 
    
         
            +
                      end
         
     | 
| 
      
 316 
     | 
    
         
            +
             
     | 
| 
      
 317 
     | 
    
         
            +
                      if job
         
     | 
| 
      
 318 
     | 
    
         
            +
                        JobDispatch.logger.info("JobDispatch::Broker dispatching job #{job.id} to worker #{worker_id.to_json}")
         
     | 
| 
      
 319 
     | 
    
         
            +
                        send_job_to_worker(job, worker_id)
         
     | 
| 
      
 320 
     | 
    
         
            +
             
     | 
| 
      
 321 
     | 
    
         
            +
                        job.expire_execution_at = Time.now + (job.timeout || Job::DEFAULT_EXECUTION_TIMEOUT)
         
     | 
| 
      
 322 
     | 
    
         
            +
                        job.status = JobDispatch::Job::IN_PROGRESS
         
     | 
| 
      
 323 
     | 
    
         
            +
                        job.save
         
     | 
| 
      
 324 
     | 
    
         
            +
             
     | 
| 
      
 325 
     | 
    
         
            +
                        publish_job_status(job)
         
     | 
| 
      
 326 
     | 
    
         
            +
                      end
         
     | 
| 
      
 327 
     | 
    
         
            +
                    end
         
     | 
| 
      
 328 
     | 
    
         
            +
                  end
         
     | 
| 
      
 329 
     | 
    
         
            +
                end
         
     | 
| 
      
 330 
     | 
    
         
            +
             
     | 
| 
      
 331 
     | 
    
         
            +
             
     | 
| 
      
 332 
     | 
    
         
            +
                def expire_timed_out_jobs
         
     | 
| 
      
 333 
     | 
    
         
            +
                  expired_job_ids = @jobs_in_progress.each_with_object([]) do |(job_id, job), expired|
         
     | 
| 
      
 334 
     | 
    
         
            +
                    # check if job has timed out. If so, implement retry logic.
         
     | 
| 
      
 335 
     | 
    
         
            +
                    expired << job_id if job.timed_out?
         
     | 
| 
      
 336 
     | 
    
         
            +
                  end
         
     | 
| 
      
 337 
     | 
    
         
            +
             
     | 
| 
      
 338 
     | 
    
         
            +
                  expired_job_ids.each do |job_id|
         
     | 
| 
      
 339 
     | 
    
         
            +
                    job = jobs_in_progress.delete(job_id)
         
     | 
| 
      
 340 
     | 
    
         
            +
                    @jobs_in_progress_workers.delete(job_id)
         
     | 
| 
      
 341 
     | 
    
         
            +
                    if job.is_a? InternalJob
         
     | 
| 
      
 342 
     | 
    
         
            +
                      # no action / publish required
         
     | 
| 
      
 343 
     | 
    
         
            +
                    elsif job
         
     | 
| 
      
 344 
     | 
    
         
            +
                      JobDispatch.logger.info("JobDispatch::Broker expiring job #{job_id} has timed out.")
         
     | 
| 
      
 345 
     | 
    
         
            +
                      job.failed!("job timed out")
         
     | 
| 
      
 346 
     | 
    
         
            +
                      publish_job_status(job)
         
     | 
| 
      
 347 
     | 
    
         
            +
                    end
         
     | 
| 
      
 348 
     | 
    
         
            +
                  end
         
     | 
| 
      
 349 
     | 
    
         
            +
                end
         
     | 
| 
      
 350 
     | 
    
         
            +
             
     | 
| 
      
 351 
     | 
    
         
            +
                def queues_with_available_workers
         
     | 
| 
      
 352 
     | 
    
         
            +
                  @queues.each_with_object([]) do |(queue, workers), object|
         
     | 
| 
      
 353 
     | 
    
         
            +
                    object << queue unless workers.nil? || workers.empty?
         
     | 
| 
      
 354 
     | 
    
         
            +
                  end
         
     | 
| 
      
 355 
     | 
    
         
            +
                end
         
     | 
| 
      
 356 
     | 
    
         
            +
             
     | 
| 
      
 357 
     | 
    
         
            +
             
     | 
| 
      
 358 
     | 
    
         
            +
                def handle_completed_job(command)
         
     | 
| 
      
 359 
     | 
    
         
            +
                  # look up the job and process its completion.
         
     | 
| 
      
 360 
     | 
    
         
            +
                  job_id = command.parameters[:job_id]
         
     | 
| 
      
 361 
     | 
    
         
            +
                  if job_id
         
     | 
| 
      
 362 
     | 
    
         
            +
                    job = jobs_in_progress.delete(job_id)
         
     | 
| 
      
 363 
     | 
    
         
            +
                    jobs_in_progress_workers.delete(job_id)
         
     | 
| 
      
 364 
     | 
    
         
            +
                    if job.is_a? InternalJob
         
     | 
| 
      
 365 
     | 
    
         
            +
                      # no publish or save action required.
         
     | 
| 
      
 366 
     | 
    
         
            +
                    else
         
     | 
| 
      
 367 
     | 
    
         
            +
                      # ensure the job record is up to date. Also in mongo, lock time is reduced by doing a read before
         
     | 
| 
      
 368 
     | 
    
         
            +
                      # doing an update.
         
     | 
| 
      
 369 
     | 
    
         
            +
                      begin
         
     | 
| 
      
 370 
     | 
    
         
            +
                        job = JobDispatch.config.job_class.find(job_id)
         
     | 
| 
      
 371 
     | 
    
         
            +
                      rescue StandardError => e
         
     | 
| 
      
 372 
     | 
    
         
            +
                        JobDispatch.logger.error("JobDispatch::Broker Job #{job_id} completed, but failed to reload from database: #{e}")
         
     | 
| 
      
 373 
     | 
    
         
            +
                        job = nil
         
     | 
| 
      
 374 
     | 
    
         
            +
                      end
         
     | 
| 
      
 375 
     | 
    
         
            +
             
     | 
| 
      
 376 
     | 
    
         
            +
                      if job
         
     | 
| 
      
 377 
     | 
    
         
            +
                        JobDispatch.logger.info(
         
     | 
| 
      
 378 
     | 
    
         
            +
                            "JobDispatch::Broker completed job #{job_id} " \
         
     | 
| 
      
 379 
     | 
    
         
            +
                            "from worker #{command.worker_id.to_json} " \
         
     | 
| 
      
 380 
     | 
    
         
            +
                            "status = #{command.parameters[:status]}")
         
     | 
| 
      
 381 
     | 
    
         
            +
                        if command.success?
         
     | 
| 
      
 382 
     | 
    
         
            +
                          job.succeeded!(command.parameters[:result])
         
     | 
| 
      
 383 
     | 
    
         
            +
                          publish_job_status(job)
         
     | 
| 
      
 384 
     | 
    
         
            +
                        else
         
     | 
| 
      
 385 
     | 
    
         
            +
                          job.failed!(command.parameters[:result])
         
     | 
| 
      
 386 
     | 
    
         
            +
                          publish_job_status(job)
         
     | 
| 
      
 387 
     | 
    
         
            +
                        end
         
     | 
| 
      
 388 
     | 
    
         
            +
                      end
         
     | 
| 
      
 389 
     | 
    
         
            +
                    end
         
     | 
| 
      
 390 
     | 
    
         
            +
                  end
         
     | 
| 
      
 391 
     | 
    
         
            +
                end
         
     | 
| 
      
 392 
     | 
    
         
            +
             
     | 
| 
      
 393 
     | 
    
         
            +
                def process_quit
         
     | 
| 
      
 394 
     | 
    
         
            +
                  JobDispatch.logger.info("JobDispatch::Broker Sending quit message to idle workers")
         
     | 
| 
      
 395 
     | 
    
         
            +
             
     | 
| 
      
 396 
     | 
    
         
            +
                  quit_params = {command: 'quit'}
         
     | 
| 
      
 397 
     | 
    
         
            +
                  until workers_waiting_for_jobs.empty?
         
     | 
| 
      
 398 
     | 
    
         
            +
                    worker_id, worker = workers_waiting_for_jobs.first
         
     | 
| 
      
 399 
     | 
    
         
            +
                    send_job_to_worker(InternalJob.new('quit', worker.queue), worker_id)
         
     | 
| 
      
 400 
     | 
    
         
            +
                  end
         
     | 
| 
      
 401 
     | 
    
         
            +
                end
         
     | 
| 
      
 402 
     | 
    
         
            +
             
     | 
| 
      
 403 
     | 
    
         
            +
             
     | 
| 
      
 404 
     | 
    
         
            +
                def json_for_job(job)
         
     | 
| 
      
 405 
     | 
    
         
            +
                  hash = if job.respond_to? :as_job_queue_item
         
     | 
| 
      
 406 
     | 
    
         
            +
                           job.as_job_queue_item
         
     | 
| 
      
 407 
     | 
    
         
            +
                         else
         
     | 
| 
      
 408 
     | 
    
         
            +
                           job.as_json
         
     | 
| 
      
 409 
     | 
    
         
            +
                         end.with_indifferent_access
         
     | 
| 
      
 410 
     | 
    
         
            +
                  hash[:id] = hash[:id].to_s
         
     | 
| 
      
 411 
     | 
    
         
            +
                  hash
         
     | 
| 
      
 412 
     | 
    
         
            +
                end
         
     | 
| 
      
 413 
     | 
    
         
            +
             
     | 
| 
      
 414 
     | 
    
         
            +
             
     | 
| 
      
 415 
     | 
    
         
            +
                def status_response
         
     | 
| 
      
 416 
     | 
    
         
            +
                  response = {
         
     | 
| 
      
 417 
     | 
    
         
            +
                      :status => status,
         
     | 
| 
      
 418 
     | 
    
         
            +
                      :queues => {}
         
     | 
| 
      
 419 
     | 
    
         
            +
                  }
         
     | 
| 
      
 420 
     | 
    
         
            +
             
     | 
| 
      
 421 
     | 
    
         
            +
                  queues.each_pair do |queue, _|
         
     | 
| 
      
 422 
     | 
    
         
            +
                    response[:queues][queue.to_sym] = {}
         
     | 
| 
      
 423 
     | 
    
         
            +
                  end
         
     | 
| 
      
 424 
     | 
    
         
            +
             
     | 
| 
      
 425 
     | 
    
         
            +
                  jobs_in_progress.each_with_object(response[:queues]) do |(job_id, job), _queues|
         
     | 
| 
      
 426 
     | 
    
         
            +
                    queue = job.queue.to_sym
         
     | 
| 
      
 427 
     | 
    
         
            +
                    _queues[queue] ||= {}
         
     | 
| 
      
 428 
     | 
    
         
            +
                    worker_id = jobs_in_progress_workers[job_id]
         
     | 
| 
      
 429 
     | 
    
         
            +
                    _queues[queue][worker_id.to_hex] = {
         
     | 
| 
      
 430 
     | 
    
         
            +
                        :status => :processing,
         
     | 
| 
      
 431 
     | 
    
         
            +
                        :name => worker_names[worker_id],
         
     | 
| 
      
 432 
     | 
    
         
            +
                        :job_id => job_id,
         
     | 
| 
      
 433 
     | 
    
         
            +
                        :queue => job.queue,
         
     | 
| 
      
 434 
     | 
    
         
            +
                        :job => json_for_job(job),
         
     | 
| 
      
 435 
     | 
    
         
            +
                    }
         
     | 
| 
      
 436 
     | 
    
         
            +
                  end
         
     | 
| 
      
 437 
     | 
    
         
            +
             
     | 
| 
      
 438 
     | 
    
         
            +
                  workers_waiting_for_jobs.each_with_object(response[:queues]) do |(worker_id, worker), _queues|
         
     | 
| 
      
 439 
     | 
    
         
            +
                    queue = worker.queue.to_sym
         
     | 
| 
      
 440 
     | 
    
         
            +
                    _queues[queue] ||= {}
         
     | 
| 
      
 441 
     | 
    
         
            +
                    _queues[queue][worker_id.to_hex] = {
         
     | 
| 
      
 442 
     | 
    
         
            +
                        :status => :idle,
         
     | 
| 
      
 443 
     | 
    
         
            +
                        :name => worker_names[worker_id],
         
     | 
| 
      
 444 
     | 
    
         
            +
                        :queue => worker.queue,
         
     | 
| 
      
 445 
     | 
    
         
            +
                    }
         
     | 
| 
      
 446 
     | 
    
         
            +
                  end
         
     | 
| 
      
 447 
     | 
    
         
            +
             
     | 
| 
      
 448 
     | 
    
         
            +
                  response
         
     | 
| 
      
 449 
     | 
    
         
            +
                end
         
     | 
| 
      
 450 
     | 
    
         
            +
             
     | 
| 
      
 451 
     | 
    
         
            +
                # reset the timeout on the job. Called for a long process to confirm to the dispatcher that the worker is
         
     | 
| 
      
 452 
     | 
    
         
            +
                # still actively working on the job and has not died.
         
     | 
| 
      
 453 
     | 
    
         
            +
                #
         
     | 
| 
      
 454 
     | 
    
         
            +
                # @return [Hash] result to be sent to client.
         
     | 
| 
      
 455 
     | 
    
         
            +
                def touch_job(command)
         
     | 
| 
      
 456 
     | 
    
         
            +
                  job_id = command.parameters[:job_id]
         
     | 
| 
      
 457 
     | 
    
         
            +
                  timeout = command.parameters[:timeout] || Job::DEFAULT_EXECUTION_TIMEOUT
         
     | 
| 
      
 458 
     | 
    
         
            +
                  job = @jobs_in_progress[job_id]
         
     | 
| 
      
 459 
     | 
    
         
            +
                  if job
         
     | 
| 
      
 460 
     | 
    
         
            +
                    job.expire_execution_at = Time.now + timeout
         
     | 
| 
      
 461 
     | 
    
         
            +
                    JobDispatch.logger.info("JobDispatch::Broker#touch timeout on job #{job_id} to #{job.expire_execution_at}")
         
     | 
| 
      
 462 
     | 
    
         
            +
                    job.save
         
     | 
| 
      
 463 
     | 
    
         
            +
                    {status: "success"}
         
     | 
| 
      
 464 
     | 
    
         
            +
                  else
         
     | 
| 
      
 465 
     | 
    
         
            +
                    {status: "error", message: "the specified job does not appear to be in progress"}
         
     | 
| 
      
 466 
     | 
    
         
            +
                  end
         
     | 
| 
      
 467 
     | 
    
         
            +
                end
         
     | 
| 
      
 468 
     | 
    
         
            +
             
     | 
| 
      
 469 
     | 
    
         
            +
                def create_job(command)
         
     | 
| 
      
 470 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 471 
     | 
    
         
            +
                    raise MissingParameterError, "Missing 'job' from command" unless command.parameters[:job].present?
         
     | 
| 
      
 472 
     | 
    
         
            +
             
     | 
| 
      
 473 
     | 
    
         
            +
                    job_attrs = command.parameters[:job]
         
     | 
| 
      
 474 
     | 
    
         
            +
                    job = job_source.create!(job_attrs)
         
     | 
| 
      
 475 
     | 
    
         
            +
                    {status: 'success', job_id: job.id.to_s}
         
     | 
| 
      
 476 
     | 
    
         
            +
                  rescue StandardError => e
         
     | 
| 
      
 477 
     | 
    
         
            +
                    JobDispatch.logger.error "JobDispatch::Broker#create_job error: #{e}"
         
     | 
| 
      
 478 
     | 
    
         
            +
                    {status: 'error', message: e.to_s}
         
     | 
| 
      
 479 
     | 
    
         
            +
                  end
         
     | 
| 
      
 480 
     | 
    
         
            +
                end
         
     | 
| 
      
 481 
     | 
    
         
            +
             
     | 
| 
      
 482 
     | 
    
         
            +
                private
         
     | 
| 
      
 483 
     | 
    
         
            +
             
     | 
| 
      
 484 
     | 
    
         
            +
                def job_source
         
     | 
| 
      
 485 
     | 
    
         
            +
                  JobDispatch.config.job_class
         
     | 
| 
      
 486 
     | 
    
         
            +
                end
         
     | 
| 
      
 487 
     | 
    
         
            +
             
     | 
| 
      
 488 
     | 
    
         
            +
                def publish_job_status(job)
         
     | 
| 
      
 489 
     | 
    
         
            +
                  parameters = job_status_parameters(job)
         
     | 
| 
      
 490 
     | 
    
         
            +
             
     | 
| 
      
 491 
     | 
    
         
            +
                  if pub_socket
         
     | 
| 
      
 492 
     | 
    
         
            +
                    # send as plain text so that ZMQ SUB filtering can be done on the job_id.
         
     | 
| 
      
 493 
     | 
    
         
            +
                    # sent as two lines: job_id then LF then status.
         
     | 
| 
      
 494 
     | 
    
         
            +
                    pub_socket.send("#{job.id}\n#{parameters[:status]}")
         
     | 
| 
      
 495 
     | 
    
         
            +
                  end
         
     | 
| 
      
 496 
     | 
    
         
            +
             
     | 
| 
      
 497 
     | 
    
         
            +
                  socket_ids = job_subscribers.delete(job.id.to_s)
         
     | 
| 
      
 498 
     | 
    
         
            +
                  if socket_ids
         
     | 
| 
      
 499 
     | 
    
         
            +
                    socket_ids.each do |socket_id|
         
     | 
| 
      
 500 
     | 
    
         
            +
                      # send the command.
         
     | 
| 
      
 501 
     | 
    
         
            +
                      command = Broker::Command.new(socket_id, parameters)
         
     | 
| 
      
 502 
     | 
    
         
            +
                      JobDispatch.logger.info("JobDispatch::Broker Sending job notification for job id '#{job.id}' status = #{status} to socket: #{socket_id.to_json}")
         
     | 
| 
      
 503 
     | 
    
         
            +
                      send_command(command)
         
     | 
| 
      
 504 
     | 
    
         
            +
                    end
         
     | 
| 
      
 505 
     | 
    
         
            +
                  end
         
     | 
| 
      
 506 
     | 
    
         
            +
                end
         
     | 
| 
      
 507 
     | 
    
         
            +
             
     | 
| 
      
 508 
     | 
    
         
            +
                def job_status_parameters(job)
         
     | 
| 
      
 509 
     | 
    
         
            +
                  {
         
     | 
| 
      
 510 
     | 
    
         
            +
                      status: Job::STATUS_STRINGS[job.status] || 'unknown',
         
     | 
| 
      
 511 
     | 
    
         
            +
                      job_id: job.id.to_s,
         
     | 
| 
      
 512 
     | 
    
         
            +
                      job: json_for_job(job)
         
     | 
| 
      
 513 
     | 
    
         
            +
                  }
         
     | 
| 
      
 514 
     | 
    
         
            +
                end
         
     | 
| 
      
 515 
     | 
    
         
            +
             
     | 
| 
      
 516 
     | 
    
         
            +
                class MissingParameterError < StandardError
         
     | 
| 
      
 517 
     | 
    
         
            +
                end
         
     | 
| 
      
 518 
     | 
    
         
            +
              end
         
     | 
| 
      
 519 
     | 
    
         
            +
            end
         
     | 
| 
      
 520 
     | 
    
         
            +
             
     | 
| 
      
 521 
     | 
    
         
            +
            require 'job_dispatch/broker/command'
         
     | 
| 
      
 522 
     | 
    
         
            +
            require 'job_dispatch/broker/internal_job'
         
     | 
| 
      
 523 
     | 
    
         
            +
            require 'job_dispatch/broker/socket'
         
     | 
| 
         @@ -0,0 +1,34 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # encoding: UTF-8
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module JobDispatch
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
              # This is a simple class for making synchronous calls to the Job Queue dispatcher.
         
     | 
| 
      
 6 
     | 
    
         
            +
              class Client
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                class Proxy
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                  attr :options
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                  def initialize(client, target, options={})
         
     | 
| 
      
 13 
     | 
    
         
            +
                    @client = client
         
     | 
| 
      
 14 
     | 
    
         
            +
                    @target = case target
         
     | 
| 
      
 15 
     | 
    
         
            +
                                when Class
         
     | 
| 
      
 16 
     | 
    
         
            +
                                  target.to_s
         
     | 
| 
      
 17 
     | 
    
         
            +
                                when String
         
     | 
| 
      
 18 
     | 
    
         
            +
                                  target
         
     | 
| 
      
 19 
     | 
    
         
            +
                                else
         
     | 
| 
      
 20 
     | 
    
         
            +
                                  raise NotImplementedError, "Don't yet know how to serialize an object instance as a target"
         
     | 
| 
      
 21 
     | 
    
         
            +
                              end
         
     | 
| 
      
 22 
     | 
    
         
            +
                    @options = options
         
     | 
| 
      
 23 
     | 
    
         
            +
                  end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                  def method_missing(method, *args)
         
     | 
| 
      
 26 
     | 
    
         
            +
                    @client.enqueue(queue: queue, target: @target, method: method.to_s, parameters: args)
         
     | 
| 
      
 27 
     | 
    
         
            +
                  end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                  def queue
         
     | 
| 
      
 30 
     | 
    
         
            +
                    @options[:queue] || :default
         
     | 
| 
      
 31 
     | 
    
         
            +
                  end
         
     | 
| 
      
 32 
     | 
    
         
            +
                end
         
     | 
| 
      
 33 
     | 
    
         
            +
              end
         
     | 
| 
      
 34 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,18 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # encoding: UTF-8
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module JobDispatch
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
              # This is a simple class for making synchronous calls to the Job Queue dispatcher.
         
     | 
| 
      
 6 
     | 
    
         
            +
              class Client
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                # When a proxy result is a failure, this exception is a class that will encapsulate the result.
         
     | 
| 
      
 9 
     | 
    
         
            +
                class ProxyError < StandardError
         
     | 
| 
      
 10 
     | 
    
         
            +
                  attr :response
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                  def initialize(message, response=nil)
         
     | 
| 
      
 13 
     | 
    
         
            +
                    @response = response
         
     | 
| 
      
 14 
     | 
    
         
            +
                    super(message)
         
     | 
| 
      
 15 
     | 
    
         
            +
                  end
         
     | 
| 
      
 16 
     | 
    
         
            +
                end
         
     | 
| 
      
 17 
     | 
    
         
            +
              end
         
     | 
| 
      
 18 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,29 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # encoding: UTF-8
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module JobDispatch
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
              # This is a simple class for making synchronous calls to the Job Queue dispatcher.
         
     | 
| 
      
 6 
     | 
    
         
            +
              class Client
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                class SynchronousProxy < Proxy
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                  def method_missing(method, *args)
         
     | 
| 
      
 11 
     | 
    
         
            +
                    job_spec = @client.enqueue(queue: queue, target: @target, method: method.to_s, parameters: args)
         
     | 
| 
      
 12 
     | 
    
         
            +
                    completed_job = @client.notify(job_spec["job_id"])
         
     | 
| 
      
 13 
     | 
    
         
            +
                    if completed_job.nil?
         
     | 
| 
      
 14 
     | 
    
         
            +
                      raise ProxyError.new("Internal error! There should not be a nil response from the broker.")
         
     | 
| 
      
 15 
     | 
    
         
            +
                    end
         
     | 
| 
      
 16 
     | 
    
         
            +
                    result = completed_job["job"] && completed_job["job"]["result"]
         
     | 
| 
      
 17 
     | 
    
         
            +
                    case completed_job["status"]
         
     | 
| 
      
 18 
     | 
    
         
            +
                      when "failed"
         
     | 
| 
      
 19 
     | 
    
         
            +
                        raise ProxyError.new("Job failed: #{result}", completed_job)
         
     | 
| 
      
 20 
     | 
    
         
            +
                      when "completed"
         
     | 
| 
      
 21 
     | 
    
         
            +
                        return result
         
     | 
| 
      
 22 
     | 
    
         
            +
                      else
         
     | 
| 
      
 23 
     | 
    
         
            +
                        raise ProxyError.new("Notify should not return for a pending or in progress job!")
         
     | 
| 
      
 24 
     | 
    
         
            +
                    end
         
     | 
| 
      
 25 
     | 
    
         
            +
                  end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                end
         
     | 
| 
      
 28 
     | 
    
         
            +
              end
         
     | 
| 
      
 29 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,49 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # encoding: UTF-8
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'active_support/core_ext/hash'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module JobDispatch
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
              # This is a simple class for making synchronous calls to the Job Queue dispatcher.
         
     | 
| 
      
 8 
     | 
    
         
            +
              class Client
         
     | 
| 
      
 9 
     | 
    
         
            +
                def initialize(connect_address=nil)
         
     | 
| 
      
 10 
     | 
    
         
            +
                  @socket = JobDispatch.context.socket(ZMQ::REQ)
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @socket.connect(connect_address || JobDispatch.config.broker[:connect])
         
     | 
| 
      
 12 
     | 
    
         
            +
                end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                def send_request(command, options={})
         
     | 
| 
      
 15 
     | 
    
         
            +
                  options[:command] = command
         
     | 
| 
      
 16 
     | 
    
         
            +
                  @socket.send(JSON.dump(options))
         
     | 
| 
      
 17 
     | 
    
         
            +
                  json = @socket.recv
         
     | 
| 
      
 18 
     | 
    
         
            +
                  #puts "Received: #{json}"
         
     | 
| 
      
 19 
     | 
    
         
            +
                  response = JSON.parse(json)
         
     | 
| 
      
 20 
     | 
    
         
            +
                  response.is_a?(Hash) ? response.with_indifferent_access : response
         
     | 
| 
      
 21 
     | 
    
         
            +
                end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                def method_missing(method, *args, ** kwargs)
         
     | 
| 
      
 24 
     | 
    
         
            +
                  payload = kwargs
         
     | 
| 
      
 25 
     | 
    
         
            +
                  payload[:parameters] = args
         
     | 
| 
      
 26 
     | 
    
         
            +
                  send_request(method, payload)
         
     | 
| 
      
 27 
     | 
    
         
            +
                end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                def proxy_for(target, options={})
         
     | 
| 
      
 30 
     | 
    
         
            +
                  Proxy.new(self, target, options)
         
     | 
| 
      
 31 
     | 
    
         
            +
                end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                def synchronous_proxy_for(target, options={})
         
     | 
| 
      
 34 
     | 
    
         
            +
                  SynchronousProxy.new(self, target, options)
         
     | 
| 
      
 35 
     | 
    
         
            +
                end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                def enqueue(job_attrs)
         
     | 
| 
      
 38 
     | 
    
         
            +
                  send_request('enqueue', {job: job_attrs})
         
     | 
| 
      
 39 
     | 
    
         
            +
                end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                def notify(job_id)
         
     | 
| 
      
 42 
     | 
    
         
            +
                  send_request('notify', {job_id: job_id})
         
     | 
| 
      
 43 
     | 
    
         
            +
                end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
              end
         
     | 
| 
      
 46 
     | 
    
         
            +
            end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
            require 'job_dispatch/client/proxy'
         
     | 
| 
      
 49 
     | 
    
         
            +
            require 'job_dispatch/client/synchronous_proxy'
         
     |