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