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 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
@@ -0,0 +1,7 @@
1
+ Gemfile.lock
2
+ *.gem
3
+ .bundle/
4
+ .DS_Store
5
+ test.log
6
+ tmp/
7
+ pkg/
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
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,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'disque_jockey'
4
+ DisqueJockey.run!
@@ -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,3 @@
1
+ module DisqueJockey
2
+ VERSION = '0.0.1'
3
+ 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
@@ -0,0 +1,2 @@
1
+ ENV['DISQUE_JOCKEY_ENV'] = 'test'
2
+ require 'disque_jockey'
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