job_dispatch 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e00ac3ea419f26f9e3d8ae4a52a73c267f306ce7
4
- data.tar.gz: 2f2941010e8775f2e6ec0759952438fd899f17a5
3
+ metadata.gz: 401fd02367fe4acc36558989fe00883365afcd87
4
+ data.tar.gz: f45f4e6595a88462a394deb27a103d9a9d274ae5
5
5
  SHA512:
6
- metadata.gz: 03c7b5606808c97b37c0c3903ec9058db29f8f96741a5b4b42a2220e459017249b9b48cb6b701c8fbd75392e0ba212bee8dc38ff29257512fd119e39b3739671
7
- data.tar.gz: 414171d3052dcfdc29f5c8c30f3216aeb755d4bdf96c205452016241c1ac493bd859c374d29c24d1287709092bc16e0d04cda87b020a13b9f4228bf9f8bf13a7
6
+ metadata.gz: d07c1dc6f62db463ec888a8e3130893bfb37541854756477200ecc77d79ae8e0e615d4773925b18f71798fccb59d3bb308c1e8ec7e1696958b9b694dd5e7c161
7
+ data.tar.gz: f3a9e4646d3f4e0e1aea6b5b7691758f3b29057215bbc104875239375a6efb0a99f1ea035d86e7e6ed6dc913d87dffcf8731b42c8e8874a52400aadcdccb1348
data/.gitignore CHANGED
@@ -16,4 +16,6 @@ test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
18
  .idea
19
+ *.orig
20
+ *.swp
19
21
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # job_dispatch change log
2
2
 
3
+ ## Version 0.1.0
4
+
5
+ * Added 'num_tcp_connections' to status command response
6
+ * Fixed leaking sockets from job-worker command.
7
+
3
8
  ## Version 0.0.2
4
9
 
5
10
  * Broker sends an idle command to a worker immediately upon connect. This helps recover from a case where a worker
data/bin/job-worker CHANGED
@@ -28,8 +28,6 @@ JobDispatch.load_config_from_yml('config/job_dispatch.yml', ENV["RAILS_ENV"])
28
28
 
29
29
  require File.expand_path('config/environment.rb', ROOT_DIR)
30
30
 
31
- JobDispatch.logger = Rails.logger
32
-
33
31
  endpoint = JobDispatch.config.broker[:connect]
34
32
  if endpoint.nil? || endpoint.empty?
35
33
  $stderr.puts "No Job Dispatch broker connect address has been specified."
@@ -11,7 +11,7 @@ module JobDispatch
11
11
  class Broker
12
12
 
13
13
  WORKER_IDLE_TIME = 10.123
14
- POLL_TIME = 5.123
14
+ POLL_TIME = 31
15
15
  STOP_SIGNALS = %w[INT TERM KILL]
16
16
 
17
17
  IdleWorker = Struct.new :worker_id, :idle_since, :queue, :worker_name, :idle_count
@@ -32,6 +32,7 @@ module JobDispatch
32
32
  attr :job_subscribers # Key: job_id, value: list of Socket Identities waiting for job completion notifications.
33
33
  attr :pub_socket
34
34
  attr_accessor :reply_exceptions
35
+ attr :queues_ready
35
36
 
36
37
  def initialize(worker_bind_address, wakeup_bind_address, publish_bind_address=nil)
37
38
  @worker_bind_address = worker_bind_address
@@ -48,6 +49,7 @@ module JobDispatch
48
49
  @jobs_in_progress_workers = {} #key: job_id, value: worker_id
49
50
  @worker_names = {} # Key: Symbol socket identity, value: String claimed name of worker
50
51
  @job_subscribers = {} # Key: job_id, value: list of Socket Identities waiting for job completion notifications.
52
+ @queues_ready = {} # Key: Symbol queue name, value: bool ready?
51
53
  @status = "OK"
52
54
  @reply_exceptions = true
53
55
 
@@ -127,7 +129,7 @@ module JobDispatch
127
129
  # TODO: calculate the amount of time to sleep to wake up such that a scheduled event happens as close
128
130
  # as possible to the time it was supposed to happen. This could additionally mean that the POLL_TIME
129
131
  # could be arbitrarily large. As any communication with the broker will wake it immediately.
