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.
- 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,54 @@
|
|
1
|
+
require 'active_support/core_ext'
|
2
|
+
require 'active_support/core_ext/object/json'
|
3
|
+
|
4
|
+
module JobDispatch
|
5
|
+
# Identity encapsulates a ZeroMQ socket identity, which is a string of binary characters, typically
|
6
|
+
# containing nulls or non-utf8 compatible characters in ASCII-8BIT encoding.
|
7
|
+
class Identity
|
8
|
+
|
9
|
+
include Comparable
|
10
|
+
|
11
|
+
attr_reader :identity
|
12
|
+
|
13
|
+
def initialize(identity)
|
14
|
+
@identity = identity.to_sym
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
@identity.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_str
|
22
|
+
@identity.to_str
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_hex
|
26
|
+
@identity.to_s.bytes.map { |x| '%02x' % x }.join
|
27
|
+
end
|
28
|
+
|
29
|
+
def as_json(options={})
|
30
|
+
to_hex.as_json(options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_sym
|
34
|
+
@identity
|
35
|
+
end
|
36
|
+
|
37
|
+
def hash
|
38
|
+
@identity.hash
|
39
|
+
end
|
40
|
+
|
41
|
+
def ==(other)
|
42
|
+
@identity == other.identity
|
43
|
+
end
|
44
|
+
|
45
|
+
def eql?(other)
|
46
|
+
self.class == other.class && @identity == other.identity
|
47
|
+
end
|
48
|
+
|
49
|
+
def <=>(other)
|
50
|
+
@identity <=> other.identity
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module JobDispatch
|
2
|
+
module Job
|
3
|
+
|
4
|
+
DEFAULT_EXECUTION_TIMEOUT = 30
|
5
|
+
|
6
|
+
PENDING = 0
|
7
|
+
IN_PROGRESS = 1
|
8
|
+
COMPLETED = 2
|
9
|
+
FAILED = 3
|
10
|
+
|
11
|
+
STATUS_STRINGS = {
|
12
|
+
PENDING => 'pending',
|
13
|
+
IN_PROGRESS => 'in progress',
|
14
|
+
COMPLETED => 'completed',
|
15
|
+
FAILED => 'failed'
|
16
|
+
}
|
17
|
+
|
18
|
+
def timed_out?
|
19
|
+
expire_execution_at < Time.now
|
20
|
+
end
|
21
|
+
|
22
|
+
def failed!(results)
|
23
|
+
# update database
|
24
|
+
self.completed_at = Time.now
|
25
|
+
self.result = results
|
26
|
+
if retry_count && retry_count > 0 && retry_delay && retry_delay > 0
|
27
|
+
self.retry_count -= 1
|
28
|
+
self.scheduled_at = Time.now + retry_delay.seconds
|
29
|
+
self.retry_delay *= 2
|
30
|
+
self.status = PENDING
|
31
|
+
else
|
32
|
+
self.status = FAILED
|
33
|
+
end
|
34
|
+
save!
|
35
|
+
end
|
36
|
+
|
37
|
+
def succeeded!(results)
|
38
|
+
self.status = COMPLETED
|
39
|
+
self.result = results
|
40
|
+
self.completed_at = Time.now
|
41
|
+
save!
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module JobDispatch
|
2
|
+
|
3
|
+
# This class represents a ZeroMQ socket for signalling to the broker that there are jobs immediately available.
|
4
|
+
class Signaller
|
5
|
+
attr :socket
|
6
|
+
|
7
|
+
def initialize(wakeup_connect_address)
|
8
|
+
@wakeup_connect_address = wakeup_connect_address
|
9
|
+
end
|
10
|
+
|
11
|
+
def connect
|
12
|
+
if @socket.nil?
|
13
|
+
@socket = JobDispatch.context.socket(ZMQ::PUB)
|
14
|
+
@socket.connect(@wakeup_connect_address)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def disconnect
|
19
|
+
if @socket
|
20
|
+
@socket.close
|
21
|
+
@socket = nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# signals are a straight
|
26
|
+
def signal(queue='default')
|
27
|
+
@socket.send(queue)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module JobDispatch::Sockets
|
2
|
+
class Enqueue
|
3
|
+
def initialize(bind_address)
|
4
|
+
@socket = JobDispatch.context.socket(ZMQ::REQ)
|
5
|
+
@socket.bind(bind_address)
|
6
|
+
end
|
7
|
+
|
8
|
+
def poll_item
|
9
|
+
@poll_item ||= ZMQ::Pollitem(@socket, ZMQ::POLLIN)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Enqueue socket when it receives a message simply stores it in the database.
|
13
|
+
# It will also send a message to wake a connected dispatcher
|
14
|
+
def process
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'text-table'
|
2
|
+
|
3
|
+
module JobDispatch
|
4
|
+
class Status
|
5
|
+
|
6
|
+
attr :socket
|
7
|
+
|
8
|
+
|
9
|
+
def initialize(connect_address)
|
10
|
+
@connect_address = connect_address
|
11
|
+
end
|
12
|
+
|
13
|
+
def connect
|
14
|
+
if @socket.nil?
|
15
|
+
@socket = JobDispatch.context.socket(ZMQ::REQ)
|
16
|
+
@socket.connect(@connect_address)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def disconnect
|
21
|
+
@socket.close
|
22
|
+
@socket = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def fetch
|
26
|
+
@socket.send(JSON.dump({command:'status'}))
|
27
|
+
json = @socket.recv
|
28
|
+
@status = JSON.parse(json).with_indifferent_access
|
29
|
+
@time = Time.now
|
30
|
+
end
|
31
|
+
|
32
|
+
def print
|
33
|
+
puts "Job Dispatcher status: #{@status[:status]} at #{@time}"
|
34
|
+
puts ""
|
35
|
+
|
36
|
+
table = Text::Table.new
|
37
|
+
table.head = ['Queue', 'Worker ID', 'Worker Name', 'Status', 'Job ID', 'Job Details']
|
38
|
+
table.rows = []
|
39
|
+
|
40
|
+
@status[:queues].each do |queue, workers|
|
41
|
+
if workers.empty?
|
42
|
+
table.rows << [
|
43
|
+
queue,
|
44
|
+
'- no workers -',
|
45
|
+
'',
|
46
|
+
'',
|
47
|
+
'',
|
48
|
+
'',
|
49
|
+
]
|
50
|
+
else
|
51
|
+
workers.each_pair do |worker_id, worker_status|
|
52
|
+
|
53
|
+
job = worker_status[:job]
|
54
|
+
if job
|
55
|
+
params_str = if job[:parameters]
|
56
|
+
job[:parameters].map(&:inspect).join(',')[0..20]
|
57
|
+
else
|
58
|
+
''
|
59
|
+
end
|
60
|
+
job_details = "#{job[:target]}.#{job[:method]}(#{params_str})"
|
61
|
+
end
|
62
|
+
|
63
|
+
table.rows << [
|
64
|
+
queue,
|
65
|
+
worker_id,
|
66
|
+
worker_status[:name],
|
67
|
+
worker_status[:status],
|
68
|
+
worker_status[:job_id],
|
69
|
+
job_details,
|
70
|
+
]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
puts table.to_s
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'active_support/dependencies'
|
4
|
+
|
5
|
+
module JobDispatch
|
6
|
+
class Worker
|
7
|
+
|
8
|
+
#
|
9
|
+
# This represents a unit of work to be done. It will be serialised to Mongo database
|
10
|
+
#
|
11
|
+
class Item
|
12
|
+
attr_accessor :job_id
|
13
|
+
attr :target
|
14
|
+
attr :method
|
15
|
+
attr :params
|
16
|
+
attr :result
|
17
|
+
attr :status
|
18
|
+
|
19
|
+
def initialize(target, method, *params)
|
20
|
+
@target, @method, @params = target, method, params
|
21
|
+
end
|
22
|
+
|
23
|
+
# execute the method on the target with the given parameters
|
24
|
+
# This will capture standard exceptions for return over network.
|
25
|
+
def execute
|
26
|
+
begin
|
27
|
+
JobDispatch.logger.info "Worker executing job #{job_id}: #{target}.#{method}"
|
28
|
+
Thread.current["JobDispatch::Worker.job_id"] = job_id
|
29
|
+
@klass = target.constantize
|
30
|
+
@result = @klass.__send__(method.to_sym, *params)
|
31
|
+
@status = :success
|
32
|
+
rescue StandardError => ex
|
33
|
+
@result = ex
|
34
|
+
@status = :error
|
35
|
+
ensure
|
36
|
+
Thread.current["JobDispatch::Worker.job_id"] = nil
|
37
|
+
JobDispatch.logger.info "Worker completed job #{job_id}: #{target}.#{method}, status: #{@status}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module JobDispatch
|
6
|
+
|
7
|
+
class Worker
|
8
|
+
class Socket
|
9
|
+
|
10
|
+
attr :socket
|
11
|
+
attr :item_class
|
12
|
+
|
13
|
+
def initialize(connect_address, item_klass)
|
14
|
+
@socket = JobDispatch.context.socket(ZMQ::REQ)
|
15
|
+
@socket.connect(connect_address)
|
16
|
+
@item_class = item_klass
|
17
|
+
end
|
18
|
+
|
19
|
+
def poll_item
|
20
|
+
@poll_item ||= ZMQ::Pollitem(@socket, ZMQ::POLLIN)
|
21
|
+
end
|
22
|
+
|
23
|
+
def ask_for_work(queue)
|
24
|
+
@socket.send(JSON.dump({command: 'ready', queue: queue, worker_name: identity}))
|
25
|
+
end
|
26
|
+
|
27
|
+
def send_goodbye(queue)
|
28
|
+
@socket.send(JSON.dump({command: 'goodbye', worker_name: identity}))
|
29
|
+
end
|
30
|
+
|
31
|
+
def close
|
32
|
+
@socket.close
|
33
|
+
end
|
34
|
+
|
35
|
+
def identity
|
36
|
+
@identity ||= begin
|
37
|
+
hostname = ::Socket.gethostname
|
38
|
+
process = Process.pid
|
39
|
+
thread = Thread.current.object_id.to_s(16)
|
40
|
+
['ruby', hostname, process, thread].join(':')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# read an incoming message. The thread will block if there is no readable message.
|
45
|
+
#
|
46
|
+
# @return [JobDispatch::Item] the item to be processed (or nil if there isn't a valid job)
|
47
|
+
def read_item
|
48
|
+
json = @socket.recv
|
49
|
+
begin
|
50
|
+
params = JSON.parse(json)
|
51
|
+
case params["command"]
|
52
|
+
when "job"
|
53
|
+
item = item_class.new params["target"], params["method"], *params["parameters"]
|
54
|
+
when "idle"
|
55
|
+
item = item_class.new "JobDispatch", "idle"
|
56
|
+
when "quit"
|
57
|
+
puts "It's quittin' time!"
|
58
|
+
Process.exit(0)
|
59
|
+
else
|
60
|
+
item = item_class.new "JobDispatch", "unknown_command", params
|
61
|
+
end
|
62
|
+
item.job_id = params["job_id"]
|
63
|
+
rescue StandardError => e
|
64
|
+
JobDispatch.logger.error "Failed to read message from worker socket: #{e}"
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
item
|
68
|
+
end
|
69
|
+
|
70
|
+
# after execution, send the response.
|
71
|
+
def send_response(job_id, status, result)
|
72
|
+
JobDispatch.logger.info "Worker #{Process.pid} completed job_id: #{job_id}: #{status}, result: #{result}"
|
73
|
+
response = {
|
74
|
+
command: 'completed',
|
75
|
+
ready: true,
|
76
|
+
job_id: job_id,
|
77
|
+
result: result,
|
78
|
+
status: status
|
79
|
+
}
|
80
|
+
@socket.send(JSON.dump(response))
|
81
|
+
end
|
82
|
+
|
83
|
+
def send_touch(job_id, timeout=nil)
|
84
|
+
hash = {
|
85
|
+
command: 'touch',
|
86
|
+
job_id: job_id
|
87
|
+
}
|
88
|
+
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}"}
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module JobDispatch
|
4
|
+
|
5
|
+
#
|
6
|
+
# This class is the main worker loop. Run it as a whole process or just as a thread in a multi-threaded worker
|
7
|
+
# process.
|
8
|
+
#
|
9
|
+
class Worker
|
10
|
+
|
11
|
+
IDLE_TIME = 3
|
12
|
+
IDLE_COUNT = 10
|
13
|
+
|
14
|
+
attr :socket
|
15
|
+
attr :queue
|
16
|
+
attr :item_class
|
17
|
+
|
18
|
+
def initialize(connect_address, options={})
|
19
|
+
options ||= {}
|
20
|
+
@connect_address = connect_address
|
21
|
+
@queue = options[:queue] || 'default'
|
22
|
+
@running = false
|
23
|
+
@item_class = options[:item_class] || Worker::Item
|
24
|
+
end
|
25
|
+
|
26
|
+
def connect
|
27
|
+
@socket ||= Worker::Socket.new(@connect_address, item_class)
|
28
|
+
Thread.current["JobDispatch::Worker.socket"] = @socket
|
29
|
+
end
|
30
|
+
|
31
|
+
def disconnect
|
32
|
+
if @socket
|
33
|
+
@socket.close
|
34
|
+
@socket = nil
|
35
|
+
Thread.current["JobDispatch::Worker.socket"] = nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def run
|
40
|
+
@running = true
|
41
|
+
while running?
|
42
|
+
puts "connecting"
|
43
|
+
connect
|
44
|
+
puts "asking for work"
|
45
|
+
ask_for_work
|
46
|
+
|
47
|
+
# if we are idle for too many times, the broker has restarted or gone away, and we will be stuck in receive
|
48
|
+
# state, so we need to close the socket and make a new one to ask for work again.
|
49
|
+
|
50
|
+
idle_count = 0
|
51
|
+
poller = ZMQ::Poller.new
|
52
|
+
poller.register(socket.poll_item)
|
53
|
+
while running? and idle_count < IDLE_COUNT
|
54
|
+
begin
|
55
|
+
poller.poll(IDLE_TIME)
|
56
|
+
if poller.readables.include?(socket.socket)
|
57
|
+
process
|
58
|
+
idle_count = 0
|
59
|
+
else
|
60
|
+
idle
|
61
|
+
idle_count += 1
|
62
|
+
end
|
63
|
+
rescue Interrupt
|
64
|
+
puts "Worker stopping."
|
65
|
+
JobDispatch.logger.info("Worker #{}")
|
66
|
+
stop
|
67
|
+
disconnect
|
68
|
+
connect
|
69
|
+
send_goodbye
|
70
|
+
end
|
71
|
+
end
|
72
|
+
disconnect
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def ask_for_work
|
77
|
+
socket.ask_for_work(queue)
|
78
|
+
end
|
79
|
+
|
80
|
+
def send_goodbye
|
81
|
+
socket.send_goodbye(queue)
|
82
|
+
end
|
83
|
+
|
84
|
+
def running?
|
85
|
+
@running
|
86
|
+
end
|
87
|
+
|
88
|
+
def stop
|
89
|
+
@running = false
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.touch(timeout=nil)
|
93
|
+
sock = Thread.current["JobDispatch::Worker.socket"]
|
94
|
+
job_id = Thread.current["JobDispatch::Worker.job_id"]
|
95
|
+
if sock && job_id
|
96
|
+
sock.send_touch(job_id, timeout)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
# called when the socket is readable. do some work.
|
103
|
+
def process
|
104
|
+
item = @socket.read_item
|
105
|
+
if item
|
106
|
+
item.execute
|
107
|
+
@socket.send_response(item.job_id, item.status, item.result)
|
108
|
+
else
|
109
|
+
@socket.send_response("unknown", :error, "failed to decode command")
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def idle
|
114
|
+
puts "waiting for job to do…"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
require 'job_dispatch/worker/socket'
|
120
|
+
require 'job_dispatch/worker/item'
|
data/lib/job_dispatch.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require "job_dispatch/version"
|
4
|
+
|
5
|
+
require 'active_support/dependencies/autoload'
|
6
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
7
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
8
|
+
require 'nullobject'
|
9
|
+
require 'rbczmq'
|
10
|
+
|
11
|
+
module JobDispatch
|
12
|
+
|
13
|
+
extend ActiveSupport::Autoload
|
14
|
+
|
15
|
+
autoload :Broker
|
16
|
+
autoload :Client
|
17
|
+
autoload :Configuration
|
18
|
+
autoload :Identity
|
19
|
+
autoload :Job
|
20
|
+
autoload :Signaller
|
21
|
+
autoload :Status
|
22
|
+
autoload :Worker
|
23
|
+
|
24
|
+
def configure(&block)
|
25
|
+
Configuration.configure(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def config
|
29
|
+
Configuration.config
|
30
|
+
end
|
31
|
+
|
32
|
+
def load_config_from_yml(filename='config/job_dispatch.yml', environment="default")
|
33
|
+
require 'yaml'
|
34
|
+
_config = YAML.load_file(filename).with_indifferent_access
|
35
|
+
_config = _config[environment] || _config[:default]
|
36
|
+
load_config(_config)
|
37
|
+
end
|
38
|
+
|
39
|
+
def load_config(hash)
|
40
|
+
configure do |c|
|
41
|
+
hash.each_pair do |key, value|
|
42
|
+
c[key] = value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [ZMQ::Context] return or create a ZeroMQ context.
|
48
|
+
def context
|
49
|
+
ZMQ.context || ZMQ::Context.new
|
50
|
+
end
|
51
|
+
|
52
|
+
def idle
|
53
|
+
"idle, doing nothing"
|
54
|
+
end
|
55
|
+
|
56
|
+
def unknown_command(params)
|
57
|
+
puts "Unknown command: #{params.inspect}"
|
58
|
+
end
|
59
|
+
|
60
|
+
# This signals to the job broker(s) that there are jobs immediately available on the given queue.
|
61
|
+
def signal(queue='default')
|
62
|
+
self.signaller ||= if config.signaller && config.signaller[:connect]
|
63
|
+
signaller = JobDispatch::Signaller.new(config.signaller[:connect])
|
64
|
+
signaller.connect
|
65
|
+
signaller
|
66
|
+
else
|
67
|
+
Null::Object.instance
|
68
|
+
end
|
69
|
+
self.signaller.signal(queue)
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
def enqueue(job_attrs)
|
74
|
+
address = JobDispatch.config.broker[:connect]
|
75
|
+
socket = JobDispatch.context.socket(ZMQ::REQ)
|
76
|
+
socket.connect(address)
|
77
|
+
socket.send(JSON.dump({command:'enqueue',job:job_attrs}))
|
78
|
+
result = JSON.parse(socket.recv)
|
79
|
+
socket.close
|
80
|
+
result
|
81
|
+
end
|
82
|
+
|
83
|
+
module_function :context
|
84
|
+
module_function :idle
|
85
|
+
module_function :unknown_command
|
86
|
+
module_function :signal
|
87
|
+
module_function :configure
|
88
|
+
module_function :config
|
89
|
+
module_function :enqueue
|
90
|
+
module_function :load_config
|
91
|
+
module_function :load_config_from_yml
|
92
|
+
|
93
|
+
mattr_accessor :logger
|
94
|
+
mattr_accessor :signaller
|
95
|
+
end
|
96
|
+
|
97
|
+
JobDispatch.logger = Null::Object.instance
|
@@ -0,0 +1,19 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
factory :job do
|
3
|
+
id { SecureRandom.uuid }
|
4
|
+
queue :default
|
5
|
+
status JobDispatch::Job::PENDING
|
6
|
+
parameters []
|
7
|
+
target "SecureRandom"
|
8
|
+
add_attribute(:method) { "uuid" }
|
9
|
+
|
10
|
+
enqueued_at { Time.now }
|
11
|
+
scheduled_at { Time.at(0) }
|
12
|
+
expire_execution_at { nil}
|
13
|
+
timeout 10
|
14
|
+
retry_count 0
|
15
|
+
retry_delay 20
|
16
|
+
completed_at nil
|
17
|
+
result nil
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe JobDispatch::Broker::Socket do
|
4
|
+
|
5
|
+
subject { JobDispatch::Broker::Socket.new('tcp://localhost:1999') }
|
6
|
+
|
7
|
+
context "Reading messages from a worker" do
|
8
|
+
before :each do
|
9
|
+
@socket = double('Socket')
|
10
|
+
subject.stub(:socket => @socket)
|
11
|
+
end
|
12
|
+
|
13
|
+
context "with a valid message" do
|
14
|
+
before :each do
|
15
|
+
message = ZMQ::Message.new
|
16
|
+
message.addstr(JSON.dump({command: 'ready', queue: 'my_queue'}))
|
17
|
+
message.wrap(ZMQ::Frame('my_worker_id'))
|
18
|
+
@socket.stub(:recv_message => message)
|
19
|
+
@command = subject.read_command
|
20
|
+
end
|
21
|
+
|
22
|
+
it "returns a Command" do
|
23
|
+
expect(@command).to be_a(JobDispatch::Broker::Command)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "reads the command" do
|
27
|
+
expect(@command.parameters[:command]).to eq('ready')
|
28
|
+
end
|
29
|
+
|
30
|
+
it "reads the worker id" do
|
31
|
+
expect(@command.worker_id.to_sym).to eq(:my_worker_id)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "with an invalid message" do
|
36
|
+
before :each do
|
37
|
+
message = ZMQ::Message.new
|
38
|
+
message.addstr("Hello")
|
39
|
+
message.wrap(ZMQ::Frame('my_worker_id'))
|
40
|
+
@socket.stub(:recv_message => message)
|
41
|
+
@command = subject.read_command
|
42
|
+
end
|
43
|
+
|
44
|
+
it "returns a Command" do
|
45
|
+
expect(@command).to be_a(JobDispatch::Broker::Command)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "reads the worker id" do
|
49
|
+
expect(@command.worker_id.to_sym).to eq(:my_worker_id)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|