ruote-resque 0.1.0

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.
@@ -0,0 +1,42 @@
1
+ # encoding: UTF-8
2
+
3
+ module Ruote
4
+ module Resque
5
+
6
+ # An object to easy Participant registration
7
+ # @example Register a participant
8
+ # # Will register a participant be_awesome
9
+ # # that enqueues the Job MyAwesomeJob to my_queue
10
+ # Ruote::Resque.register(dashboard) do
11
+ # be_awesome 'MyAwesomeJob', :my_queue
12
+ # # or via the participant method
13
+ # participant 'be_awesome', 'MyAwesomeJob', :my_queue
14
+ # end
15
+ class ParticipantRegistrar
16
+
17
+ # @param [Ruote::Dashboard] dashboard
18
+ # @return [Ruote::Resque::ParticipantRegistrar]
19
+ def initialize(dashboard)
20
+ @dashboard = dashboard
21
+ end
22
+
23
+ # Implements the dsl to register participants
24
+ # @see Ruote::Resque::ParticipantRegistrar
25
+ def method_missing(method_name, *args, &block)
26
+ participant(method_name.to_s, *args, &block)
27
+ end
28
+
29
+ # Call this method to register a participant (or use method_missing)
30
+ # @param [#to_s] name the name of the participant
31
+ # @param [#to_s] klass the class of the Resque job
32
+ # @param [#to_s] queue the queue of the job
33
+ # @param [Hash] options options to be passed on to +Ruote::Resque::Participant+
34
+ # @return [void]
35
+ def participant(name, klass, queue, options = {})
36
+ options.merge!({ :class => klass, :queue => queue })
37
+ @dashboard.register_participant(name, Ruote::Resque::Participant, options)
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,140 @@
1
+ # encoding: UTF-8
2
+
3
+ module Ruote
4
+ module Resque
5
+
6
+ # Raised when a job different from `Ruote::Resque::ReplyJob
7
+ # is found on the reply_queue.
8
+ class InvalidJob < RuntimeError
9
+ end
10
+
11
+ # Raised when a reply job has an invalid workitem.
12
+ class InvalidWorkitem < RuntimeError
13
+ end
14
+
15
+ # The receiver will poll the reply_queue in Resque, waiting for reply jobs.
16
+ # It does so in a new thread.
17
+ #
18
+ # By default it polls the reply_queue every 5 seconds, but this is configurable via
19
+ # the `interval` configuration option. See {Ruote::Resque}.
20
+ #
21
+ # You should launch the Receiver as soon as your engine is set up.
22
+ #
23
+ # @example Running a ruote-resque Receiver
24
+ # Ruote::Resque::Receiver.new(dashboard)
25
+ #
26
+ # @example Overriding the handle_error method for custom exception handling
27
+ # class Ruote::Resque::Receiver
28
+ # def handle_error(e)
29
+ # MyErrorHandler.handle(e)
30
+ # end
31
+ # end
32
+ #
33
+ # Ruote::Resque::Receiver.new(dashboard)
34
+ #
35
+ class Receiver < ::Ruote::Receiver
36
+
37
+ # Retunrs a new Receiver instance and spawns a worker thread.
38
+ # @param [Ruote::Dashboard] cwes Accepts context, worker, engine or storage
39
+ # @param [Hash] options Passed on to Ruote, currently unused.
40
+ # @return [Receiver]
41
+ def initialize(cwes, options = {})
42
+ super
43
+ @listener = listen
44
+ end
45
+
46
+ # Stops the worker thread.
47
+ # @return [void]
48
+ def shutdown
49
+ @listener.kill
50
+ end
51
+
52
+ # Called when an error is raised during the poll/reserve/process flow of the Receiver.
53
+ # You should override this method for custom error handling.
54
+ # By default it just logs the exception.
55
+ # @param [Exception] e
56
+ # @return [void]
57
+ def handle_error(e)
58
+ Ruote::Resque.logger.error(e)
59
+ end
60
+
61
+ private
62
+
63
+ def listen
64
+
65
+ Thread.new do
66
+ loop do
67
+ work
68
+ end
69
+ end
70
+
71
+ end
72
+
73
+ def work
74
+
75
+ reserve
76
+
77
+ # handle_error may raise an exception itself
78
+ # in this case protect the thread
79
+ rescue => e
80
+ Ruote::Resque.logger.error('*** UNCAUGHT EXCEPTION IN RUOTE::RESQUE::RECEIVER ***')
81
+ Ruote::Resque.logger.error(e)
82
+ end
83
+
84
+ def reserve
85
+
86
+ if job = ::Resque.reserve(Ruote::Resque.configuration.reply_queue)
87
+ validate_job(job)
88
+ process(job)
89
+ else
90
+ sleep Ruote::Resque.configuration.interval
91
+ end
92
+
93
+ rescue => e
94
+ handle_error(e)
95
+ end
96
+
97
+ def process(job)
98
+
99
+ job_arguments = job.args
100
+ item = job_arguments.pop
101
+
102
+ if job_arguments.any?
103
+ flunk(item, *job_arguments)
104
+ else
105
+ receive(item)
106
+ end
107
+
108
+ rescue => e
109
+ # Fail the job on Resque, then raise to let handle_error do it's work
110
+ job.fail(e)
111
+ raise
112
+ end
113
+
114
+ def validate_job(job)
115
+
116
+ job_class = job.payload_class.to_s
117
+ unless job_class == 'Ruote::Resque::ReplyJob'
118
+ raise InvalidJob.new(job_class)
119
+ end
120
+
121
+ item = job.args.last
122
+ unless item.is_a?(Hash) && item['fields'] && item['fei']
123
+ raise InvalidWorkitem.new(item.inspect)
124
+ end
125
+
126
+ end
127
+
128
+ def flunk(workitem, class_name, message, backtrace)
129
+
130
+ error = Ruote::ReceivedError.new(class_name, message, backtrace)
131
+ args = [error, message, backtrace]
132
+
133
+ super(workitem, *args)
134
+
135
+ end
136
+
137
+ end
138
+
139
+ end
140
+ end
@@ -0,0 +1,23 @@
1
+ # encoding: UTF-8
2
+
3
+ module Ruote
4
+ module Resque
5
+
6
+ # An empty job used for message passing between job instances and ruote-resque.
7
+ module ReplyJob
8
+
9
+ # @return [#to_s] The configured queue for message passing.
10
+ def self.queue
11
+ Ruote::Resque.configuration.reply_queue
12
+ end
13
+
14
+ # This is a no-op.
15
+ # @return [void]
16
+ def self.perform(*args)
17
+ # noop
18
+ end
19
+
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ # encoding: UTF-8
2
+
3
+ module Ruote
4
+ module Resque
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ruote/resque/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ruote-resque"
8
+ spec.version = Ruote::Resque::VERSION
9
+ spec.authors = ["Adrien Kohlbecker"]
10
+ spec.email = ["adrien.kohlbecker@gmail.com"]
11
+ spec.description = %q{Resque participant/receiver pair for Ruote}
12
+ spec.summary = %q{Resque participant/receiver pair for Ruote}
13
+ spec.homepage = "https://github.com/adrienkohlbecker/ruote-resque"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency 'resque'
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency 'mutant'
25
+ spec.add_development_dependency 'rubocop'
26
+ spec.add_development_dependency 'yard'
27
+ spec.add_development_dependency 'redcarpet'
28
+ end
@@ -0,0 +1,129 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Ruote::Resque::Job do
6
+
7
+ context '::after_perform_reply_to_ruote' do
8
+
9
+ class Job
10
+ @queue = :rspec
11
+ extend Ruote::Resque::Job
12
+ def self.perform(workitem)
13
+ workitem['is_resque_awesome'] = true
14
+ end
15
+ end
16
+
17
+ let(:workitem) { { 'is_rspec_awesome' => true } }
18
+ let(:enqued_job) { ::Resque.reserve(Ruote::Resque.configuration.reply_queue) }
19
+
20
+ context 'enqueues a job' do
21
+
22
+ before :each do
23
+ Job.after_perform_reply_to_ruote(workitem)
24
+ end
25
+
26
+ it 'to resque' do
27
+ expect(enqued_job.class).to eq ::Resque::Job
28
+ end
29
+
30
+ it 'to the configured reply_queue' do
31
+ expect(enqued_job.queue).to eq Ruote::Resque.configuration.reply_queue
32
+ end
33
+
34
+ it 'with the workitem as arguments' do
35
+ expect(enqued_job.args).to eq [workitem]
36
+ end
37
+
38
+ end
39
+
40
+ context 'replies with the mutated workitem' do
41
+
42
+ let(:mutated_workitem) { { 'is_rspec_awesome' => true, 'is_resque_awesome' => true } }
43
+
44
+ before :each do
45
+ Resque.inline = true
46
+ end
47
+
48
+ after :each do
49
+ Resque.inline = false
50
+ end
51
+
52
+ it 'allows the workitem to be mutated' do
53
+
54
+ Ruote::Resque::ReplyJob.should_receive(:perform).with(mutated_workitem)
55
+ Resque.enqueue(Job, workitem)
56
+
57
+ end
58
+
59
+ end
60
+
61
+ end
62
+
63
+ context '::on_failure_reply_to_ruote' do
64
+
65
+ class ErrorJob
66
+ @queue = :rspec
67
+ extend Ruote::Resque::Job
68
+ def self.perform(workitem)
69
+ workitem['is_resque_awesome'] = true
70
+ raise 'i am a failure'
71
+ end
72
+ end
73
+
74
+ let(:workitem) { { 'is_rspec_awesome' => true } }
75
+ let(:enqued_job) { ::Resque.reserve(Ruote::Resque.configuration.reply_queue) }
76
+ let(:exception) { RuntimeError.new('i am a failure') }
77
+ let(:expected_job_args) do
78
+ [
79
+ exception.class.name,
80
+ exception.message,
81
+ exception.backtrace,
82
+ workitem
83
+ ]
84
+ end
85
+
86
+ context 'enqueues a job' do
87
+
88
+ before :each do
89
+ ErrorJob.on_failure_reply_to_ruote(exception, workitem)
90
+ end
91
+
92
+ it 'to resque' do
93
+ expect(enqued_job.class).to eq ::Resque::Job
94
+ end
95
+
96
+ it 'to the configured reply_queue' do
97
+ expect(enqued_job.queue).to eq Ruote::Resque.configuration.reply_queue
98
+ end
99
+
100
+ it 'with the workitem and the error as arguments' do
101
+ expect(enqued_job.args).to eq expected_job_args
102
+ end
103
+
104
+ end
105
+
106
+ context 'replies with the mutated workitem' do
107
+
108
+ let(:mutated_workitem) { { 'is_rspec_awesome' => true, 'is_resque_awesome' => true } }
109
+
110
+ before :each do
111
+ Resque.inline = true
112
+ end
113
+
114
+ after :each do
115
+ Resque.inline = false
116
+ end
117
+
118
+ it 'allows the workitem to be mutated' do
119
+
120
+ Ruote::Resque::ReplyJob.should_receive(:perform).with(hash_including(mutated_workitem))
121
+ Resque.enqueue(Job, workitem)
122
+
123
+ end
124
+
125
+ end
126
+
127
+ end
128
+
129
+ end
@@ -0,0 +1,52 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ class MyAwesomeJob
6
+ end
7
+
8
+ describe Ruote::Resque::ParticipantRegistrar do
9
+
10
+ context '#participant' do
11
+
12
+ let(:mock_dashboard) { Object.new }
13
+ let(:registrar) { Ruote::Resque::ParticipantRegistrar.new(mock_dashboard) }
14
+
15
+ it 'registers the participant to the dashboard' do
16
+
17
+ mock_dashboard.should_receive(:register_participant).with('be_awesome', Ruote::Resque::Participant, { :class => MyAwesomeJob, :queue => :rspec })
18
+ registrar.participant('be_awesome', MyAwesomeJob, :rspec)
19
+
20
+ end
21
+
22
+ it 'allows more options to be sent' do
23
+
24
+ mock_dashboard.should_receive(:register_participant).with('be_awesome', Ruote::Resque::Participant, { :class => MyAwesomeJob, :queue => :rspec, :custom_option => :custom_value })
25
+ registrar.participant('be_awesome', MyAwesomeJob, :rspec, :custom_option => :custom_value)
26
+
27
+ end
28
+
29
+ end
30
+
31
+ context '#method_missing' do
32
+
33
+ let(:mock_dashboard) { Object.new }
34
+ let(:registrar) { Ruote::Resque::ParticipantRegistrar.new(mock_dashboard) }
35
+
36
+ it 'registers the participant to the dashboard' do
37
+
38
+ mock_dashboard.should_receive(:register_participant).with('be_awesome', Ruote::Resque::Participant, { :class => MyAwesomeJob, :queue => :rspec })
39
+ registrar.be_awesome(MyAwesomeJob, :rspec)
40
+
41
+ end
42
+
43
+ it 'allows more options to be sent' do
44
+
45
+ mock_dashboard.should_receive(:register_participant).with('be_awesome', Ruote::Resque::Participant, { :class => MyAwesomeJob, :queue => :rspec, :custom_option => :custom_value })
46
+ registrar.be_awesome(MyAwesomeJob, :rspec, :custom_option => :custom_value)
47
+
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,118 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Ruote::Resque::Participant do
6
+
7
+ it 'includes Ruote::LocalParticipant' do
8
+ expect(Ruote::Resque::Participant.ancestors).to include(Ruote::LocalParticipant)
9
+ end
10
+
11
+ it 'does not thread' do
12
+ expect(Ruote::Resque::Participant.new('class' => Class, 'queue' => :my_queue).do_not_thread).to be_true
13
+ end
14
+
15
+ context '#initialize' do
16
+
17
+ it 'takes a job class in options' do
18
+
19
+ participant = Ruote::Resque::Participant.new('class' => Class, 'queue' => :my_other_queue)
20
+ expect(participant.instance_variable_get(:@job_klass)).to eq Class
21
+
22
+ end
23
+
24
+ it 'takes a job queue in options' do
25
+
26
+ participant = Ruote::Resque::Participant.new('class' => Class, 'queue' => :my_other_queue)
27
+ expect(participant.instance_variable_get(:@job_queue)).to eq :my_other_queue
28
+
29
+ end
30
+
31
+ it 'validates that jobs have a queue' do
32
+ expect { Ruote::Resque::Participant.new('class' => Class) }.to raise_error ::Resque::NoQueueError
33
+ end
34
+
35
+ it 'validates that jobs have a class' do
36
+ expect { Ruote::Resque::Participant.new('class' => '', 'queue' => :my_other_queue) }.to raise_error ::Resque::NoClassError
37
+ end
38
+
39
+ end
40
+
41
+ context '#on_workitem' do
42
+
43
+ let(:workitem) { { 'rspec_is_awesome' => true } }
44
+
45
+ before :each do
46
+ participant.stub(:workitem).and_return Ruote::Workitem.new(workitem)
47
+ end
48
+
49
+ context 'with forget set to false' do
50
+
51
+ let(:participant) { Ruote::Resque::Participant.new('class' => Class, 'queue' => :my_queue) }
52
+
53
+ it 'enqueues the given job to Resque' do
54
+ participant.on_workitem
55
+ expected_job = { 'class' => 'Class', 'args' => [workitem] }
56
+ expect(::Resque.pop(:my_queue)).to eq expected_job
57
+ end
58
+
59
+ it 'does not reply to ruote' do
60
+ participant.should_not_receive(:reply)
61
+ participant.on_workitem
62
+ end
63
+
64
+ end
65
+
66
+ context 'with forget set to true' do
67
+
68
+ let(:participant) { Ruote::Resque::Participant.new('class' => Class, 'queue' => :my_queue, 'forget' => true) }
69
+
70
+ it 'replies to ruote' do
71
+ participant.should_receive(:reply)
72
+ participant.on_workitem
73
+ end
74
+
75
+ end
76
+
77
+ end
78
+
79
+ context '#on_cancel' do
80
+
81
+ let(:workitem) { { 'rspec_is_awesome' => true } }
82
+ let(:participant) { Ruote::Resque::Participant.new('class' => Class, 'queue' => :my_queue) }
83
+
84
+ before :each do
85
+ participant.stub(:workitem).and_return Ruote::Workitem.new(workitem)
86
+ participant.stub(:applied_workitem).and_return Ruote::Workitem.new(workitem)
87
+ participant.on_workitem
88
+ end
89
+
90
+ it 'removes the given job to Resque' do
91
+ participant.on_cancel
92
+ expect(::Resque.pop(:my_queue)).to eq nil
93
+ end
94
+
95
+ it 'removes only jobs with the same arguments' do
96
+ another_participant = Ruote::Resque::Participant.new('class' => Class, 'queue' => :my_queue)
97
+ another_workitem = { 'im_another_workitem' => true }
98
+ another_participant.stub(:workitem).and_return Ruote::Workitem.new(another_workitem)
99
+ another_participant.on_workitem
100
+
101
+ participant.on_cancel
102
+ expect(::Resque.pop(:my_queue)).to eq({ 'class' => 'Class', 'args' => [another_workitem] })
103
+ end
104
+
105
+ end
106
+
107
+ context '#encode_workitem' do
108
+
109
+ let(:workitem_hash) { { 'rspec_is_awesome' => true } }
110
+ let(:workitem) { Ruote::Workitem.new(workitem_hash) }
111
+
112
+ it 'returns a hash representation of the workitem' do
113
+ expect(Ruote::Resque::Participant.new('class' => Class, 'queue' => :my_queue).encode_workitem(workitem)).to eq(workitem_hash)
114
+ end
115
+
116
+ end
117
+
118
+ end