130
- poll_time = POLL_TIME
132
+ poll_time = JobDispatch.config.broker_options.try(:[], :poll_time) || POLL_TIME
131
133
  poller.poll(poll_time)
132
134
 
133
135
  if @wake_socket && poller.readables.include?(@wake_socket)
@@ -136,6 +138,7 @@ module JobDispatch
136
138
 
137
139
  if poller.readables.include?(socket.socket)
138
140
  command = read_command
141
+ JobDispatch.logger.debug("JobDispatch::Broker received command: #{command.command}(#{command.parameters.inspect})")
139
142
  reply = process_command(command)
140
143
  send_command(reply) if reply
141
144
  end
@@ -217,6 +220,12 @@ module JobDispatch
217
220
  when "enqueue"
218
221
  reply.parameters = create_job(command)
219
222
 
223
+ when "last"
224
+ reply.parameters = last_job(command)
225
+
226
+ when "fetch"
227
+ reply.parameters = fetch_job(command)
228
+
220
229
  when "quit"
221
230
  process_quit
222
231
  reply.parameters = {:status => 'bye'}
@@ -233,6 +242,7 @@ module JobDispatch
233
242
  if reply_exceptions
234
243
  # all others reply over socket.
235
244
  JobDispatch.logger.error("JobDispatch::Broker #{e}")
245
+ JobDispatch.logger.error e.backtrace.join("\n")
236
246
  reply.parameters = {:status => 'error', :message => e.to_s}
237
247
  else
238
248
  # used during testing to raise errors so that Rspec can catch them as a test failure.
@@ -294,6 +304,7 @@ module JobDispatch
294
304
  idle_worker = IdleWorker.new(command.worker_id, Time.now, queue, command.worker_name, idle_count)
295
305
  workers_waiting_for_jobs[command.worker_id] = idle_worker
296
306
  queues[queue] << command.worker_id
307
+ queues_ready[queue] = true
297
308
  if command.worker_name # this is only sent on initial requests.
298
309
  worker_names[command.worker_id] = command.worker_name
299
310
  end
@@ -323,9 +334,9 @@ module JobDispatch
323
334
 
324
335
  def dispatch_jobs_to_workers
325
336
  # dequeue jobs from database for each queue
326
- @queues.each_pair do |queue, worker_ids|
337
+ queues.each_pair do |queue, worker_ids|
327
338
  # we only need to check the database if there are available workers in that queue
328
- if worker_ids.count > 0
339
+ if worker_ids.count > 0 && queues_ready[queue]
329
340
  worker_id = worker_ids.first
330
341
 
331
342
  job = begin
@@ -344,8 +355,10 @@ module JobDispatch
344
355
  job.expire_execution_at = Time.now + (job.timeout || Job::DEFAULT_EXECUTION_TIMEOUT)
345
356
  job.status = JobDispatch::Job::IN_PROGRESS
346
357
  job.save
347
-
348
358
  publish_job_status(job)
359
+ else
360
+ # no job. mark the queue as not ready so we don't repeatedly check for jobs in an empty queue.
361
+ queues_ready[queue] = false
349
362
  end
350
363
  end
351
364
  end
@@ -425,19 +438,24 @@ module JobDispatch
425
438
 
426
439
 
427
440
  def json_for_job(job)
428
- hash = if job.respond_to? :as_job_queue_item
429
- job.as_job_queue_item
430
- else
431
- job.as_json
432
- end.with_indifferent_access
433
- hash[:id] = hash[:id].to_s
434
- hash
441
+ if job
442
+ hash = if job.respond_to? :as_job_queue_item
443
+ job.as_job_queue_item
444
+ else
445
+ job.as_json
446
+ end.with_indifferent_access
447
+ hash[:id] = hash[:id].to_s
448
+ hash
449
+ end
435
450
  end
436
451
 
437
452
 
438
453
  def status_response
454
+ num_tcp_connections = `lsof -p #{Process.pid}`.split.select { |l| l=~ /TCP/ }.count
455
+
439
456
  response = {
440
457
  :status => status,
458
+ :num_tcp_connections => num_tcp_connections,
441
459
  :queues => {}
442
460
  }
443
461
 
@@ -477,14 +495,15 @@ module JobDispatch
477
495
  # @return [Hash] result to be sent to client.
