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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b05fed659c186b001b926e08650b3a281160668f
|
4
|
+
data.tar.gz: 6ed34896dfbe1a5dcc1382a36b39e7c3e73f2f2f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4a211880bafb99581f6a4d8bd884ddcc37d68170f5ba28fde9943d0cdf2eb9735a6cd3c78f683ef4c923f02bc4c19d5479461904d8b25d060088c088048e2747
|
7
|
+
data.tar.gz: 30656712d978383424f23945daa8ae77e3f61dd6c4314d324b8063203e6ecf604347588074b9db0f9ea39fed4b0289869d55d20dc5e3dfaed901caea7d5ad674
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in job_dispatch.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
# required for testing rubinius on travis:
|
7
|
+
platforms :rbx do
|
8
|
+
gem 'rubysl', '~> 2.0'
|
9
|
+
gem 'rubinius', '~> 2.0'
|
10
|
+
gem "rubinius-coverage", github: "rubinius/rubinius-coverage"
|
11
|
+
end
|
12
|
+
|
13
|
+
group :test do
|
14
|
+
gem 'guard'
|
15
|
+
group :mac do
|
16
|
+
gem 'growl'
|
17
|
+
gem 'guard-rspec'
|
18
|
+
gem 'terminal-notifier-guard'
|
19
|
+
end
|
20
|
+
end
|
data/Guardfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
# To use zeus, cmd: 'zeus rspec'
|
5
|
+
guard 'rspec', cmd: 'rspec' do
|
6
|
+
|
7
|
+
notification :growl, :sticky => false #, :path => '/usr/local/bin/growlnotify'
|
8
|
+
notification :terminal_notifier
|
9
|
+
|
10
|
+
watch(%r{^spec/.+_spec\.rb$})
|
11
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
12
|
+
watch('spec/spec_helper.rb') { "spec" }
|
13
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Matt Connolly
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# JobDispatch
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/mobiledataanywhere/job_dispatch.png)](https://travis-ci.org/mobiledataanywhere/job_dispatch)
|
4
|
+
|
5
|
+
Job Dispatch is a gem for dispatching jobs to workers in an asynchronous manner.
|
6
|
+
Job Dispatch does not require any specific database, deliberately separating the storage of jobs
|
7
|
+
from the dispatching of jobs to workers.
|
8
|
+
Jobs are dispatched using ZeroMQ messages and workers can be implemented in any language.
|
9
|
+
|
10
|
+
Only the dispatcher needs to access the database. This decouples the workers from the database.
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Add this line to your application's Gemfile:
|
15
|
+
|
16
|
+
gem 'job_dispatch'
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
$ bundle
|
21
|
+
|
22
|
+
Or install it yourself as:
|
23
|
+
|
24
|
+
$ gem install job_dispatch
|
25
|
+
|
26
|
+
|
27
|
+
## Requirements
|
28
|
+
|
29
|
+
### Dispatcher
|
30
|
+
|
31
|
+
This gem uses [rbczmq](https://github.com/methodmissing/rbczmq) for sending messages, so its use is limited to
|
32
|
+
platforms supporting this gem, namely a posix OS and MRI Ruby or Rubinius.
|
33
|
+
|
34
|
+
JRuby is not supported. Nor is Windows.
|
35
|
+
|
36
|
+
### Workers
|
37
|
+
|
38
|
+
The built in Ruby worker has the same ruby requirements as the broker. However, additional workers can be implemented in any
|
39
|
+
language on any platform provided they have access to ZeroMQ for messaging and a JSON library for reading/writing message payloads.
|
40
|
+
|
41
|
+
|
42
|
+
## Usage
|
43
|
+
|
44
|
+
TODO: Write usage instructions here.
|
45
|
+
|
46
|
+
JobDispatch will work with any Job model class, provided it fulfils the following:
|
47
|
+
|
48
|
+
Attributes:
|
49
|
+
|
50
|
+
* `queue`
|
51
|
+
* `status`
|
52
|
+
* `target`
|
53
|
+
* `method`
|
54
|
+
* `enqueued_at`
|
55
|
+
* `scheduled_at`
|
56
|
+
* `expire_execution_at`
|
57
|
+
* `completed_at`
|
58
|
+
* `timeout`
|
59
|
+
* `retry_count`
|
60
|
+
* `retry_delay`
|
61
|
+
* `result`
|
62
|
+
|
63
|
+
Class Methods:
|
64
|
+
|
65
|
+
dequeue_job_for_queue(queue, time=nil)
|
66
|
+
|
67
|
+
This method is for retrieving a single PENDING job from the database and atomically
|
68
|
+
marking it as being IN PROGRESS so that it can not be received again. It is the dispatcher's
|
69
|
+
Responsibility to update its status to completed/failed and schedule a retry if the
|
70
|
+
retry_count > 0.
|
71
|
+
|
72
|
+
See the example model classes in the examples/ directory.
|
73
|
+
|
74
|
+
## Contributing
|
75
|
+
|
76
|
+
1. Fork it
|
77
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
78
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
79
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
80
|
+
5. Create new Pull Request
|
81
|
+
|
82
|
+
## License
|
83
|
+
|
84
|
+
Licensed under MIT license, see LICENSE.txt.
|
85
|
+
|
data/Rakefile
ADDED
data/bin/job-dispatcher
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This command is expected to be run in the root of a Rails application which has a Job model class
|
4
|
+
# and has an initializer to configure JobDispatch.
|
5
|
+
|
6
|
+
ROOT_DIR = Dir.pwd
|
7
|
+
APP_PATH = File.expand_path('config/application.rb', ROOT_DIR)
|
8
|
+
|
9
|
+
unless File.exist?('config/job_dispatch.yml')
|
10
|
+
$stderr.puts "Configuration file at 'config/job_dispatch.yml' not found."
|
11
|
+
exit 1
|
12
|
+
end
|
13
|
+
|
14
|
+
if ENV['BUNDLE_GEMFILE'] || File.exists('Gemfile')
|
15
|
+
require 'bundler/setup'
|
16
|
+
end
|
17
|
+
|
18
|
+
require 'job_dispatch'
|
19
|
+
|
20
|
+
# boot the rails app so we can access the Rails stored job queue.
|
21
|
+
require File.expand_path('config/boot.rb', ROOT_DIR)
|
22
|
+
|
23
|
+
ENV["RAILS_ENV"] ||= "development"
|
24
|
+
JobDispatch.load_config_from_yml('config/job_dispatch.yml', ENV["RAILS_ENV"])
|
25
|
+
|
26
|
+
# TODO: Find a way to start the dispatcher with only ActiveRecord/Mongoid skipping the rest of rails
|
27
|
+
# as it uses ~100MB RAM.
|
28
|
+
|
29
|
+
require File.expand_path('config/environment.rb', ROOT_DIR)
|
30
|
+
|
31
|
+
JobDispatch.logger = Rails.logger
|
32
|
+
|
33
|
+
broker = JobDispatch::Broker.new(JobDispatch.config.broker[:bind], JobDispatch.config.signaller[:bind])
|
34
|
+
broker.run
|
data/bin/job-status
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This command is expected to be run in the root of a Rails application which has a Job model class
|
4
|
+
# and has an initializer to configure JobDispatch.
|
5
|
+
|
6
|
+
ROOT_DIR = Dir.pwd
|
7
|
+
APP_PATH = File.expand_path('config/application.rb', ROOT_DIR)
|
8
|
+
|
9
|
+
unless File.exist?('config/job_dispatch.yml')
|
10
|
+
$stderr.puts "Configuration file at 'config/job_dispatch.yml' not found."
|
11
|
+
exit 1
|
12
|
+
end
|
13
|
+
|
14
|
+
if ENV['BUNDLE_GEMFILE'] || File.exists('Gemfile')
|
15
|
+
require 'bundler/setup'
|
16
|
+
end
|
17
|
+
|
18
|
+
require 'job_dispatch'
|
19
|
+
|
20
|
+
# boot the rails app so we can access the Rails stored job queue.
|
21
|
+
require File.expand_path('config/boot.rb', ROOT_DIR)
|
22
|
+
|
23
|
+
ENV["RAILS_ENV"] ||= "development"
|
24
|
+
JobDispatch.load_config_from_yml('config/job_dispatch.yml', ENV["RAILS_ENV"])
|
25
|
+
|
26
|
+
$:.unshift(File.join(ROOT_DIR, "lib"))
|
27
|
+
|
28
|
+
|
29
|
+
require 'job_dispatch/status'
|
30
|
+
require 'json'
|
31
|
+
|
32
|
+
repeat = 0
|
33
|
+
|
34
|
+
if ARGV.count > 0
|
35
|
+
repeat = ARGV.first.to_i
|
36
|
+
end
|
37
|
+
|
38
|
+
endpoint = JobDispatch.config.broker[:connect]
|
39
|
+
if endpoint.nil? || endpoint.empty?
|
40
|
+
$stderr.puts "No Job Dispatch broker connect address has been specified."
|
41
|
+
exit 1
|
42
|
+
end
|
43
|
+
|
44
|
+
status = JobDispatch::Status.new(endpoint)
|
45
|
+
status.connect
|
46
|
+
loop do
|
47
|
+
begin
|
48
|
+
Timeout.timeout(5) do
|
49
|
+
status.fetch
|
50
|
+
status.print
|
51
|
+
end
|
52
|
+
|
53
|
+
if repeat > 0
|
54
|
+
sleep(repeat)
|
55
|
+
else
|
56
|
+
break
|
57
|
+
end
|
58
|
+
|
59
|
+
rescue TimeoutError
|
60
|
+
# message may have been sent and we might be waiting for a reply that never comes, so
|
61
|
+
# close socket and reconnect.
|
62
|
+
puts "Job Dispatcher status: No response from broker, reconnecting..."
|
63
|
+
status.disconnect
|
64
|
+
status.connect
|
65
|
+
rescue Interrupt
|
66
|
+
exit 0
|
67
|
+
end
|
68
|
+
end
|
69
|
+
status.disconnect
|
data/bin/job-worker
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This command is expected to be run in the root of a Rails application which has a Job model class
|
4
|
+
# and has an initializer to configure JobDispatch.
|
5
|
+
|
6
|
+
ROOT_DIR = Dir.pwd
|
7
|
+
APP_PATH = File.expand_path('config/application.rb', ROOT_DIR)
|
8
|
+
|
9
|
+
unless File.exist?('config/job_dispatch.yml')
|
10
|
+
$stderr.puts "Configuration file at 'config/job_dispatch.yml' not found."
|
11
|
+
exit 1
|
12
|
+
end
|
13
|
+
|
14
|
+
if ENV['BUNDLE_GEMFILE'] || File.exists('Gemfile')
|
15
|
+
require 'bundler/setup'
|
16
|
+
end
|
17
|
+
|
18
|
+
require 'job_dispatch'
|
19
|
+
|
20
|
+
# boot the rails app so we can access the Rails stored job queue.
|
21
|
+
require File.expand_path('config/boot.rb', ROOT_DIR)
|
22
|
+
|
23
|
+
ENV["RAILS_ENV"] ||= "development"
|
24
|
+
JobDispatch.load_config_from_yml('config/job_dispatch.yml', ENV["RAILS_ENV"])
|
25
|
+
|
26
|
+
# TODO: Find a way to start the dispatcher with only ActiveRecord/Mongoid skipping the rest of rails
|
27
|
+
# as it uses ~100MB RAM.
|
28
|
+
|
29
|
+
require File.expand_path('config/environment.rb', ROOT_DIR)
|
30
|
+
|
31
|
+
JobDispatch.logger = Rails.logger
|
32
|
+
|
33
|
+
endpoint = JobDispatch.config.broker[:connect]
|
34
|
+
if endpoint.nil? || endpoint.empty?
|
35
|
+
$stderr.puts "No Job Dispatch broker connect address has been specified."
|
36
|
+
exit 1
|
37
|
+
end
|
38
|
+
|
39
|
+
worker = JobDispatch::Worker.new(endpoint, JobDispatch.config.worker_options)
|
40
|
+
worker.run
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# JobDispatch compliant job.
|
2
|
+
#
|
3
|
+
class Job
|
4
|
+
|
5
|
+
include Mongoid::Document
|
6
|
+
|
7
|
+
# important: Need this to include the success! and fail! methods.
|
8
|
+
include JobDispatch::Job
|
9
|
+
|
10
|
+
# required fields:
|
11
|
+
field :queue, :type => String, :default => 'default'
|
12
|
+
field :status, :type => Integer, :default => PENDING
|
13
|
+
|
14
|
+
field :parameters, :type => Array # list of parameters for the action
|
15
|
+
field :target, :type => String
|
16
|
+
field :method, :type => String
|
17
|
+
|
18
|
+
field :enqueued_at, :type => Time #, :default => -> { Time.now.to_i }
|
19
|
+
field :scheduled_at, :type => Time, :default => 0
|
20
|
+
field :expire_execution_at, :type => Time
|
21
|
+
field :timeout, :type => Integer
|
22
|
+
field :retry_count, :type => Integer, :default => 0
|
23
|
+
field :retry_delay, :type => Integer, :default => 10
|
24
|
+
field :completed_at, :type => Time
|
25
|
+
|
26
|
+
# If the job completed successfully, this is the result. If it failed,
|
27
|
+
# this is the exception error message
|
28
|
+
field :result, :type => Object
|
29
|
+
|
30
|
+
# any indexes:
|
31
|
+
index({queue: 1, status: 1, scheduled_at: 1})
|
32
|
+
|
33
|
+
# dequeue a job from the database.
|
34
|
+
# This is an atomic operation that also marks the job as being in the pending state.
|
35
|
+
def self.dequeue_job_for_queue(queue, time=nil)
|
36
|
+
time ||= Time.now
|
37
|
+
self.
|
38
|
+
where(:queue => queue, :status => PENDING).
|
39
|
+
where(:scheduled_at.lte => time).
|
40
|
+
sort(:enqueued_at => 1).
|
41
|
+
find_and_modify({"$set" => {:status => IN_PROGRESS}}, new: true)
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'job_dispatch/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "job_dispatch"
|
8
|
+
spec.version = JobDispatch::VERSION
|
9
|
+
spec.authors = ["Matt Connolly"]
|
10
|
+
spec.email = ["matt@mobiledataanywhere.com"]
|
11
|
+
spec.description = %q{Job Dispatch to workers via ZeroMQ}
|
12
|
+
spec.summary = %q{Job Dispatch is a gem for dispatching jobs to workers in an asynchronous manner. Job Dispatch does not require any specific database. Jobs are dispatched using ZeroMQ messages and workers can be implemented in any language.}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_runtime_dependency "rbczmq", "~> 1.7"
|
22
|
+
spec.add_runtime_dependency 'nullobject'
|
23
|
+
spec.add_runtime_dependency 'json'
|
24
|
+
spec.add_runtime_dependency 'text-table', '~> 1.2.3'
|
25
|
+
spec.add_runtime_dependency 'activesupport', '>= 3.0.0'
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
28
|
+
spec.add_development_dependency "rspec", "~> 2.14"
|
29
|
+
spec.add_development_dependency "simplecov", "~> 0.8"
|
30
|
+
spec.add_development_dependency "timecop", "~> 0.7"
|
31
|
+
spec.add_development_dependency "factory_girl", "~> 4.3.0"
|
32
|
+
spec.add_development_dependency "rake"
|
33
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module JobDispatch
|
4
|
+
# A very simple representation of a worker and a message received from or to be sent to that worker.
|
5
|
+
# this represents job messages from the Broker's ROUTER socket perspective. ie: messages are wrapped
|
6
|
+
# by the worker ID so that they can be routed to the client.
|
7
|
+
#
|
8
|
+
# The parameters object is expected to be a Hash with indifferent access
|
9
|
+
class Broker
|
10
|
+
class Command
|
11
|
+
attr_accessor :worker_id
|
12
|
+
attr_accessor :parameters
|
13
|
+
|
14
|
+
def initialize(worker_id=nil, parameters=nil)
|
15
|
+
@worker_id, @parameters = worker_id, parameters
|
16
|
+
end
|
17
|
+
|
18
|
+
def worker_ready?
|
19
|
+
@parameters[:command] == "ready" || @parameters[:ready]
|
20
|
+
end
|
21
|
+
|
22
|
+
def queue
|
23
|
+
(@parameters[:queue] || :default).to_sym
|
24
|
+
end
|
25
|
+
|
26
|
+
def command
|
27
|
+
@parameters[:command]
|
28
|
+
end
|
29
|
+
|
30
|
+
def status
|
31
|
+
@parameters[:status] && @parameters[:status].to_sym
|
32
|
+
end
|
33
|
+
|
34
|
+
def success?
|
35
|
+
status == :success
|
36
|
+
end
|
37
|
+
|
38
|
+
# the name of the worker
|
39
|
+
def worker_name
|
40
|
+
@parameters[:worker_name]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module JobDispatch
|
2
|
+
class Broker
|
3
|
+
|
4
|
+
# a class to represent a worker processing a broker command that is not a job. It is still tracked by
|
5
|
+
# the broker as if it was a job, though. If the worker does not reply, they will be timed out. And
|
6
|
+
# status report will show the worker state as executing this command.
|
7
|
+
class InternalJob
|
8
|
+
attr :id, :command, :queue
|
9
|
+
|
10
|
+
def initialize(command, queue)
|
11
|
+
@id = SecureRandom.uuid
|
12
|
+
@command = command
|
13
|
+
@timeout_at = Time.now + JobDispatch::Job::DEFAULT_EXECUTION_TIMEOUT
|
14
|
+
@queue = queue
|
15
|
+
end
|
16
|
+
|
17
|
+
def timed_out?
|
18
|
+
Time.now > @timeout_at
|
19
|
+
end
|
20
|
+
|
21
|
+
def as_json
|
22
|
+
{
|
23
|
+
command: command,
|
24
|
+
id: id,
|
25
|
+
queue: queue,
|
26
|
+
target: "JobDispatch",
|
27
|
+
method: command
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module JobDispatch
|
2
|
+
class Broker
|
3
|
+
#
|
4
|
+
# This class represents a ZMQ socket where workers can phone in and ask for work to do.
|
5
|
+
# If there is no work to do, an idle command is returned
|
6
|
+
#
|
7
|
+
# The response will be a single JSON encoded message such as:
|
8
|
+
#
|
9
|
+
# { "command":"idle" }
|
10
|
+
#
|
11
|
+
# or
|
12
|
+
#
|
13
|
+
# { "target":"MyMailer", "method":"deliver", "parameters":[14, "Subject", "Some other parameter"]}
|
14
|
+
#
|
15
|
+
# In which the worker should execute the code `MyMailer.deliver(14, 'Subject', 'Some other parameter')`
|
16
|
+
#
|
17
|
+
#
|
18
|
+
# Other commands can be sent to the broker, such as:
|
19
|
+
#
|
20
|
+
# { "command":"status" } => returns status of the broker, number of idle workers per queue, etc.
|
21
|
+
#
|
22
|
+
class Socket
|
23
|
+
attr_reader :socket
|
24
|
+
|
25
|
+
def initialize(bind_address)
|
26
|
+
@bind_address = bind_address
|
27
|
+
@socket = nil
|
28
|
+
@queues = {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def connect
|
32
|
+
unless @socket
|
33
|
+
@socket = JobDispatch.context.socket(ZMQ::ROUTER)
|
34
|
+
@socket.bind(@bind_address)
|
35
|
+
end
|
36
|
+
@socket
|
37
|
+
end
|
38
|
+
|
39
|
+
def disconnect
|
40
|
+
if @socket
|
41
|
+
@socket.close
|
42
|
+
@socket = nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def poll_item
|
47
|
+
@poll_item ||= ZMQ::Pollitem(@socket, ZMQ::POLLIN)
|
48
|
+
end
|
49
|
+
|
50
|
+
def readable?
|
51
|
+
poller = ZMQ::Poller.new
|
52
|
+
poller.register(poll_item)
|
53
|
+
poller.poll(0) > 0
|
54
|
+
end
|
55
|
+
|
56
|
+
# Process a message received on the broker socket. The returned Command object contains the identity of the
|
57
|
+
# requester (worker) so that replies can be sent to them.
|
58
|
+
#
|
59
|
+
# @return [Command]
|
60
|
+
def read_command
|
61
|
+
message = socket.recv_message #=> ZMQ::Message
|
62
|
+
worker_id = Identity.new(message.unwrap.data.to_s.to_sym)
|
63
|
+
json = message.first.data
|
64
|
+
parameters = begin
|
65
|
+
JSON.parse(json).with_indifferent_access
|
66
|
+
rescue
|
67
|
+
JobDispatch.logger.error("Received invalid json data: #{json.inspect} from socket id '#{worker_id}'")
|
68
|
+
{error: "Invalid JSON"}
|
69
|
+
end
|
70
|
+
Command.new(worker_id, parameters)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Send a command message to a worker.
|
74
|
+
# @param [Command command] the command to send to the worker.
|
75
|
+
def send_command(command)
|
76
|
+
message = ZMQ::Message.new
|
77
|
+
message.addstr(JSON.dump(command.parameters))
|
78
|
+
message.wrap(ZMQ::Frame(command.worker_id.to_s))
|
79
|
+
socket.send_message message
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|