job_dispatch 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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'
|