resque-clues 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,69 @@
1
+ require 'digest/md5'
2
+ require 'time'
3
+
4
+ module Resque
5
+ module Plugins
6
+ module Clues
7
+ # Module capable of redefining the Resque#push and Resque#pop methods so
8
+ # that:
9
+ #
10
+ # * metadata will be stored in redis.
11
+ # * The metadata can be injected with arbitrary data by a configured item
12
+ # preprocessor.
13
+ # * That event data (including its metadata) will be published, provided
14
+ # an event publisher has been configured.
15
+ module QueueExtension
16
+ # push an item onto the queue. If resque-clues is configured, this
17
+ # will First create the metadata associated with the event and adds it
18
+ # to the item. This will include:
19
+ #
20
+ # * event_hash: a unique item identifying the job, will be included
21
+ # with other events arising from that job.
22
+ # * hostname: the hostname of the machine where the event occurred.
23
+ # * process: The process id of the ruby process where the event
24
+ # occurred.
25
+ # * plus any items injected into the item via a configured
26
+ # item_preprocessor.
27
+ #
28
+ # After that, an enqueued event is published and the original push
29
+ # operation is invoked.
30
+ #
31
+ # queue:: The queue to push onto
32
+ # orig:: The original item to push onto the queue.
33
+ def _clues_push(queue, item)
34
+ return _base_push(queue, item) unless Clues.clues_configured?
35
+ item['clues_metadata'] = {
36
+ 'event_hash' => Clues.event_hash,
37
+ 'hostname' => Clues.hostname,
38
+ 'process' => Clues.process,
39
+ 'enqueued_time' => Time.now.utc.to_f
40
+ }
41
+ if Resque::Plugins::Clues.item_preprocessor
42
+ Resque::Plugins::Clues.item_preprocessor.call(queue, item)
43
+ end
44
+ Clues.event_publisher.publish(:enqueued, Clues.now, queue, item['clues_metadata'], item[:class], *item[:args])
45
+ _base_push(queue, item)
46
+ end
47
+
48
+ # pops an item off the head of the queue. This will use the original
49
+ # pop operation to get the item, then calculate the time in queue and
50
+ # broadcast a dequeued event.
51
+ #
52
+ # queue:: The queue to pop from.
53
+ def _clues_pop(queue)
54
+ _base_pop(queue).tap do |item|
55
+ # TODO refactor
56
+ unless item.nil?
57
+ if Clues.clues_configured? and item['clues_metadata']
58
+ item['clues_metadata']['hostname'] = Clues.hostname
59
+ item['clues_metadata']['process'] = Clues.process
60
+ item['clues_metadata']['time_in_queue'] = Clues.time_delta_since(item['clues_metadata']['enqueued_time'])
61
+ Clues.event_publisher.publish(:dequeued, Clues.now, queue, item['clues_metadata'], item['class'], *item['args'])
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,45 @@
1
+ require 'time'
2
+ require 'delegate'
3
+
4
+ module Resque
5
+ module Plugins
6
+ module Clues
7
+ # A unique event hash crafted from the hostname, process and time.
8
+ def self.event_hash
9
+ Digest::MD5.hexdigest("#{hostname}#{process}#{Time.now.utc.to_f}")
10
+ end
11
+
12
+ # The hostname Resque is running on.
13
+ def self.hostname
14
+ @hostname ||= Socket.gethostname
15
+ end
16
+
17
+ # The process id.
18
+ def self.process
19
+ $$
20
+ end
21
+
22
+ # The current UTC time in iso 8601 format.
23
+ def self.now
24
+ Time.now.utc.iso8601
25
+ end
26
+
27
+ # The delta of time between the passed time and now.
28
+ def self.time_delta_since(start)
29
+ result = Time.now.utc.to_f - start.to_f
30
+ result >= 0.0 ? result : 0.0
31
+ end
32
+
33
+ # Convenience method to determine if resque-clues is properly
34
+ # configured.
35
+ def self.clues_configured?
36
+ Resque::Plugins::Clues.configured?
37
+ end
38
+
39
+ # Convenience method to access the resque-clues event publisher.
40
+ def self.event_publisher
41
+ Resque::Plugins::Clues.event_publisher
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,5 @@
1
+ module Resque
2
+ module Clues
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'resque/plugins/clues/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "resque-clues"
8
+ gem.version = Resque::Clues::VERSION
9
+ gem.authors = ["Lance Woodson"]
10
+ gem.email = ["lance.woodson@peopleadmin.com"]
11
+ gem.description = %q{Adds event publishing and job tracking ability to Resque}
12
+ gem.summary = %q{Adds event publishing and job tracking}
13
+ gem.homepage = "https://github.com/PeopleAdmin/resque-clues"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ gem.add_dependency 'resque', '>= 1.20.0'
20
+ gem.add_dependency 'multi_json', '~> 1.7.4'
21
+ gem.add_development_dependency 'rake', '~> 0.9.2.2'
22
+ gem.add_development_dependency 'rspec'
23
+ gem.add_development_dependency 'pry'
24
+ gem.add_development_dependency 'pry-debugger'
25
+ gem.add_development_dependency 'cane'
26
+ end
@@ -0,0 +1,193 @@
1
+ require 'spec_helper'
2
+ require 'stringio'
3
+ require 'json'
4
+ require 'fileutils'
5
+ require 'time'
6
+ require 'tmpdir'
7
+
8
+ describe 'event publishers' do
9
+
10
+ before do
11
+ @current_time = Time.now.utc.iso8601
12
+ end
13
+
14
+ def publish_event_type(type)
15
+ @publisher.publish(type, @current_time, :test_queue, {}, "FooBar", "a", "b")
16
+ end
17
+
18
+ describe Resque::Plugins::Clues::StreamPublisher do
19
+ before do
20
+ @stream = StreamIO.new
21
+ @publisher = Resque::Plugins::Clues::StreamPublisher.new(@stream)
22
+ end
23
+ end
24
+
25
+ describe Resque::Plugins::Clues::StreamPublisher do
26
+ def verify_output_for_event_type(type)
27
+ @stream.rewind
28
+ event = MultiJson.load(@stream.readlines[-1].chomp)
29
+ event["queue"].should == "test_queue"
30
+ event["metadata"].should == {}
31
+ event["timestamp"].should_not be_nil
32
+ event["event_type"].should == type.to_s
33
+ event["worker_class"].should == "FooBar"
34
+ event["args"].should == ["a", "b"]
35
+ end
36
+
37
+ before do
38
+ @stream = StringIO.new
39
+ @publisher = Resque::Plugins::Clues::StreamPublisher.new(@stream)
40
+ end
41
+
42
+ it "should pass Resque lint detection" do
43
+ Resque::Plugin.lint(Resque::Plugins::Clues::StandardOutPublisher)
44
+ end
45
+
46
+ it "should send enqueued event to STDOUT" do
47
+ publish_event_type :enqueued
48
+ verify_output_for_event_type :enqueued
49
+ end
50
+
51
+ it "should send dequeued event to STDOUT" do
52
+ publish_event_type :dequeued
53
+ verify_output_for_event_type :dequeued
54
+ end
55
+
56
+ it "should send perform_started event to STDOUT" do
57
+ publish_event_type :perform_started
58
+ verify_output_for_event_type :perform_started
59
+ end
60
+
61
+ it "should send perform_finished event to STDOUT" do
62
+ publish_event_type :perform_finished
63
+ verify_output_for_event_type :perform_finished
64
+ end
65
+
66
+ it "should send failed event to STDOUT" do
67
+ publish_event_type :failed
68
+ verify_output_for_event_type :failed
69
+ end
70
+
71
+ it "should send destroyed event to STDOUT" do
72
+ publish_event_type :destroyed
73
+ verify_output_for_event_type :destroyed
74
+ end
75
+ end
76
+
77
+
78
+ describe Resque::Plugins::Clues::LogPublisher do
79
+ def verify_event_written_to_log(event_type)
80
+ last_event["event_type"].should == event_type.to_s
81
+ last_event["timestamp"].should == @current_time.to_s
82
+ last_event["queue"].should == 'test_queue'
83
+ last_event["metadata"].should == {}
84
+ last_event["worker_class"].should == "FooBar"
85
+ last_event["args"].should == ["a", "b"]
86
+ end
87
+
88
+ def last_event
89
+ MultiJson.load(File.readlines(@log_path)[-1].chomp)
90
+ end
91
+
92
+ before do
93
+ @log_path = File.join(Dir.tmpdir, "test_log.log")
94
+ FileUtils.rm(@log_path) if File.exists?(@log_path)
95
+ @publisher = Resque::Plugins::Clues::LogPublisher.new(@log_path)
96
+ end
97
+
98
+ it "should pass Resque lint detection" do
99
+ Resque::Plugin.lint(Resque::Plugins::Clues::LogPublisher)
100
+ end
101
+
102
+ it "should write enqueued event to file" do
103
+ publish_event_type :enqueued
104
+ verify_event_written_to_log :enqueued
105
+ end
106
+
107
+ it "should write dequeued event to file" do
108
+ publish_event_type :dequeued
109
+ verify_event_written_to_log :dequeued
110
+ end
111
+
112
+ it "should write destroyed event to file" do
113
+ publish_event_type :destroyed
114
+ verify_event_written_to_log :destroyed
115
+ end
116
+
117
+ it "should write perform_started event to file" do
118
+ publish_event_type :perform_started
119
+ verify_event_written_to_log :perform_started
120
+ end
121
+
122
+ it "should write perform_finished event to file" do
123
+ publish_event_type :perform_finished
124
+ verify_event_written_to_log :perform_finished
125
+ end
126
+
127
+ it "should write failed event to file" do
128
+ publish_event_type :failed
129
+ verify_event_written_to_log :failed
130
+ end
131
+
132
+ it "should write 1 event per line in the file" do
133
+ publish_event_type :enqueued
134
+ publish_event_type :dequeued
135
+ File.readlines(@log_path)[1..-1].size.should == 2
136
+ end
137
+ end
138
+
139
+ describe Resque::Plugins::Clues::CompositePublisher do
140
+ before do
141
+ @publisher = Resque::Plugins::Clues::CompositePublisher.new
142
+ @publisher << Resque::Plugins::Clues::StandardOutPublisher.new
143
+ @publisher << Resque::Plugins::Clues::StandardOutPublisher.new
144
+ end
145
+
146
+ def verify_event_delegated_to_children(event_type)
147
+ @publisher.each do |child|
148
+ child.should_receive(:publish).with(
149
+ event_type, @current_time, :test_queue, {}, "FooBar", "a", "b")
150
+ end
151
+ end
152
+
153
+ it "should pass Resque lint detection" do
154
+ Resque::Plugin.lint(Resque::Plugins::Clues::CompositePublisher)
155
+ end
156
+
157
+ it "should delegate enqueued event to children" do
158
+ verify_event_delegated_to_children :enqueued
159
+ publish_event_type :enqueued
160
+ end
161
+
162
+ it "should delegate dequeued event to children" do
163
+ verify_event_delegated_to_children :dequeued
164
+ publish_event_type :dequeued
165
+ end
166
+
167
+ it "should delegate destroyed event to children" do
168
+ verify_event_delegated_to_children :destroyed
169
+ publish_event_type :destroyed
170
+ end
171
+
172
+ it "should delegate perform_started event to children" do
173
+ verify_event_delegated_to_children :perform_started
174
+ publish_event_type :perform_started
175
+ end
176
+
177
+ it "should delegate perform_finished event to children" do
178
+ verify_event_delegated_to_children :perform_finished
179
+ publish_event_type :perform_finished
180
+ end
181
+
182
+ it "should delegate failed event to children" do
183
+ verify_event_delegated_to_children :failed
184
+ publish_event_type :failed
185
+ end
186
+
187
+ it "all children should be invoked when the first child throws an exception" do
188
+ @publisher[0].stub(:enqueued) {raise 'YOU SHALL NOT PASS'}
189
+ verify_event_delegated_to_children :enqueued
190
+ publish_event_type :enqueued
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,183 @@
1
+ require 'spec_helper'
2
+ require 'stringio'
3
+ require 'timeout'
4
+ require 'json'
5
+
6
+ # Setting $TESTING to true will cause Resque to not fork a child to perform the
7
+ # job, doing all the work within the test process and allowing us to test
8
+ # results for all aspects of the job performance.
9
+ $TESTING = true
10
+
11
+ class DummyWorker
12
+ @queue = :test_queue
13
+
14
+ def self.invoked?
15
+ @invoked
16
+ end
17
+
18
+ def self.reset!
19
+ @invoked = false
20
+ end
21
+
22
+ def self.perform(msg)
23
+ @invoked = true
24
+ end
25
+ end
26
+
27
+ class FailingDummyWorker
28
+ @queue = :test_queue
29
+
30
+ def self.perform(msg)
31
+ raise msg
32
+ end
33
+ end
34
+
35
+ describe 'end-to-end integration' do
36
+ before(:each) do
37
+ DummyWorker.reset!
38
+ @stream = StringIO.new
39
+ @worker = Resque::Worker.new(:test_queue)
40
+ #@worker.very_verbose = true
41
+ Resque::Plugins::Clues.event_publisher = Resque::Plugins::Clues::StreamPublisher.new(@stream)
42
+ end
43
+
44
+ def stream_size
45
+ @stream.rewind
46
+ @stream.readlines.size
47
+ end
48
+
49
+ def enqueue_then_verify(klass, *args, &block)
50
+ Resque.enqueue(klass, *args)
51
+ work_and_verify(&block)
52
+ end
53
+
54
+ def work
55
+ timeout(0.2) do
56
+ @worker.work(0.1)
57
+ end
58
+ end
59
+
60
+ def work_and_verify(&block)
61
+ work
62
+ @stream.rewind
63
+ block.call(@stream.readlines.map{|line| MultiJson.decode(line)})
64
+ end
65
+
66
+ def verify_event(event, type, klass, *args)
67
+ event["worker_class"].should == klass.to_s
68
+ event["args"].should == args
69
+ event["event_type"].should == type.to_s
70
+ event["timestamp"].should_not be_nil
71
+ event["metadata"]["event_hash"].should_not be_nil
72
+ event["metadata"]["hostname"].should == `hostname`.strip
73
+ event["metadata"]["process"].should == $$
74
+ yield(event) if block_given?
75
+ end
76
+
77
+ context "for job that finishes normally" do
78
+ it "should publish enqueued, dequeued, perform_started and perform_finished events" do
79
+ enqueue_then_verify(DummyWorker, 'test') do |events|
80
+ events.size.should == 4
81
+ verify_event(events[0], :enqueued, DummyWorker, "test")
82
+ verify_event(events[1], :dequeued, DummyWorker, "test") do |event|
83
+ event["metadata"]["time_in_queue"].should_not be_nil
84
+ end
85
+ verify_event(events[2], :perform_started, DummyWorker, "test")
86
+ verify_event(events[3], :perform_finished, DummyWorker, "test") do |event|
87
+ event["metadata"]["time_to_perform"].should_not be_nil
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ context "for job that fails" do
94
+ it "should publish enqueued, dequeued, perform_started and failed events" do
95
+ enqueue_then_verify(FailingDummyWorker, 'test') do |events|
96
+ events.size.should == 4
97
+ verify_event(events[0], :enqueued, FailingDummyWorker, "test")
98
+ verify_event(events[1], :dequeued, FailingDummyWorker, "test") do |event|
99
+ event["metadata"]["time_in_queue"].should_not be_nil
100
+ end
101
+ verify_event(events[2], :perform_started, FailingDummyWorker, "test")
102
+ verify_event(events[3], :failed, FailingDummyWorker, "test") do |event|
103
+ event["metadata"]["time_to_perform"].should_not be_nil
104
+ event["metadata"]["exception"].should == RuntimeError.to_s
105
+ event["metadata"]["message"].should == 'test'
106
+ event["metadata"]["backtrace"].should_not be_nil
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ context "for job enqueued prior to use of resque-clues gem" do
113
+ def enqueue_unpatched(worker, *args)
114
+ unpatch_resque
115
+ begin
116
+ Resque.enqueue(worker, *args)
117
+ ensure
118
+ repatch_resque
119
+ end
120
+ end
121
+
122
+ context "job that performs normally" do
123
+ before do
124
+ enqueue_unpatched(DummyWorker, "test")
125
+ end
126
+
127
+ it "should succeed without failures" do
128
+ work
129
+ DummyWorker.invoked?.should == true
130
+ Resque::Failure.all.should == nil
131
+ end
132
+ end
133
+
134
+ context "job failures" do
135
+ before do
136
+ enqueue_unpatched(FailingDummyWorker, "test")
137
+ end
138
+
139
+ it "should report failure normally" do
140
+ work
141
+ Resque::Failure.count.should == 1
142
+ end
143
+ end
144
+ end
145
+
146
+ context "where job enqueued with resque-clues gem but worker performing job is not" do
147
+ def unpatch_and_work
148
+ unpatch_resque
149
+ work
150
+ yield if block_given?
151
+ ensure
152
+ repatch_resque
153
+ end
154
+
155
+ context "for job that performs normally" do
156
+ before {Resque.enqueue DummyWorker, "test"}
157
+
158
+ it "should succeed without failures" do
159
+ unpatch_and_work {Resque::Failure.all.should == nil}
160
+ end
161
+ end
162
+
163
+ context "for job failures" do
164
+ before {Resque.enqueue FailingDummyWorker, "test"}
165
+
166
+ it "should report failure normally" do
167
+ unpatch_and_work {Resque::Failure.count.should == 1}
168
+ end
169
+ end
170
+ end
171
+
172
+ context "Resque internals assumptions" do
173
+ describe "Resque#push" do
174
+ it "should receive item with class and args as symbols" do
175
+ received_item = nil
176
+ Resque.stub(:push) {|queue, item| received_item = item}
177
+ Resque.enqueue(DummyWorker, 'test')
178
+ received_item[:class].should == "DummyWorker"
179
+ received_item[:args].should == ['test']
180
+ end
181
+ end
182
+ end
183
+ end