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'
|