478
496
  def touch_job(command)
479
497
  job_id = command.parameters[:job_id]
480
- timeout = command.parameters[:timeout] || Job::DEFAULT_EXECUTION_TIMEOUT
481
498
  job = @jobs_in_progress[job_id]
482
499
  if job
500
+ timeout = command.parameters[:timeout] || job.timeout || Job::DEFAULT_EXECUTION_TIMEOUT
483
501
  job.expire_execution_at = Time.now + timeout
484
502
  JobDispatch.logger.info("JobDispatch::Broker#touch timeout on job #{job_id} to #{job.expire_execution_at}")
485
503
  job.save
486
504
  {status: "success"}
487
505
  else
506
+ JobDispatch.logger.info("JobDispatch::Broker#touch job #{job_id} not in progress.")
488
507
  {status: "error", message: "the specified job does not appear to be in progress"}
489
508
  end
490
509
  end
@@ -494,10 +513,42 @@ module JobDispatch
494
513
  raise MissingParameterError, "Missing 'job' from command" unless command.parameters[:job].present?
495
514
 
496
515
  job_attrs = command.parameters[:job]
516
+ job_attrs[:queue] ||= :default
497
517
  job = job_source.create!(job_attrs)
518
+ queues_ready[job_attrs[:queue].to_sym] = true
498
519
  {status: 'success', job_id: job.id.to_s}
499
520
  rescue StandardError => e
500
521
  JobDispatch.logger.error "JobDispatch::Broker#create_job error: #{e}"
522
+ JobDispatch.logger.error e.backtrace.join("\n")
523
+ {status: 'error', message: e.to_s}
524
+ end
525
+ end
526
+
527
+ def last_job(command)
528
+ begin
529
+ queue = command.parameters[:queue] || 'default'
530
+ job = job_source.where(:queue => queue).last
531
+ if job
532
+ {status: 'success', job: json_for_job(job)}
533
+ else
534
+ {status: 'error', message: 'no last job'}
535
+ end
536
+ rescue StandardError => e
537
+ JobDispatch.logger.error e.to_s
538
+ JobDispatch.logger.error e.backtrace.join("\n")
539
+ {status: 'error', message: e.to_s}
540
+ end
541
+ end
542
+
543
+ def fetch_job(command)
544
+ begin
545
+ raise "Missing parameter 'job_id'" unless command.parameters[:job_id]
546
+ job = job_source.find(command.parameters[:job_id])
547
+ raise "Job not found" unless job
548
+ {status: 'success', job: json_for_job(job)}
549
+ rescue StandardError => e
550
+ JobDispatch.logger.error e.to_s
551
+ JobDispatch.logger.error e.backtrace.join("\n")
501
552
  {status: 'error', message: e.to_s}
502
553
  end
503
554
  end
@@ -34,14 +34,46 @@ module JobDispatch
34
34
  SynchronousProxy.new(self, target, options)
35
35
  end
36
36
 
37
- def enqueue(job_attrs)
37
+ # Enqueue a job to be processed describe by the passed job attributes.
38
+ #
39
+ # Required attributes:
40
+ # target: The target object that will execute the job. typically a class.
41
+ # method: the message to be sent to the target.
42
+ # Optional:
43
+ # parameters: an array of parameters to be passed to the method.
44
+ # timeout: number of seconds after which the job is considered timed out and failed.
45
+ def enqueue(job_attrs={})
38
46
  send_request('enqueue', {job: job_attrs})
39
47
  end
40
48
 
49
+ # send a message to the dispatcher requesting to be notified when the job completes (or fails).
41
50
  def notify(job_id)
42
51
  send_request('notify', {job_id: job_id})
43
52
  end
44
53
 
54
+ # as the dispatcher what was the last job enqueued on the given queue (or default)
55
+ def last(queue=nil)
56
+ job_or_raise send_request('last', {queue: queue||'default'})
57
+ end
58
+
59
+ # fetch the complete details for hte last job
60
+ def fetch(job_id)
61
+ job_or_raise send_request('fetch', {job_id: job_id})
62
+ end
63
+
64
+ private
65
+
66
+ def job_or_raise(response)
67
+ if response.is_a?(Hash) && response[:status] == 'success'
68
+ response[:job]
69
+ else
70
+ p response
71
+ raise ClientError, response[:message]
72
+ end
73
+ end
74
+ end
75
+
76
+ class ClientError < StandardError
45
77
  end
