disque_jockey 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 +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
|