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
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
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .idea
19
+
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,13 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.1
4
+ - 2.0.0
5
+ - 1.9.3
6
+ - rbx
7
+ gemfile:
8
+ - Gemfile
9
+ matrix:
10
+ allow_failures:
11
+ - rvm: rbx
12
+ env: RUBYOPT="-Ku"
13
+
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
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ task :default => [:spec]
5
+
6
+ desc "Run the specs."
7
+ RSpec::Core::RakeTask.new do |t|
8
+ t.pattern = "spec/**/*_spec.rb"
9
+ end
10
+
@@ -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
+