46
78
  end
47
79
 
@@ -1,3 +1,3 @@
1
1
  module JobDispatch
2
- VERSION = "0.0.2"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -102,6 +102,7 @@ module JobDispatch
102
102
  job_id = Thread.current["JobDispatch::Worker.job_id"]
103
103
  if sock && job_id
104
104
  sock.send_touch(job_id, timeout)
105
+ JobDispatch.logger.debug { "touching job #{job_id}"}
105
106
  end
106
107
  end
107
108
 
@@ -36,6 +36,7 @@ module JobDispatch
36
36
  backtrace: ex.backtrace,
37
37
  }
38
38
  @status = :error
39
+ JobDispatch.logger.debug ex
39
40
  ensure
40
41
  Thread.current["JobDispatch::Worker.job_id"] = nil
41
42
  JobDispatch.logger.info "Worker completed job #{job_id}: #{target}.#{method}, status: #{@status}"
@@ -8,11 +8,14 @@ module JobDispatch
8
8
  class Socket
9
9
 
10
10
  attr :socket
11
+ attr :touch_socket
11
12
  attr :item_class
12
13
 
13
14
  def initialize(connect_address, item_klass)
14
15
  @socket = JobDispatch.context.socket(ZMQ::REQ)
15
16
  @socket.connect(connect_address)
17
+ @touch_socket = JobDispatch.context.socket(ZMQ::DEALER)
18
+ @touch_socket.connect(connect_address)
16
19
  @item_class = item_klass
17
20
  end
18
21
 
@@ -29,7 +32,14 @@ module JobDispatch
29
32
  end
30
33
 
31
34
  def close
32
- @socket.close
35
+ if @socket
36
+ @socket.close rescue nil
37
+ @socket = nil
38
+ end
39
+ if @touch_socket
40
+ @touch_socket.close rescue nil
41
+ @touch_socket = nil
42
+ end
33
43
  end
34
44
 
35
45
  def identity
@@ -45,8 +55,9 @@ module JobDispatch
45
55
  #
46
56
  # @return [JobDispatch::Item] the item to be processed (or nil if there isn't a valid job)
47
57
  def read_item
48
- json = @socket.recv
49
58
  begin
59
+ drain_touch_socket
60
+ json = @socket.recv
50
61
  params = JSON.parse(json)
51
62
  case params["command"]
52
63
  when "job"
@@ -67,6 +78,14 @@ module JobDispatch
67
78
  item
68
79
  end
69
80
 
81
+ # drain any messages that may have been received on the touch socket.
82
+ def drain_touch_socket
83
+ loop do
84
+ message = @touch_socket.recv_nonblock
85
+ break if message.nil?
86
+ end
87
+ end
88
+
70
89
  # after execution, send the response.
71
90
  def send_response(job_id, status, result)
72
91
  JobDispatch.logger.info "Worker #{Process.pid} completed job_id: #{job_id}: #{status}, result: #{result}"
@@ -86,11 +105,8 @@ module JobDispatch
86
105
  job_id: job_id
87
106
  }
88
107
  hash[:timeout] = timeout if timeout
89
- @socket.send(JSON.dump(hash))
90
- json = @socket.recv # wait for acknowledgement... this could be done via pub/sub to be asynchronous.
91
- JSON.parse(json) rescue {:error => "Failed to decode JSON from dispatcher: #{json}"}
108
+ @touch_socket.send(JSON.dump(hash))
92
109
  end
93
-
94
110
  end
95
111
  end
96
112
  end
@@ -488,6 +488,8 @@ describe JobDispatch::Broker do
488
488
  @job = FactoryGirl.build :job
489
489
  @socket = double('Broker::Socket', :send_command => nil)
490
490
  subject.stub(:socket => @socket)
491
+ @job_class = double('JobClass')
492
+ JobDispatch.config.job_class = @job_class
491
493
  end
492
494
 
493
495
  it "the job is sent to an idle worker" do
@@ -497,10 +499,8 @@ describe JobDispatch::Broker do
497
499
  expect(cmd.parameters[:target]).to eq(@job.target)
