resque-job-tracking 0.0.1

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.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ cubbyhole.*
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'http://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in resque-job-tracking.gemspec
4
+ gemspec
5
+
6
+ gem 'json'
7
+
8
+ group :test do
9
+ gem 'cubbyhole'
10
+ end
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2011 Engine Yard
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,15 @@
1
+ A resque plugin for tracking jobs and their state (pending, running, failed) based on some originating entity.
2
+ This plugin relies heavily on resque-meta to store meta data on resque jobs.
3
+
4
+ Our use case is:
5
+
6
+ Account is a model in our database. Accounts do lots of things which trigger background jobs. We want to be able to see for a given account:
7
+ 1. What jobs are waiting to be run.
8
+ 2. What jobs are currently running.
9
+ 3. What jobs ran and had some exception in the past 24 hours.
10
+
11
+ You define how jobs are tracked by defining the "track" method on your job class (see the tests for an example)
12
+
13
+ You define how long meta data is kept around by defining expire_meta_in, expire_normal_meta_in, and/or expire_failures_meta_in on your job class (see tests for an example)
14
+
15
+ You can access the list of pending, running, and failed jobs using the Resque::Plugins::JobTracking methods: pending_jobs, running_jobs, failed_jobs (again, see tests)
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,23 @@
1
+ #TODO: send a PULL request with this change!!!
2
+ Resque::Plugins::Meta::Metadata.class_eval do
3
+
4
+ def initialize(data_hash)
5
+ data_hash['enqueued_at'] ||= to_time_format_str(Time.now)
6
+ @data = data_hash
7
+ @meta_id = data_hash['meta_id'].dup
8
+ @enqueued_at = from_time_format_str('enqueued_at')
9
+ @job_class = data_hash['job_class']
10
+ if @job_class.is_a?(String)
11
+ @job_class = Resque.constantize(data_hash['job_class'])
12
+ else
13
+ data_hash['job_class'] = @job_class.to_s
14
+ end
15
+ @expire_in = data_hash["expire_in"] || @job_class.expire_meta_in || 0
16
+ end
17
+
18
+ def expire_in=(val)
19
+ @expire_in = val
20
+ data["expire_in"] = val
21
+ end
22
+
23
+ end
@@ -0,0 +1,7 @@
1
+ module Resque
2
+ module Plugins
3
+ module JobTracking
4
+ VERSION = "0.0.1"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,78 @@
1
+ require 'resque/plugins/meta'
2
+ require 'resque/plugins/job_tracking/meta_ext'
3
+
4
+ module Resque
5
+ module Plugins
6
+ module JobTracking
7
+ def self.extended(mod)
8
+ mod.extend(Resque::Plugins::Meta)
9
+ end
10
+
11
+ def self.pending_jobs(identifier)
12
+ Resque.redis.smembers("#{identifier}:pending") || []
13
+ end
14
+
15
+ def self.running_jobs(identifier)
16
+ Resque.redis.smembers("#{identifier}:running") || []
17
+ end
18
+
19
+ def self.failed_jobs(identifier)
20
+ Resque.redis.smembers("#{identifier}:failed") || []
21
+ end
22
+
23
+ def expire_normal_meta_in
24
+ expire_meta_in
25
+ end
26
+
27
+ def expire_failures_meta_in
28
+ expire_meta_in + (24 * 60 * 60)
29
+ end
30
+
31
+ def before_enqueue_job_tracking(meta_id, *jobargs)
32
+ if self.respond_to?(:track)
33
+ identifiers = track(*jobargs)
34
+ identifiers.each do |ident|
35
+ Resque.redis.sadd("#{ident}:pending", meta_id)
36
+ end
37
+ meta = get_meta(meta_id)
38
+ meta["job_args"] = jobargs
39
+ meta.save
40
+ end
41
+ end
42
+
43
+ def around_perform_job_tracking(meta_id, *jobargs)
44
+ if self.respond_to?(:track)
45
+ identifiers = track(*jobargs)
46
+ identifiers.each do |ident|
47
+ Resque.redis.srem("#{ident}:pending", meta_id)
48
+ Resque.redis.sadd("#{ident}:running", meta_id)
49
+ end
50
+ begin
51
+ to_return = yield
52
+ meta = get_meta(meta_id)
53
+ meta.expire_in = expire_normal_meta_in
54
+ meta.save
55
+ to_return
56
+ rescue => e
57
+ identifiers.each do |ident|
58
+ Resque.redis.sadd("#{ident}:failed", meta_id)
59
+ end
60
+ meta = get_meta(meta_id)
61
+ meta.expire_in = expire_failures_meta_in
62
+ meta['exception_message'] = e.message
63
+ meta['exception_backtrace'] = e.backtrace
64
+ meta.save
65
+ raise e
66
+ ensure
67
+ identifiers.each do |ident|
68
+ Resque.redis.srem("#{ident}:running", meta_id)
69
+ end
70
+ end
71
+ else
72
+ yield
73
+ end
74
+ end
75
+
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/resque/plugins/job_tracking/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Jacob Burkhart"]
6
+ gem.email = ["jacob@engineyard.com"]
7
+ gem.description = %q{A resque plugin for tracking jobs and their state (pending, running, failed) based on some originating entity}
8
+ gem.summary = %q{A resque plugin for tracking jobs and their state (pending, running, failed) based on some originating entity}
9
+ gem.homepage = ""
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "resque-job-tracking"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Resque::Plugins::JobTracking::VERSION
17
+
18
+ gem.add_dependency 'resque', '>= 1.8.0'
19
+ gem.add_dependency 'resque-meta', '>= 1.0.0'
20
+
21
+ gem.add_development_dependency 'rspec'
22
+ end
@@ -0,0 +1,48 @@
1
+ require 'resque'
2
+ require 'resque/plugins/job_tracking'
3
+
4
+ require 'spec_helper'
5
+
6
+ class WhatHappened
7
+ require 'tempfile'
8
+ def self.reset!
9
+ @what_happened = Tempfile.new("what_happened")
10
+ end
11
+ def self.what_happened
12
+ File.read(@what_happened.path)
13
+ end
14
+ def self.record(*event)
15
+ @what_happened.write(event.to_s)
16
+ @what_happened.flush
17
+ end
18
+ end
19
+
20
+ class BasicJob
21
+ extend Resque::Plugins::JobTracking
22
+ @queue = :test
23
+
24
+ def self.perform(*args)
25
+ begin
26
+ WhatHappened.record(self, args)
27
+ rescue => e
28
+ puts e.inspect
29
+ puts e.backtrace.join("\n")
30
+ end
31
+ end
32
+ end
33
+
34
+
35
+ describe "the basics" do
36
+ before do
37
+ WhatHappened.reset!
38
+ Resque.redis.flushall
39
+ end
40
+
41
+ it "works" do
42
+ meta = BasicJob.enqueue('foo', 'bar')
43
+ worker = Resque::Worker.new(:test)
44
+ worker.work(0)
45
+ meta = BasicJob.get_meta(meta.meta_id)
46
+ WhatHappened.what_happened.should == "BasicJob#{meta.meta_id}foobar"
47
+ end
48
+ end
@@ -0,0 +1 @@
1
+ Dir[File.expand_path('../support/*.rb', __FILE__)].each{|f| require f}
@@ -0,0 +1,50 @@
1
+ module WorkerSupport
2
+
3
+ def work(worker_count = 5)
4
+ @workers = []
5
+ worker_count.times do
6
+ @workers << Process.fork do
7
+ begin
8
+ Resque.redis.client.reconnect
9
+ Resque::Worker.new(:test).work(1)
10
+ rescue => e
11
+ puts e.inspect
12
+ puts e.backtrace.join("\n")
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ def finished?
19
+ Resque.redis.keys("meta*").each do |key|
20
+ meta = Resque::Plugins::Meta.get_meta(key.split(":").last)
21
+ if meta.finished?
22
+ # puts "finished #{meta['job_class']}"
23
+ else
24
+ return false
25
+ # puts "still running #{meta['job_class']}"
26
+ end
27
+ end
28
+ return true
29
+ end
30
+
31
+ def wait_until_finished
32
+ while(!finished?)
33
+ sleep(0.5)
34
+ end
35
+ end
36
+
37
+ def work_until_finished
38
+ work
39
+ wait_until_finished
40
+ end
41
+
42
+ def cleanup
43
+ if @workers
44
+ @workers.each do |p|
45
+ Process.kill(9, p)
46
+ end
47
+ end
48
+ end
49
+
50
+ end
@@ -0,0 +1,156 @@
1
+ require 'resque'
2
+ require 'resque/plugins/job_tracking'
3
+
4
+ require 'spec_helper'
5
+
6
+ class WhatHappened
7
+ require 'tempfile'
8
+ def self.reset!
9
+ @what_happened = Tempfile.new("what_happened")
10
+ end
11
+ def self.what_happened
12
+ File.read(@what_happened.path)
13
+ end
14
+ def self.record(*event)
15
+ @what_happened.write(event.to_s)
16
+ @what_happened.flush
17
+ end
18
+ end
19
+
20
+ class BaseJobWithPerform
21
+ extend Resque::Plugins::JobTracking
22
+ def self.queue
23
+ :test
24
+ end
25
+
26
+ def self.expire_meta_in
27
+ 1
28
+ end
29
+
30
+ def self.perform(meta_id, *args)
31
+ self.new.perform(*args)
32
+ end
33
+
34
+ end
35
+
36
+ require 'cubbyhole/base'
37
+ class Account < Cubbyhole::Base
38
+
39
+ def pending_jobs
40
+ Resque::Plugins::JobTracking.pending_jobs(job_tracking_identifier)
41
+ end
42
+
43
+ def running_jobs
44
+ Resque::Plugins::JobTracking.running_jobs(job_tracking_identifier)
45
+ end
46
+
47
+ def failed_jobs
48
+ Resque::Plugins::JobTracking.failed_jobs(job_tracking_identifier)
49
+ end
50
+
51
+ def job_tracking_identifier
52
+ "account#{self.id}"
53
+ end
54
+
55
+ end
56
+
57
+
58
+ class TypicalProblemJob < BaseJobWithPerform
59
+
60
+ def self.track(account_id, something)
61
+ [Account.get(account_id).job_tracking_identifier]
62
+ end
63
+
64
+ def perform(account_id, something)
65
+ if something == 'fail_immediate'
66
+ raise "failing immediate"
67
+ end
68
+ sleep(2)
69
+ if something == 'fail_please'
70
+ raise "i fail now"
71
+ end
72
+ end
73
+
74
+ end
75
+
76
+
77
+ describe TypicalProblemJob do
78
+ include WorkerSupport
79
+
80
+ before do
81
+ WhatHappened.reset!
82
+ Resque.redis.flushall
83
+ end
84
+ after do
85
+ cleanup
86
+ end
87
+
88
+ it "should keep meta data for failed jobs" do
89
+ account = Account.create
90
+ TypicalProblemJob.enqueue(account.id, 'fail_please')
91
+ account.pending_jobs.size.should eq 1
92
+ account.running_jobs.size.should eq 0
93
+ account.failed_jobs.size.should eq 0
94
+ meta_id = account.pending_jobs.first
95
+ TypicalProblemJob.get_meta(meta_id).should_not be_nil
96
+ work(1)
97
+ sleep(1)
98
+ account.pending_jobs.size.should eq 0
99
+ account.running_jobs.size.should eq 1
100
+ account.failed_jobs.size.should eq 0
101
+ account.running_jobs.first.should eq meta_id
102
+ TypicalProblemJob.get_meta(meta_id).should_not be_nil
103
+ wait_until_finished
104
+ account.pending_jobs.size.should eq 0
105
+ account.running_jobs.size.should eq 0
106
+ account.failed_jobs.size.should eq 1
107
+ account.failed_jobs.first.should eq meta_id
108
+ meta = TypicalProblemJob.get_meta(meta_id)
109
+ meta.should_not be_nil
110
+ end
111
+
112
+ it "should lose meta data for non-failing jobs" do
113
+ account = Account.create
114
+ TypicalProblemJob.enqueue(account.id, 'pass_please')
115
+ account.pending_jobs.size.should eq 1
116
+ account.running_jobs.size.should eq 0
117
+ account.failed_jobs.size.should eq 0
118
+ meta_id = account.pending_jobs.first
119
+ TypicalProblemJob.get_meta(meta_id).should_not be_nil
120
+ work(1)
121
+ sleep(1)
122
+ account.pending_jobs.size.should eq 0
123
+ account.running_jobs.size.should eq 1
124
+ account.failed_jobs.size.should eq 0
125
+ account.running_jobs.first.should eq meta_id
126
+ TypicalProblemJob.get_meta(meta_id).should_not be_nil
127
+ wait_until_finished
128
+ account.pending_jobs.size.should eq 0
129
+ account.running_jobs.size.should eq 0
130
+ account.failed_jobs.size.should eq 0
131
+ sleep(2)
132
+ TypicalProblemJob.get_meta(meta_id).should be_nil
133
+ end
134
+
135
+ it "should store the exception in meta data" do
136
+ account = Account.create
137
+ TypicalProblemJob.enqueue(account.id, 'fail_immediate')
138
+ work_until_finished
139
+ account.failed_jobs.size.should eq 1
140
+ meta_id = account.failed_jobs.first
141
+ meta_data = TypicalProblemJob.get_meta(meta_id)
142
+ meta_data['exception_message'].should eq "failing immediate"
143
+ meta_data['exception_backtrace'].should_not be_nil
144
+ end
145
+
146
+ it "should store the job class and args in meta data" do
147
+ account = Account.create
148
+ TypicalProblemJob.enqueue(account.id, 'dontcare')
149
+ account.pending_jobs.size.should eq 1
150
+ meta_id = account.pending_jobs.first
151
+ meta_data = TypicalProblemJob.get_meta(meta_id)
152
+ meta_data['job_class'].should eq "TypicalProblemJob"
153
+ meta_data['job_args'].should eq [account.id, 'dontcare']
154
+ end
155
+
156
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: resque-job-tracking
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Jacob Burkhart
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-10-13 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: resque
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 55
29
+ segments:
30
+ - 1
31
+ - 8
32
+ - 0
33
+ version: 1.8.0
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: resque-meta
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 23
45
+ segments:
46
+ - 1
47
+ - 0
48
+ - 0
49
+ version: 1.0.0
50
+ type: :runtime
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: rspec
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ hash: 3
61
+ segments:
62
+ - 0
63
+ version: "0"
64
+ type: :development
65
+ version_requirements: *id003
66
+ description: A resque plugin for tracking jobs and their state (pending, running, failed) based on some originating entity
67
+ email:
68
+ - jacob@engineyard.com
69
+ executables: []
70
+
71
+ extensions: []
72
+
73
+ extra_rdoc_files: []
74
+
75
+ files:
76
+ - .gitignore
77
+ - Gemfile
78
+ - LICENSE
79
+ - README
80
+ - Rakefile
81
+ - lib/resque/plugins/job_tracking.rb
82
+ - lib/resque/plugins/job_tracking/meta_ext.rb
83
+ - lib/resque/plugins/job_tracking/version.rb
84
+ - resque-job-tracking.gemspec
85
+ - spec/basic_spec.rb
86
+ - spec/spec_helper.rb
87
+ - spec/support/worker_support.rb
88
+ - spec/tracking_spec.rb
89
+ homepage: ""
90
+ licenses: []
91
+
92
+ post_install_message:
93
+ rdoc_options: []
94
+
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ hash: 3
103
+ segments:
104
+ - 0
105
+ version: "0"
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ hash: 3
112
+ segments:
113
+ - 0
114
+ version: "0"
115
+ requirements: []
116
+
117
+ rubyforge_project:
118
+ rubygems_version: 1.8.10
119
+ signing_key:
120
+ specification_version: 3
121
+ summary: A resque plugin for tracking jobs and their state (pending, running, failed) based on some originating entity
122
+ test_files:
123
+ - spec/basic_spec.rb
124
+ - spec/spec_helper.rb
125
+ - spec/support/worker_support.rb
126
+ - spec/tracking_spec.rb