ruote-resque 0.1.0

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