498
500
  end
499
501
 
500
- job_class = double('JobClass')
501
- job_class.stub(:dequeue_job_for_queue).and_return(@job)
502
- job_class.should_receive(:dequeue_job_for_queue).with('example')
503
- JobDispatch.config.job_class = job_class
502
+ @job_class.stub(:dequeue_job_for_queue).and_return(@job)
503
+ @job_class.should_receive(:dequeue_job_for_queue).with('example')
504
504
 
505
505
  # send ready command => adds idle worker state
506
506
  subject.workers_waiting_for_reply << worker_id # simulating read_command
@@ -511,9 +511,29 @@ describe JobDispatch::Broker do
511
511
  }))
512
512
  expect(@result).to be_nil # no immediate response
513
513
  expect(subject.workers_waiting_for_jobs[worker_id]).not_to be_nil
514
+ subject.queues_ready[:example] = true
514
515
 
515
516
  subject.dispatch_jobs_to_workers
516
517
  end
518
+
519
+ it "when no job is found, the queue is marked inactive" do
520
+ # send ready command => adds idle worker state
521
+ subject.workers_waiting_for_reply << worker_id # simulating read_command
522
+ @result = subject.process_command(Command.new(worker_id, {
523
+ command: 'ready',
524
+ queue: 'example',
525
+ worker_name: 'ruby worker',
526
+ }))
527
+
528
+ @job_class.stub(:dequeue_job_for_queue).and_return(nil)
529
+
530
+ expect(@result).to be_nil # no immediate response
531
+ expect(subject.workers_waiting_for_jobs[worker_id]).not_to be_nil
532
+ subject.queues_ready[:example] = true
533
+
534
+ subject.dispatch_jobs_to_workers
535
+ expect(subject.queues_ready[:example]).to be_false
536
+ end
517
537
  end
518
538
 
519
539
  context "when an error occurs dequeuing jobs" do
@@ -578,7 +598,7 @@ describe JobDispatch::Broker do
578
598
 
579
599
  context "touching a job" do
580
600
  before :each do
581
- @time = Time.now
601
+ @time = Time.now.change(:usec => 0)
582
602
  # this worker will be IDLE
583
603
  @job = FactoryGirl.build :job, :expire_execution_at => @time + 5.seconds
584
604
  @job_id = @job.id.to_s
@@ -594,7 +614,7 @@ describe JobDispatch::Broker do
594
614
  Timecop.freeze(@time) do
595
615
  subject.touch_job(Command.new(worker_id, {command: "touch", job_id: @job_id}))
596
616
  end
597
- expect(@job.expire_execution_at).to eq(@time + JobDispatch::Job::DEFAULT_EXECUTION_TIMEOUT)
617
+ expect(@job.expire_execution_at).to eq(@time + @job.timeout)
598
618
  end
599
619
 
600
620
  it "updates the expire_execution_at time with a custom timeout" do
@@ -608,10 +628,10 @@ describe JobDispatch::Broker do
608
628
  context "enqueue a job" do
609
629
  before :each do
610
630
  @job_attrs = FactoryGirl.attributes_for :job
631
+ JobDispatch.config.job_class = double('JobClass')
611
632
  end
612
633
 
613
634
  it "Creates a job" do
614
- JobDispatch.config.job_class = double('JobClass')
615
635
  JobDispatch.config.job_class.should_receive(:create!).with(@job_attrs)
616
636
  command = Command.new(:some_client, {command: "enqueue", job: @job_attrs})
617
637
  subject.process_command(command)
@@ -619,7 +639,6 @@ describe JobDispatch::Broker do
619
639
 
620
640
  it "returns the job id" do
621
641
  job_id = 12345
622
- JobDispatch.config.job_class = double('JobClass')
623
642
  JobDispatch.config.job_class.stub(:create! => double('Job', :id => job_id))
624
643
  command = Command.new(:some_client, {command: "enqueue", job: @job_attrs})
625
644
  result = subject.process_command(command)
@@ -628,7 +647,6 @@ describe JobDispatch::Broker do
628
647
  end
629
648
 
630
649
  it "returns an error if the arguments are no good" do
