disque_jockey 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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