resque-clues 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,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