631
- JobDispatch.config.job_class = double('JobClass')
632
650
  JobDispatch.config.job_class.stub(:create!).and_raise("no good") # simulate some database save error
633
651
  command = Command.new(:some_client, {command: "enqueue", job: @job_attrs})
634
652
  result = subject.process_command(command)
@@ -637,13 +655,18 @@ describe JobDispatch::Broker do
637
655
  end
638
656
 
639
657
  it "returns an error if the 'job' parameter is missing" do
640
- JobDispatch.config.job_class = double('JobClass')
641
658
  command = Command.new(:some_client, {command: "enqueue"})
642
659
  result = subject.process_command(command)
643
660
  expect(result.parameters[:status]).to eq('error')
644
661
  end
645
- end
646
662
 
663
+ it "marks the queue as ready" do
664
+ JobDispatch.config.job_class.stub(:create! => double('Job', :id => 1))
665
+ command = Command.new(:some_client, {command: "enqueue", job: @job_attrs})
666
+ result = subject.process_command(command)
667
+ expect(subject.queues_ready[:default]).to be_true
668
+ end
669
+ end
647
670
 
648
671
  context "'notify'" do
649
672
 
@@ -779,4 +802,61 @@ describe JobDispatch::Broker do
779
802
  end
780
803
  end
781
804
 
805
+ context "last" do
806
+ let(:json){ {'id' => '12341234', 'target' => 'Example', 'method' => 'method'}}
807
+ before do
808
+ @job_class = double('JobClass')
809
+ JobDispatch.config.job_class = @job_class
810
+ end
811
+ it "returns last job in specified queue" do
812
+ command = Command.new(:client, {command: 'last', queue: 'ruby'})
813
+ relation = double('relation')
814
+ @job_class.should_receive(:where).with(queue: 'ruby').and_return(relation)
815
+ relation.should_receive(:last).and_return(double("job", id: "12341234", as_json: json))
816
+ result = subject.process_command(command)
817
+ expect(result.parameters).to eq({status: 'success', job: json})
818
+ end
819
+ it "returns last job in default queue" do
820
+ command = Command.new(:client, {command: 'last'})
821
+ relation = double('relation')
822
+ @job_class.should_receive(:where).with(queue: 'default').and_return(relation)
823
+ relation.should_receive(:last).and_return(double("job", id: "12341234", as_json: json))
824
+ result = subject.process_command(command)
825
+ expect(result.parameters).to eq({status: 'success', job: json})
826
+ end
827
+ it "handles no last job" do
828
+ command = Command.new(:client, {command: 'last'})
829
+ relation = double('relation')
830
+ @job_class.should_receive(:where).with(queue: 'default').and_return(relation)
831
+ relation.should_receive(:last).and_return(nil)
832
+ result = subject.process_command(command)
833
+ expect(result.parameters).to eq({status: 'error', message: 'no last job'})
834
+ end
835
+ end
836
+
837
+ context "fetch" do
838
+ before do
839
+ @job_class = double('JobClass')
840
+ JobDispatch.config.job_class = @job_class
841
+ end
842
+ it "returns the job" do
843
+ command = Command.new(:client, {command: 'fetch', job_id: '12341234'})
844
+ json = {'id' => '12341234', 'queue' => 'ruby', 'target' => 'String', 'method' => 'new'}
845
+ job = double("Job", as_json: json)
846
+ @job_class.should_receive(:find).with('12341234').and_return(job)
847
+ result = subject.process_command(command)
848
+ expect(result.parameters).to eq({status: 'success', job: json})
849
+ end
850
+ it "returns error when job_id is not present" do
851
+ command = Command.new(:client, {command: 'fetch'})
852
+ result = subject.process_command(command)
853
+ expect(result.parameters[:status]).to eq('error')
854
+ end
855
+ it "returns error when job_id is not found" do
856
+ command = Command.new(:client, {command: 'fetch', job_id: '12341234'})
857
+ @job_class.should_receive(:find).with('12341234').and_raise(StandardError, "not found")
858
+ result = subject.process_command(command)
859
+ expect(result.parameters[:status]).to eq('error')
860
+ end
861
+ end
782
862
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: job_dispatch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Connolly
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-05-13 00:00:00.000000000 Z
11
+ date: 2014-07-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rbczmq