job_dispatch 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +13 -0
  5. data/Gemfile +20 -0
  6. data/Guardfile +13 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +85 -0
  9. data/Rakefile +10 -0
  10. data/bin/job-dispatcher +34 -0
  11. data/bin/job-status +69 -0
  12. data/bin/job-worker +40 -0
  13. data/examples/mongoid-job.rb +43 -0
  14. data/job_dispatch.gemspec +33 -0
  15. data/lib/job_dispatch/broker/command.rb +45 -0
  16. data/lib/job_dispatch/broker/internal_job.rb +32 -0
  17. data/lib/job_dispatch/broker/socket.rb +85 -0
  18. data/lib/job_dispatch/broker.rb +523 -0
  19. data/lib/job_dispatch/client/proxy.rb +34 -0
  20. data/lib/job_dispatch/client/proxy_error.rb +18 -0
  21. data/lib/job_dispatch/client/synchronous_proxy.rb +29 -0
  22. data/lib/job_dispatch/client.rb +49 -0
  23. data/lib/job_dispatch/configuration.rb +7 -0
  24. data/lib/job_dispatch/identity.rb +54 -0
  25. data/lib/job_dispatch/job.rb +44 -0
  26. data/lib/job_dispatch/signaller.rb +30 -0
  27. data/lib/job_dispatch/sockets/enqueue.rb +18 -0
  28. data/lib/job_dispatch/status.rb +79 -0
  29. data/lib/job_dispatch/version.rb +3 -0
  30. data/lib/job_dispatch/worker/item.rb +43 -0
  31. data/lib/job_dispatch/worker/socket.rb +96 -0
  32. data/lib/job_dispatch/worker.rb +120 -0
  33. data/lib/job_dispatch.rb +97 -0
  34. data/spec/factories/jobs.rb +19 -0
  35. data/spec/job_dispatch/broker/socket_spec.rb +53 -0
  36. data/spec/job_dispatch/broker_spec.rb +737 -0
  37. data/spec/job_dispatch/identity_spec.rb +88 -0
  38. data/spec/job_dispatch/job_spec.rb +77 -0
  39. data/spec/job_dispatch/worker/socket_spec.rb +32 -0
  40. data/spec/job_dispatch/worker_spec.rb +24 -0
  41. data/spec/job_dispatch_spec.rb +0 -0
  42. data/spec/spec_helper.rb +23 -0
  43. data/spec/support/test_job.rb +30 -0
  44. 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'
@@ -0,0 +1,7 @@
1
+ require 'active_support/configurable'
2
+
3
+ module JobDispatch
4
+ class Configuration
5
+ include ActiveSupport::Configurable
6
+ end
7
+ end