disque_jockey 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 +7 -0
- data/.rspec +1 -0
- data/Gemfile +3 -0
- data/README.md +66 -0
- data/Rakefile +1 -0
- data/bin/disque_jockey +4 -0
- data/disque_jockey.gemspec +25 -0
- data/lib/disque_jockey/broker.rb +22 -0
- data/lib/disque_jockey/configuration.rb +29 -0
- data/lib/disque_jockey/logger.rb +83 -0
- data/lib/disque_jockey/supervisor.rb +84 -0
- data/lib/disque_jockey/version.rb +3 -0
- data/lib/disque_jockey/worker.rb +45 -0
- data/lib/disque_jockey/worker_group.rb +108 -0
- data/lib/disque_jockey.rb +26 -0
- data/spec/disque_jockey/broker_spec.rb +51 -0
- data/spec/disque_jockey/configuration_spec.rb +19 -0
- data/spec/disque_jockey/disque_jockey_spec.rb +24 -0
- data/spec/disque_jockey/logger_spec.rb +10 -0
- data/spec/disque_jockey/supervisor_spec.rb +41 -0
- data/spec/disque_jockey/worker_group_spec.rb +58 -0
- data/spec/disque_jockey/worker_shared_setup.rb +25 -0
- data/spec/disque_jockey/worker_spec.rb +57 -0
- data/spec/fixtures/workers/fixture_worker.rb +1 -0
- data/spec/log/.keep +0 -0
- data/spec/spec_helper.rb +2 -0
- metadata +158 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ec24b527a949eebcfd10f84df9ea4c15224442de
|
4
|
+
data.tar.gz: bf38d59c5f6b95b619171f8fd64d74129d253470
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 45bcdbf55a10a3420285c16d8a9007c4b7b7d95a956780e1ba6db9d98524a2877cee3a1a2b82aad7aec0056258315659f35b4f5a3aa55fffb3d60139bfa3805f
|
7
|
+
data.tar.gz: 74e2838237a2a8793d7dffb0f09839ce72a10feffd618bce9f797edefc040e94184f28112bc457c87071647380a6ca09690ca2f9451d4dfd999ebd72cd976658
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# DisqueJockey
|
2
|
+
DisqueJockey is a fast, concurrent background job processing framework for the Disque message queue.
|
3
|
+
# Getting Started
|
4
|
+
First, you should run a Disque server if you aren't already doing so.
|
5
|
+
Disque source and build instructions can be found at: https://github.com/antirez/disque
|
6
|
+
|
7
|
+
Once Disque is set up:
|
8
|
+
````
|
9
|
+
git clone git@github.com:DevinRiley/disque_jockey.git
|
10
|
+
````
|
11
|
+
|
12
|
+
cd into the project directory
|
13
|
+
Install the gem dependencies with bundler
|
14
|
+
````
|
15
|
+
bundle install
|
16
|
+
````
|
17
|
+
|
18
|
+
to build the gem:
|
19
|
+
````
|
20
|
+
rake build
|
21
|
+
````
|
22
|
+
|
23
|
+
to install the gem on your machine:
|
24
|
+
````
|
25
|
+
gem install pkg/disque_jockey-0.0.1.gem
|
26
|
+
````
|
27
|
+
|
28
|
+
Now you're ready to use disque_jockey!
|
29
|
+
|
30
|
+
## Writing your first worker
|
31
|
+
DisqueJockey provides a framework for creating background task workers. Workers subscribe to a disque queue and are given jobs from the queue.
|
32
|
+
|
33
|
+
Your worker should inherit from the DisqueJockey::Worker class
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
require 'disque_jockey'
|
37
|
+
class ExampleWorker < DisqueJockey::Worker
|
38
|
+
subscribe_to 'example-queue'
|
39
|
+
def handle(job)
|
40
|
+
logger.info("Peforming job: #{job}")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
```
|
44
|
+
Your worker class must do two things:
|
45
|
+
- call the subscribe_to method with the name of a queue
|
46
|
+
- implement a handle method, which will take a job as its argument. Jobs from Disque are strings.
|
47
|
+
|
48
|
+
|
49
|
+
Lastly, you must place your worker in a directory named 'workers'
|
50
|
+
|
51
|
+
Once your worker is written and placed in a workers directory, you can call disque_jockey from the command line and it will start up your workers and begin delivering jobs to them.
|
52
|
+
|
53
|
+
````
|
54
|
+
disque_jockey
|
55
|
+
````
|
56
|
+
|
57
|
+
Messages successfully handled by a worker (ie no exceptions raised from the handle method) will be acknowledged and removed from the queue.
|
58
|
+
|
59
|
+
##Roadmap:
|
60
|
+
DisqueJockey is not a currently a production-ready system, and there are a number of goals for it that have not been met yet.
|
61
|
+
Here is a list of functionality I'd like to add to DisqueJockey in the near future:
|
62
|
+
- Allow workers to set auto-acknowledge or fast-acknowledge of messages.
|
63
|
+
- Better test coverage around worker groups
|
64
|
+
- Command line options (e.g. environment)
|
65
|
+
- Rails integration (ActiveJob Adapter)
|
66
|
+
- More use cases in the README (e.g. how to use alongside Rails)
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/disque_jockey
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'disque_jockey/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = 'disque_jockey'
|
8
|
+
gem.version = DisqueJockey::VERSION
|
9
|
+
gem.authors = ['Devin Riley']
|
10
|
+
gem.email = ['devinriley84+disque_jockey@gmail.com']
|
11
|
+
gem.license = "MIT"
|
12
|
+
gem.description = "A framework for managing and running ruby background workers."
|
13
|
+
gem.summary = "A background job framework."
|
14
|
+
gem.homepage = 'https://github.com/DevinRiley/disque_jockey'
|
15
|
+
|
16
|
+
gem.files = `git ls-files`.split($/)
|
17
|
+
gem.executables = ['disque_jockey']
|
18
|
+
gem.test_files = gem.files.grep(%r{^(spec)/})
|
19
|
+
gem.require_paths = ['lib']
|
20
|
+
gem.add_runtime_dependency 'disque'
|
21
|
+
gem.add_runtime_dependency 'logging'
|
22
|
+
gem.add_development_dependency('rspec', '~> 3.1', '>= 3.0')
|
23
|
+
gem.add_development_dependency('rake')
|
24
|
+
gem.add_development_dependency('pry')
|
25
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'disque'
|
2
|
+
module DisqueJockey
|
3
|
+
class Broker
|
4
|
+
|
5
|
+
def initialize(nodes = ["127.0.0.1:7711"], *args)
|
6
|
+
@client = Disque.new(nodes, *args)
|
7
|
+
end
|
8
|
+
|
9
|
+
def fetch_message_from(queue)
|
10
|
+
# fetch returns an array of jobs, but we just want the first one
|
11
|
+
@client.fetch(from: [queue]).first
|
12
|
+
end
|
13
|
+
|
14
|
+
def acknowledge(job_id)
|
15
|
+
response = @client.call('ACKJOB', job_id)
|
16
|
+
# If there is an error acking the job the Disque client
|
17
|
+
# *returns* an error object but doesn't raise it,
|
18
|
+
# so we raise it here ourselves.
|
19
|
+
response.is_a?(RuntimeError) ? raise(response) : true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module DisqueJockey
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :logger, :worker_groups, :log_path, :env
|
4
|
+
def initialize
|
5
|
+
# set defaults
|
6
|
+
@worker_groups = worker_groups_for_environment
|
7
|
+
@log_path = log_path_for_environment
|
8
|
+
end
|
9
|
+
|
10
|
+
def env
|
11
|
+
@env ||= ENV['DISQUE_JOCKEY_ENV'] || 'development'
|
12
|
+
end
|
13
|
+
|
14
|
+
def daemonize?
|
15
|
+
env != 'development'
|
16
|
+
end
|
17
|
+
|
18
|
+
def log_path_for_environment
|
19
|
+
env == 'test' ? 'spec/log' : 'log'
|
20
|
+
end
|
21
|
+
|
22
|
+
# TODO: just read this from a config file
|
23
|
+
def worker_groups_for_environment
|
24
|
+
env == 'development' ? 2 : 4
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'logging'
|
2
|
+
|
3
|
+
module DisqueJockey
|
4
|
+
class Logger
|
5
|
+
|
6
|
+
def initialize(klass)
|
7
|
+
init_color_scheme
|
8
|
+
@logger = Logging.logger[klass]
|
9
|
+
@logger.add_appenders(*log_appenders)
|
10
|
+
@logger.level = :info
|
11
|
+
end
|
12
|
+
|
13
|
+
def logger
|
14
|
+
@logger
|
15
|
+
end
|
16
|
+
|
17
|
+
# logging levels
|
18
|
+
def fatal(message)
|
19
|
+
@logger.fatal(message)
|
20
|
+
end
|
21
|
+
|
22
|
+
def error(message)
|
23
|
+
@logger.error(message)
|
24
|
+
end
|
25
|
+
|
26
|
+
def warn(message)
|
27
|
+
@logger.warn(message)
|
28
|
+
end
|
29
|
+
|
30
|
+
def info(message)
|
31
|
+
@logger.info(message)
|
32
|
+
end
|
33
|
+
|
34
|
+
def debug(message)
|
35
|
+
@logger.debug(message)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def log_appenders
|
41
|
+
appenders = []
|
42
|
+
appenders << file_appender
|
43
|
+
appenders << stdout_appender if DisqueJockey.configuration.env == 'development'
|
44
|
+
return appenders
|
45
|
+
end
|
46
|
+
|
47
|
+
def file_appender
|
48
|
+
begin
|
49
|
+
Logging.appenders.file("#{DisqueJockey.configuration.log_path}/#{DisqueJockey.configuration.env}.log",
|
50
|
+
{ layout: Logging.layouts.pattern(log_pattern) })
|
51
|
+
rescue
|
52
|
+
raise "You must provide a valid log path and log file! DisqueJockey by default will log to the current directory /log/environment.log. Make sure that directory exists and the file is writeable!. Configure DisqueJockey's log path before running if you'd like to specify a custom path"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def stdout_appender
|
57
|
+
# only add colors to the STDOUT appender to prevent color codes
|
58
|
+
# from getting into the log files and potentially impacting commands
|
59
|
+
# like 'less' and 'more'
|
60
|
+
Logging.appenders.stdout({ layout: Logging.layouts.pattern(log_pattern.merge(color_scheme: 'bright')) })
|
61
|
+
end
|
62
|
+
|
63
|
+
def log_pattern
|
64
|
+
{ pattern: '[%d] %-5l %c: %m\n' }
|
65
|
+
end
|
66
|
+
|
67
|
+
def init_color_scheme
|
68
|
+
Logging.color_scheme('bright',
|
69
|
+
:levels => {
|
70
|
+
:info => :green,
|
71
|
+
:warn => :yellow,
|
72
|
+
:error => :red,
|
73
|
+
:fatal => [:white, :on_red]
|
74
|
+
},
|
75
|
+
:date => :blue,
|
76
|
+
:logger => :magenta,
|
77
|
+
:message => :white
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module DisqueJockey
|
2
|
+
class Supervisor
|
3
|
+
|
4
|
+
def self.work!
|
5
|
+
Process.daemon(true) if DisqueJockey.configuration.daemonize?
|
6
|
+
load_workers
|
7
|
+
spawn_worker_groups
|
8
|
+
trap_signals_in_parent
|
9
|
+
monitor_worker_groups
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.logger
|
13
|
+
@logger ||= DisqueJockey::Logger.new('DisqueJockey')
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.worker_classes
|
17
|
+
@worker_classes ||= []
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def self.child_pids
|
23
|
+
@child_pids ||= []
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.register_worker(worker_class)
|
27
|
+
worker_classes.push(worker_class)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.load_workers
|
31
|
+
Dir.glob('**/workers/*.rb') {|f| require File.expand_path(f)}
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.spawn_worker_groups
|
35
|
+
DisqueJockey.configuration.worker_groups.times { spawn_worker_group }
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.spawn_worker_group
|
39
|
+
child_pids << Process.fork { WorkerGroup.new(worker_classes).work! }
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.monitor_worker_groups
|
43
|
+
# this method never returns, it just
|
44
|
+
# spawns new worker groups if their
|
45
|
+
# processes exit.
|
46
|
+
# DisqueJockey only exits if it receives a
|
47
|
+
# kill signal
|
48
|
+
loop do
|
49
|
+
@dead_disque_jockeys.each do
|
50
|
+
child_pid = @dead_disque_jockeys.shift
|
51
|
+
logger.error "Child worker group exited: #{child_pid}"
|
52
|
+
child_pids.delete(child_pid)
|
53
|
+
spawn_worker_group
|
54
|
+
end
|
55
|
+
sleep(0.1)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.trap_signals_in_parent
|
60
|
+
@dead_disque_jockeys = []
|
61
|
+
%w(QUIT TERM INT ABRT CLD).each do |sig|
|
62
|
+
trap(sig) do
|
63
|
+
if sig == 'CLD'
|
64
|
+
# if a child process dies, we want to
|
65
|
+
# respawn another worker group
|
66
|
+
# This needs to be reentrant, so we queue up dead child
|
67
|
+
# processes to be handled in the run loop, rather than
|
68
|
+
# acting here
|
69
|
+
@dead_disque_jockeys << Process.wait
|
70
|
+
else
|
71
|
+
begin
|
72
|
+
child_pids.each { |pid| Process.kill(sig, pid) }
|
73
|
+
ensure exit
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module DisqueJockey
|
2
|
+
class Worker
|
3
|
+
attr_reader :logger
|
4
|
+
def initialize(logger)
|
5
|
+
@logger = logger.new(self.class.to_s + rand(1000).to_s)
|
6
|
+
end
|
7
|
+
|
8
|
+
def log_exception(e)
|
9
|
+
logger.error "#{self.class} raised exception #{e.inspect}: "
|
10
|
+
logger.error "> " + e.backtrace.reject{|l| l =~ /\.rvm/ }.join("\n> ")
|
11
|
+
end
|
12
|
+
|
13
|
+
class << self
|
14
|
+
attr_reader :queue_name, :thread_count, :timeout_seconds
|
15
|
+
|
16
|
+
# This worker class will subscribe to queue
|
17
|
+
def subscribe_to(queue)
|
18
|
+
@queue_name = queue
|
19
|
+
end
|
20
|
+
|
21
|
+
# minimum number of worker instances of a given worker class.
|
22
|
+
def threads(size)
|
23
|
+
@thread_count = [[size, 1].max, 10].min
|
24
|
+
end
|
25
|
+
|
26
|
+
# seconds to wait for a job to be handled before timing out the worker.
|
27
|
+
# (capped between 0.01 seconds and one hour)
|
28
|
+
def timeout(seconds)
|
29
|
+
@timeout_seconds = [[seconds, 0.01].max, 3600].min
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
# callback method fired when a class inherits from DisqueJockey::Worker
|
35
|
+
def inherited(type)
|
36
|
+
# these are the defaults
|
37
|
+
type.threads 2
|
38
|
+
type.timeout 30
|
39
|
+
# register the new worker type so we can start giving it jobs
|
40
|
+
Supervisor.register_worker(type)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# A WorkerGroup lives in its own process
|
2
|
+
# and runs workers of each worker class. It is effectively
|
3
|
+
# a self-contained unit of workers that fetch jobs and work.
|
4
|
+
module DisqueJockey
|
5
|
+
class WorkerGroup
|
6
|
+
|
7
|
+
def initialize(worker_classes = [])
|
8
|
+
@worker_classes = worker_classes # array of classes to instantiate in our group
|
9
|
+
@worker_pool = {} # initialize a hash for storing workers
|
10
|
+
end
|
11
|
+
|
12
|
+
def work!
|
13
|
+
register_signal_handlers
|
14
|
+
Supervisor.logger.info("Starting worker group with PID #{Process.pid}...")
|
15
|
+
start_workers
|
16
|
+
work_until_signal
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# This loop is in a method so that we can stub it in tests
|
22
|
+
def work_until_signal
|
23
|
+
loop do
|
24
|
+
break if handle_signals
|
25
|
+
sleep(0.1)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Register signal handlers to shut down the worker group.
|
30
|
+
def register_signal_handlers
|
31
|
+
Thread.main[:signal_queue] = []
|
32
|
+
%w(QUIT TERM INT ABRT).each do |signal|
|
33
|
+
# This needs to be reentrant, so we queue up signals to be handled
|
34
|
+
# in the run loop, rather than acting on signals here
|
35
|
+
trap(signal) { Thread.main[:signal_queue] << signal }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# instantiate all the workers we want and start giving
|
40
|
+
# them jobs to do
|
41
|
+
def start_workers
|
42
|
+
@worker_classes.each do |worker_class|
|
43
|
+
build_worker_pool(worker_class)
|
44
|
+
# Each worker_class (and hence, queue), get its own
|
45
|
+
# thread because the Disque client library blocks
|
46
|
+
# when waiting for a job from a queue.
|
47
|
+
Thread.new { fetch_job_and_work(worker_class) }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Deal with signals we receive from the OS by logging the signal
|
52
|
+
# and then killing the worker group
|
53
|
+
def handle_signals
|
54
|
+
signal = Thread.main[:signal_queue].shift
|
55
|
+
if signal
|
56
|
+
Supervisor.logger.info("Received signal #{signal}. Shutting down worker group with PID #{Process.pid}...")
|
57
|
+
return true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# The worker pool gives us a fixed number of worker instances of each class
|
62
|
+
# to do the work. This could be improved by dynamically instantiating
|
63
|
+
# and removing workers from the pool based on workload. For now, we use
|
64
|
+
# a fixed number.
|
65
|
+
def build_worker_pool(worker_class)
|
66
|
+
Supervisor.logger.info("Launching #{worker_class.thread_count} #{worker_class}s")
|
67
|
+
worker_class.thread_count.times do
|
68
|
+
# Use the Queue class so we access our worker pools
|
69
|
+
# from different threads without issues.
|
70
|
+
@worker_pool[worker_class] ||= Queue.new
|
71
|
+
@worker_pool[worker_class].push worker_class.new(Logger)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Here we actually get jobs to work on and hand them off to worker
|
76
|
+
# instances.
|
77
|
+
def fetch_job_and_work(worker_class)
|
78
|
+
broker = Broker.new
|
79
|
+
loop do
|
80
|
+
# this method blocks until a job is returned
|
81
|
+
_, job_id, job = broker.fetch_message_from(worker_class.queue_name)
|
82
|
+
# Queue#pop will block until a worker becomes available
|
83
|
+
worker = @worker_pool[worker_class].pop
|
84
|
+
# now that we have a worker, give it a thread to do its work in
|
85
|
+
# so we can fetch the next job without waiting.
|
86
|
+
Thread.new do
|
87
|
+
begin
|
88
|
+
# Raise a timeout error if the worker takes too long
|
89
|
+
Timeout::timeout(worker_class.timeout_seconds) { worker.handle(job) }
|
90
|
+
# acknowlege the job once we've handled it
|
91
|
+
broker.acknowledge(job_id)
|
92
|
+
rescue StandardError => exception
|
93
|
+
worker.log_exception(exception)
|
94
|
+
# TODO: Need to implement retry logic
|
95
|
+
# Also should do more helpful logging around worker timeouts
|
96
|
+
# (explain the error, log the job and maybe metadata)
|
97
|
+
end
|
98
|
+
# We're done working, so put the worker back in the pool
|
99
|
+
@worker_pool[worker_class].push(worker)
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'disque_jockey/version'
|
2
|
+
require 'disque_jockey/broker'
|
3
|
+
require 'disque_jockey/logger'
|
4
|
+
require 'disque_jockey/supervisor'
|
5
|
+
require 'disque_jockey/worker'
|
6
|
+
require 'disque_jockey/configuration'
|
7
|
+
require 'disque_jockey/worker_group'
|
8
|
+
require 'timeout'
|
9
|
+
|
10
|
+
|
11
|
+
module DisqueJockey
|
12
|
+
# raise exceptions in all threads so we don't fail silently
|
13
|
+
Thread.abort_on_exception = true
|
14
|
+
|
15
|
+
def self.configuration
|
16
|
+
@configuration ||= DisqueJockey::Configuration.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.configure
|
20
|
+
yield(self.configuration)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.run!
|
24
|
+
DisqueJockey::Supervisor.work!
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module DisqueJockey
|
3
|
+
describe Broker do
|
4
|
+
# Note: You actually have to run a Disque server
|
5
|
+
# locally for these tests to pass
|
6
|
+
before(:all) do
|
7
|
+
begin
|
8
|
+
@broker = Broker.new
|
9
|
+
@client = Disque.new(["127.0.0.1:7711"])
|
10
|
+
rescue ArgumentError => error
|
11
|
+
raise ArgumentError, "#{error}. You need to run a Disque server on 127.0.0.1:7711 for these test to pass."
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# This will flush all queues -- potentially really dangerous.
|
16
|
+
# hopefully we can just flush test queues in the future.
|
17
|
+
after(:all) { @client.call('DEBUG', 'FLUSHALL') }
|
18
|
+
|
19
|
+
it "::new takes an array of hosts but provides a default" do
|
20
|
+
expect{ Broker.new }.to_not raise_error
|
21
|
+
expect{ Broker.new(["127.0.0.1:7711"]) }.to_not raise_error
|
22
|
+
end
|
23
|
+
|
24
|
+
it "::new passes on args to Disque client" do
|
25
|
+
expect(Disque).to receive(:new).with(['0.0.0.0'], auth: 'secret')
|
26
|
+
Broker.new(['0.0.0.0'], auth: 'secret')
|
27
|
+
end
|
28
|
+
|
29
|
+
it "#fetch_message_from delivers messages" do
|
30
|
+
@client.push("test_queue", "job", 1000)
|
31
|
+
result = @broker.fetch_message_from('test_queue')
|
32
|
+
expect(result).to be_kind_of(Array)
|
33
|
+
expect(result).to include('test_queue', 'job')
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#acknowledge' do
|
37
|
+
it "returns removes job from queue and returns true if it succeeds" do
|
38
|
+
@client.call('DEBUG', 'FLUSHALL')
|
39
|
+
job_id = @client.push('test_queue', 'test job', 1000)
|
40
|
+
expect(@client.call('QLEN', 'test_queue')).to eq 1
|
41
|
+
expect(@broker.acknowledge(job_id)).to eq true
|
42
|
+
expect(@client.call('QLEN', 'test_queue')).to eq 0
|
43
|
+
end
|
44
|
+
|
45
|
+
it "raises an error for a bad job id" do
|
46
|
+
expect{@broker.acknowledge('bad_id')}.to raise_error(RuntimeError)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DisqueJockey::Configuration do
|
4
|
+
context 'development' do
|
5
|
+
before { ENV['DISQUE_JOCKEY_ENV'] = 'development' }
|
6
|
+
after { ENV['DISQUE_JOCKEY_ENV'] = 'test' }
|
7
|
+
subject { DisqueJockey::Configuration.new }
|
8
|
+
|
9
|
+
it { expect(subject.daemonize?).to eq false }
|
10
|
+
end
|
11
|
+
|
12
|
+
it "defines a log path method" do
|
13
|
+
expect(DisqueJockey::Configuration.new).to respond_to(:log_path)
|
14
|
+
expect(DisqueJockey::Configuration.new).to respond_to(:log_path=)
|
15
|
+
config = DisqueJockey::Configuration.new
|
16
|
+
config.log_path = 'spec-path'
|
17
|
+
expect(config.log_path).to eq 'spec-path'
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DisqueJockey do
|
4
|
+
|
5
|
+
it "sets Thread.abort_on_exception to true" do
|
6
|
+
expect(Thread.abort_on_exception).to eq true
|
7
|
+
end
|
8
|
+
|
9
|
+
it "::configuration returns a configuration object" do
|
10
|
+
expect(DisqueJockey.configuration.class).to eq DisqueJockey::Configuration
|
11
|
+
end
|
12
|
+
|
13
|
+
it "::configure method changes the configuration" do
|
14
|
+
expect do
|
15
|
+
DisqueJockey.configure { |config| config.worker_groups = 1 }
|
16
|
+
end.to_not raise_error
|
17
|
+
expect(DisqueJockey.configuration.worker_groups).to eq 1
|
18
|
+
end
|
19
|
+
|
20
|
+
it "::run! calls the Supervisor::work! method" do
|
21
|
+
expect(DisqueJockey::Supervisor).to receive(:work!)
|
22
|
+
DisqueJockey.run!
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DisqueJockey::Logger do
|
4
|
+
subject { DisqueJockey::Logger }
|
5
|
+
it "defines instance methods" do
|
6
|
+
[:logger, :fatal, :error, :warn, :info, :debug].each do |method|
|
7
|
+
expect(subject.new('test')).to respond_to(method)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DisqueJockey::Supervisor do
|
4
|
+
subject { DisqueJockey::Supervisor }
|
5
|
+
|
6
|
+
after(:each) do
|
7
|
+
subject.instance_variable_set(:@worker_classes, [])
|
8
|
+
end
|
9
|
+
|
10
|
+
it "::register_worker adds a worker" do
|
11
|
+
stub_const("FakeWorker", '')
|
12
|
+
subject.register_worker(FakeWorker)
|
13
|
+
expect(subject.worker_classes).to eq [FakeWorker]
|
14
|
+
end
|
15
|
+
|
16
|
+
it "::logger provides a DisqueJockey::Logger object" do
|
17
|
+
expect(subject.logger.class).to eq DisqueJockey::Logger
|
18
|
+
end
|
19
|
+
|
20
|
+
it "::spawn_worker_groups spawns as many worker groups as the config says" do
|
21
|
+
group = double("WorkerGroup", work!: true)
|
22
|
+
expect(DisqueJockey::WorkerGroup).to receive(:new).twice.and_return(group)
|
23
|
+
allow(Process).to receive(:fork).and_yield
|
24
|
+
allow_any_instance_of(DisqueJockey::Configuration).to receive(:worker_groups).and_return 2
|
25
|
+
expect(group).to receive(:work!).twice
|
26
|
+
subject.send(:spawn_worker_groups)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "::load_workers loads classes in a workers directory" do
|
30
|
+
expect(Object.const_defined?('FixtureWorker')).to eq false
|
31
|
+
subject.send(:load_workers)
|
32
|
+
expect(Object.const_defined?('FixtureWorker')).to eq true
|
33
|
+
end
|
34
|
+
|
35
|
+
it "::spawn_worker_group forks a process and creates a new worker group" do
|
36
|
+
group = double("WorkerGroup", work!: true)
|
37
|
+
expect(DisqueJockey::WorkerGroup).to receive(:new).and_return(group)
|
38
|
+
expect(Process).to receive(:fork).and_yield
|
39
|
+
subject.send(:spawn_worker_group)
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'disque_jockey/worker_shared_setup'
|
3
|
+
module DisqueJockey
|
4
|
+
describe WorkerGroup do
|
5
|
+
include_context "worker setup"
|
6
|
+
subject { WorkerGroup }
|
7
|
+
|
8
|
+
describe "work!" do
|
9
|
+
before(:each) do
|
10
|
+
@worker_classes = [SpecWorker, SecondSpecWorker]
|
11
|
+
allow_any_instance_of(Broker).to receive(:acknowledge).and_return(true)
|
12
|
+
# stub out the method that loops forever so we can get on with our tests
|
13
|
+
allow_any_instance_of(subject).to receive(:work_until_signal) do
|
14
|
+
# We sleep here so that the job has time to run before we return from
|
15
|
+
# this method. I'm sure there is a better way to do this.
|
16
|
+
sleep(0.01)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it "can be instantiated without errors" do
|
21
|
+
expect{subject.new([SpecWorker]).work!}.to_not raise_error
|
22
|
+
end
|
23
|
+
|
24
|
+
it "instantiates the correct number of workers" do
|
25
|
+
[SpecWorker, SecondSpecWorker].each do |worker_class|
|
26
|
+
allow(worker_class).to receive(:new).and_call_original
|
27
|
+
expect(worker_class).to receive(:new).exactly(worker_class.thread_count).times
|
28
|
+
end
|
29
|
+
subject.new(@worker_classes).work!
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
it "gives workers jobs to perform" do
|
34
|
+
allow_any_instance_of(Broker).to receive(:fetch_message_from).and_return(['dummy', 'test_id', 'test job'])
|
35
|
+
expect_any_instance_of(SecondSpecWorker).to receive(:handle).at_least(:once)
|
36
|
+
subject.new(@worker_classes).work!
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "handling logic around jobs" do
|
40
|
+
|
41
|
+
it "times out workers that take too long" do
|
42
|
+
allow_any_instance_of(Broker).to receive(:fetch_message_from).and_return(['dummy', 'test_id', 'test job'])
|
43
|
+
allow_any_instance_of(subject).to receive(:work_until_signal) { sleep(0.2) }
|
44
|
+
expect_any_instance_of(SlowWorker).to receive(:log_exception).at_least(:once)
|
45
|
+
subject.new([SlowWorker]).work!
|
46
|
+
end
|
47
|
+
|
48
|
+
xit "acknowledges jobs if they are processed without errors" do
|
49
|
+
allow_any_instance_of(subject).to receive(:work_until_signal) { sleep(0.1) }
|
50
|
+
allow_any_instance_of(Broker).to receive(:fetch_message_from).and_return(['dummy', 'test_id', 'test job'])
|
51
|
+
expect_any_instance_of(Broker).to receive(:acknowledge).with('test_id').at_least(:once)
|
52
|
+
subject.new([SecondSpecWorker]).work!
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
shared_context "worker setup" do
|
2
|
+
|
3
|
+
before(:all) do
|
4
|
+
|
5
|
+
class SpecWorker < DisqueJockey::Worker
|
6
|
+
subscribe_to "test"
|
7
|
+
def handle(job); end
|
8
|
+
end
|
9
|
+
|
10
|
+
class SecondSpecWorker < DisqueJockey::Worker
|
11
|
+
subscribe_to "other-test"
|
12
|
+
threads 1
|
13
|
+
timeout 1
|
14
|
+
def handle(job); end
|
15
|
+
end
|
16
|
+
|
17
|
+
class SlowWorker < DisqueJockey::Worker
|
18
|
+
subscribe_to "slow-test"
|
19
|
+
timeout 0.01
|
20
|
+
threads 1
|
21
|
+
def handle(job); sleep(0.1); end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'disque_jockey'
|
3
|
+
require 'disque_jockey/worker_shared_setup'
|
4
|
+
|
5
|
+
describe DisqueJockey::Worker do
|
6
|
+
include_context "worker setup"
|
7
|
+
|
8
|
+
it "defines class methods" do
|
9
|
+
[ :queue_name, :thread_count, :timeout_seconds,
|
10
|
+
:subscribe_to, :timeout, :threads
|
11
|
+
].each do |method|
|
12
|
+
expect(DisqueJockey::Worker).to respond_to(method)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it "defines logger method" do
|
17
|
+
expect(DisqueJockey::Worker.instance_methods.include?(:logger)).to eq(true)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "keeps track of classes that subclass it" do
|
21
|
+
initial_subclass_count = DisqueJockey::Supervisor.worker_classes.length
|
22
|
+
class ChildWorker < DisqueJockey::Worker; end
|
23
|
+
expect(DisqueJockey::Supervisor.worker_classes.length).to eq (initial_subclass_count + 1)
|
24
|
+
end
|
25
|
+
|
26
|
+
context "defaults" do
|
27
|
+
it "sets a default for timeout" do
|
28
|
+
expect(SpecWorker.timeout_seconds).to be(30)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "sets a default for thread_count" do
|
32
|
+
expect(SpecWorker.thread_count).to be(2)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context "class methods"
|
37
|
+
context "overrides" do
|
38
|
+
it "allows overrides for timeout, and thread_count" do
|
39
|
+
expect(SecondSpecWorker.timeout_seconds).to be(1)
|
40
|
+
expect(SecondSpecWorker.thread_count).to be(1)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "instance methods"
|
45
|
+
describe "#initialize" do
|
46
|
+
before do
|
47
|
+
@logger = double(:logger, info: true, error: true, warn: true)
|
48
|
+
allow(@logger).to receive(:new).and_return(@logger)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "sets a logger" do
|
52
|
+
worker = SpecWorker.new(@logger)
|
53
|
+
expect(worker.logger).to_not be_nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
class FixtureWorker < DisqueJockey::Worker; end
|
data/spec/log/.keep
ADDED
File without changes
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: disque_jockey
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Devin Riley
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-06-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: disque
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: logging
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.1'
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '3.0'
|
51
|
+
type: :development
|
52
|
+
prerelease: false
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - "~>"
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '3.1'
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '3.0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rake
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: pry
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
description: A framework for managing and running ruby background workers.
|
90
|
+
email:
|
91
|
+
- devinriley84+disque_jockey@gmail.com
|
92
|
+
executables:
|
93
|
+
- disque_jockey
|
94
|
+
extensions: []
|
95
|
+
extra_rdoc_files: []
|
96
|
+
files:
|
97
|
+
- ".gitignore"
|
98
|
+
- ".rspec"
|
99
|
+
- Gemfile
|
100
|
+
- README.md
|
101
|
+
- Rakefile
|
102
|
+
- bin/disque_jockey
|
103
|
+
- disque_jockey.gemspec
|
104
|
+
- lib/disque_jockey.rb
|
105
|
+
- lib/disque_jockey/broker.rb
|
106
|
+
- lib/disque_jockey/configuration.rb
|
107
|
+
- lib/disque_jockey/logger.rb
|
108
|
+
- lib/disque_jockey/supervisor.rb
|
109
|
+
- lib/disque_jockey/version.rb
|
110
|
+
- lib/disque_jockey/worker.rb
|
111
|
+
- lib/disque_jockey/worker_group.rb
|
112
|
+
- spec/disque_jockey/broker_spec.rb
|
113
|
+
- spec/disque_jockey/configuration_spec.rb
|
114
|
+
- spec/disque_jockey/disque_jockey_spec.rb
|
115
|
+
- spec/disque_jockey/logger_spec.rb
|
116
|
+
- spec/disque_jockey/supervisor_spec.rb
|
117
|
+
- spec/disque_jockey/worker_group_spec.rb
|
118
|
+
- spec/disque_jockey/worker_shared_setup.rb
|
119
|
+
- spec/disque_jockey/worker_spec.rb
|
120
|
+
- spec/fixtures/workers/fixture_worker.rb
|
121
|
+
- spec/log/.keep
|
122
|
+
- spec/spec_helper.rb
|
123
|
+
homepage: https://github.com/DevinRiley/disque_jockey
|
124
|
+
licenses:
|
125
|
+
- MIT
|
126
|
+
metadata: {}
|
127
|
+
post_install_message:
|
128
|
+
rdoc_options: []
|
129
|
+
require_paths:
|
130
|
+
- lib
|
131
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
requirements: []
|
142
|
+
rubyforge_project:
|
143
|
+
rubygems_version: 2.4.5
|
144
|
+
signing_key:
|
145
|
+
specification_version: 4
|
146
|
+
summary: A background job framework.
|
147
|
+
test_files:
|
148
|
+
- spec/disque_jockey/broker_spec.rb
|
149
|
+
- spec/disque_jockey/configuration_spec.rb
|
150
|
+
- spec/disque_jockey/disque_jockey_spec.rb
|
151
|
+
- spec/disque_jockey/logger_spec.rb
|
152
|
+
- spec/disque_jockey/supervisor_spec.rb
|
153
|
+
- spec/disque_jockey/worker_group_spec.rb
|
154
|
+
- spec/disque_jockey/worker_shared_setup.rb
|
155
|
+
- spec/disque_jockey/worker_spec.rb
|
156
|
+
- spec/fixtures/workers/fixture_worker.rb
|
157
|
+
- spec/log/.keep
|
158
|
+
- spec/spec_helper.rb
|