resque_stuck_queue 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.
- checksums.yaml +15 -0
- data/.gitignore +1 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +61 -0
- data/LICENSE.txt +22 -0
- data/README.md +61 -0
- data/Rakefile +18 -0
- data/THOUGHTS +21 -0
- data/lib/resque_stuck_queue/version.rb +5 -0
- data/lib/resque_stuck_queue.rb +157 -0
- data/resque_stuck_queue.gemspec +23 -0
- data/test/resque/refresh_latest_timestamp.rb +6 -0
- data/test/resque/set_redis_key.rb +8 -0
- data/test/test_integration.rb +83 -0
- data/test/test_resque_stuck_queue.rb +105 -0
- metadata +91 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
Zjg2N2FjNjcwNjA2MTZiNTQ1NGJjZjQwZmNiZjI5YzcyYjcxZDdmYw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ZDAxNGUwYzUxYTg5NmY2N2Q0OGQ0ZTI0MzQ2MzVlNmJiNDJjY2I3OA==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MWMxODVkOTM5NGJlMGQwZWQ5YWU1YTNjN2Q0NWM1NGUzYmExZjBmZjFkN2Yx
|
10
|
+
MjA5MGIzNDliY2YwYzU5NGEwNTY4NmYxMDNhZGY5MWNkYjU0ZGM1MDhjZmNk
|
11
|
+
ZGU4YzU1NGM2ZTc5Y2Y0OTIyYWU0NGE0OTQwNmM1ZjdmZTZhZWM=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
M2VmNmZmY2YwMDExNzdlYzZjM2ZhMWUxZTM4NDViNjBjYjc2NmYzOWI1NDkz
|
14
|
+
MjRjNmZlNmM1MjA0MGJhYzAzNWUyNWIwMjVhMGVjZTFhNjM4MjkzMDA0NjQx
|
15
|
+
Yjg2M2VmZDU3ZWM0MWUyODFlNTI1YWVmOWQ4NjlhNzNmM2Y0NWU=
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*gem
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
coderay (1.1.0)
|
5
|
+
m (1.3.2)
|
6
|
+
method_source (>= 0.6.7)
|
7
|
+
rake (>= 0.9.2.2)
|
8
|
+
metaclass (0.0.1)
|
9
|
+
method_source (0.8.2)
|
10
|
+
minitest (5.2.0)
|
11
|
+
mocha (0.14.0)
|
12
|
+
metaclass (~> 0.0.1)
|
13
|
+
mono_logger (1.1.0)
|
14
|
+
multi_json (1.8.2)
|
15
|
+
pry (0.9.12.4)
|
16
|
+
coderay (~> 1.0)
|
17
|
+
method_source (~> 0.8)
|
18
|
+
slop (~> 3.4)
|
19
|
+
rack (1.5.2)
|
20
|
+
rack-protection (1.5.1)
|
21
|
+
rack
|
22
|
+
rake (10.1.0)
|
23
|
+
redis (3.0.6)
|
24
|
+
redis-namespace (1.4.1)
|
25
|
+
redis (~> 3.0.4)
|
26
|
+
resque (1.25.1)
|
27
|
+
mono_logger (~> 1.0)
|
28
|
+
multi_json (~> 1.0)
|
29
|
+
redis-namespace (~> 1.2)
|
30
|
+
sinatra (>= 0.9.2)
|
31
|
+
vegas (~> 0.1.2)
|
32
|
+
resque-mock (0.1.1)
|
33
|
+
resque
|
34
|
+
resque-scheduler (2.0.1)
|
35
|
+
redis (>= 2.0.1)
|
36
|
+
resque (>= 1.20.0)
|
37
|
+
rufus-scheduler
|
38
|
+
rufus-scheduler (2.0.19)
|
39
|
+
tzinfo (>= 0.3.23)
|
40
|
+
sinatra (1.4.4)
|
41
|
+
rack (~> 1.4)
|
42
|
+
rack-protection (~> 1.4)
|
43
|
+
tilt (~> 1.3, >= 1.3.4)
|
44
|
+
slop (3.4.7)
|
45
|
+
tilt (1.4.1)
|
46
|
+
tzinfo (0.3.38)
|
47
|
+
vegas (0.1.11)
|
48
|
+
rack (>= 1.0.0)
|
49
|
+
|
50
|
+
PLATFORMS
|
51
|
+
ruby
|
52
|
+
|
53
|
+
DEPENDENCIES
|
54
|
+
m
|
55
|
+
minitest
|
56
|
+
mocha
|
57
|
+
pry
|
58
|
+
rake
|
59
|
+
resque
|
60
|
+
resque-mock
|
61
|
+
resque-scheduler
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Shai Rosenfeld
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
## Resque stuck queue
|
2
|
+
|
3
|
+
Ever run into that? Sucks, eh?
|
4
|
+
|
5
|
+
This should enable a way to fire some handler when jobs aren't occurring within a certain timeframe.
|
6
|
+
|
7
|
+
## How it works
|
8
|
+
|
9
|
+
When you call `start` you are essentially starting two threads that will continiously run until `stop` is called or until the process shuts down.
|
10
|
+
|
11
|
+
One thread is responsible for pushing a 'heartbeat' job to resque which will essentially refresh a specific key in redis every time that job is processed.
|
12
|
+
|
13
|
+
The other thread is a continious loop that will check redis (bypassing resque) for that key and check what the latest time the hearbeat job successfully updated that key.
|
14
|
+
|
15
|
+
It will trigger a pre-defined proc (see below) if the last time the hearbeat job updated that key is older than the trigger_timeout setting (see below).
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
Add this to wherever you're setting up resque (config/initializers or wherever).
|
20
|
+
|
21
|
+
Configure it first:
|
22
|
+
|
23
|
+
<pre>
|
24
|
+
# how often to push that 'heartbeat' job to refresh the latest time it worked.
|
25
|
+
Resque::StuckQueue.config[:heartbeat] = 5.minutes
|
26
|
+
|
27
|
+
# since there is an realistic and acceptable lag for job queues, set this to how much you're
|
28
|
+
# willing to accept between the current time and when the last hearbeat job went through.
|
29
|
+
#
|
30
|
+
# obviously, take the heartbeat into consideration when setting this
|
31
|
+
Resque::StuckQueue.config[:trigger_timeout] = 10.hours
|
32
|
+
|
33
|
+
# what gets triggered when resque-stuck-queue will detect the latest heartbeat is older than the trigger_timeout time set above.
|
34
|
+
Resque::StuckQueue.config[:handler] = proc { send_email }
|
35
|
+
|
36
|
+
# optional, in case you want to set your own name for the key that will be used as the last good hearbeat time
|
37
|
+
Resque::StuckQueue.config[:global_key] = "name-the-refresh-key-as-you-please"
|
38
|
+
|
39
|
+
# optional, if you want the resque-stuck-queue threads to explicitly raise, default is false
|
40
|
+
Resque::StuckQueue.config[:abort_on_exception] = true
|
41
|
+
</pre>
|
42
|
+
|
43
|
+
Then start it:
|
44
|
+
|
45
|
+
<pre>
|
46
|
+
Resque::StuckQueue.start # blocking
|
47
|
+
Resque::StuckQueue.start_in_background # sugar for Thread.new { Resque::StuckQueue.start }
|
48
|
+
</pre>
|
49
|
+
|
50
|
+
Stopping it consists of the same idea:
|
51
|
+
|
52
|
+
<pre>
|
53
|
+
Resque::StuckQueue.stop # this will block until the threads end their current iteration
|
54
|
+
Resque::StuckQueue.force_stop! # force kill those threads and let's move on
|
55
|
+
</pre>
|
56
|
+
|
57
|
+
## Tests
|
58
|
+
|
59
|
+
Run the tests:
|
60
|
+
|
61
|
+
`bundle; bundle exec rake`
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
|
3
|
+
task :default => :test
|
4
|
+
Rake::TestTask.new do |t|
|
5
|
+
t.pattern = "test/test_*rb"
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'resque/tasks'
|
9
|
+
|
10
|
+
task :'resque:setup' do
|
11
|
+
# https://github.com/resque/resque/issues/773
|
12
|
+
# have the jobs loaded in memory
|
13
|
+
Dir["./test/resque/*.rb"].each {|file| require file}
|
14
|
+
end
|
15
|
+
|
16
|
+
require 'resque_scheduler/tasks'
|
17
|
+
task "resque:scheduler_setup"
|
18
|
+
|
data/THOUGHTS
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
other resources:
|
2
|
+
|
3
|
+
http://vitobotta.com/resque-automatically-kill-stuck-workers-retry-failed-jobs/#sthash.oQsaNeb5.dpbs
|
4
|
+
http://stackoverflow.com/questions/10757758/find-out-if-a-resque-job-is-still-running-and-kill-it-if-its-stuck
|
5
|
+
|
6
|
+
heartbeat?
|
7
|
+
|
8
|
+
redis scenario
|
9
|
+
different processes (cant share state in Resque memory etc.)
|
10
|
+
different boxes! (redis is shared state)
|
11
|
+
if this is included in servers, you have clusters os unicorn/mongrel etc,
|
12
|
+
ensure it works across multiple running instances of this lib?
|
13
|
+
|
14
|
+
Thing.setup_checker_thread
|
15
|
+
Thing.enqueue_repeating_refresh_job
|
16
|
+
|
17
|
+
reproduce the error and integrate test like that
|
18
|
+
|
19
|
+
## TODOS
|
20
|
+
|
21
|
+
verify @config options, raise if no handler, etc.
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require "resque_stuck_queue/version"
|
2
|
+
|
3
|
+
# TODO move this require into a configurable?
|
4
|
+
require 'resque'
|
5
|
+
|
6
|
+
module Resque
|
7
|
+
module StuckQueue
|
8
|
+
|
9
|
+
GLOBAL_KEY = "resque-stuck-queue"
|
10
|
+
VERIFIED_KEY = "resque-stuck-queue-ran-once"
|
11
|
+
HEARTBEAT = 60 * 60 # check/refresh every hour
|
12
|
+
TRIGGER_TIMEOUT = 5 * 60 * 60 # warn/trigger 5 hours
|
13
|
+
HANDLER = proc { $stderr.puts("Shit gone bad with them queues.") }
|
14
|
+
|
15
|
+
class << self
|
16
|
+
|
17
|
+
attr_accessor :config
|
18
|
+
|
19
|
+
# # how often we refresh the key
|
20
|
+
# :heartbeat = 5 * 60
|
21
|
+
#
|
22
|
+
# # this could just be :heartbeat but it's possible there's an acceptable lag/bottleneck
|
23
|
+
# # in the queue that we want to allow to be before we think it's bad.
|
24
|
+
# :trigger_timeout = 10 * 60
|
25
|
+
#
|
26
|
+
# # The global key that will be used to check the latest time
|
27
|
+
# :global_key = "resque-stuck-queue"
|
28
|
+
#
|
29
|
+
# # for threads involved here. default is false
|
30
|
+
# :abort_on_exception
|
31
|
+
#
|
32
|
+
# # default handler
|
33
|
+
# config[:handler] = proc { send_mail }
|
34
|
+
def config
|
35
|
+
@config ||= {}
|
36
|
+
end
|
37
|
+
|
38
|
+
def start_in_background
|
39
|
+
Thread.new do
|
40
|
+
self.start
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def stop_in_background
|
45
|
+
Thread.new do
|
46
|
+
self.start
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# call this after setting config. once started you should't be allowed to modify it
|
51
|
+
def start
|
52
|
+
@running = true
|
53
|
+
@stopped = false
|
54
|
+
@threads = []
|
55
|
+
config.freeze
|
56
|
+
|
57
|
+
mark_first_use
|
58
|
+
|
59
|
+
Thread.abort_on_exception = config[:abort_on_exception]
|
60
|
+
|
61
|
+
enqueue_repeating_refresh_job
|
62
|
+
setup_checker_thread
|
63
|
+
|
64
|
+
# fo-eva.
|
65
|
+
@threads.map(&:join)
|
66
|
+
|
67
|
+
@stopped = true
|
68
|
+
end
|
69
|
+
|
70
|
+
# for tests
|
71
|
+
def stop
|
72
|
+
@config = config.dup #unfreeze
|
73
|
+
@running = false
|
74
|
+
|
75
|
+
# wait for clean thread shutdown
|
76
|
+
while @stopped == false
|
77
|
+
sleep 1
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def force_stop!
|
82
|
+
@threads.map(&:kill)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def enqueue_repeating_refresh_job
|
88
|
+
@threads << Thread.new do
|
89
|
+
while @running
|
90
|
+
wait_for_it
|
91
|
+
# we want to go through resque jobs, because that's what we're trying to test here:
|
92
|
+
# ensure that jobs get executed and the time is updated!
|
93
|
+
#
|
94
|
+
# TODO REDIS 2.0 compat
|
95
|
+
Resque.enqueue(RefreshLatestTimestamp, global_key)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def setup_checker_thread
|
101
|
+
@threads << Thread.new do
|
102
|
+
while @running
|
103
|
+
wait_for_it
|
104
|
+
if Time.now.to_i - last_time_worked > max_wait_time
|
105
|
+
trigger_handler
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def last_time_worked
|
112
|
+
time_set = read_from_redis
|
113
|
+
if has_been_used? && time_set.nil?
|
114
|
+
# if the first job ran, the redis key should always be set
|
115
|
+
# possible cases are (1) redis data wonky (2) resque jobs don't get run
|
116
|
+
trigger_handler
|
117
|
+
end
|
118
|
+
(time_set ? time_set : Time.now).to_i # don't trigger again if time is nil
|
119
|
+
end
|
120
|
+
|
121
|
+
def trigger_handler
|
122
|
+
(config[:handler] || HANDLER).call
|
123
|
+
end
|
124
|
+
|
125
|
+
def read_from_redis
|
126
|
+
Resque.redis.get(global_key)
|
127
|
+
end
|
128
|
+
|
129
|
+
def has_been_used?
|
130
|
+
Resque.redis.get(VERIFIED_KEY)
|
131
|
+
end
|
132
|
+
|
133
|
+
def mark_first_use
|
134
|
+
Resque.redis.set(VERIFIED_KEY, "true")
|
135
|
+
end
|
136
|
+
|
137
|
+
def wait_for_it
|
138
|
+
sleep config[:heartbeat] || HEARTBEAT
|
139
|
+
end
|
140
|
+
|
141
|
+
def global_key
|
142
|
+
config[:global_key] || GLOBAL_KEY
|
143
|
+
end
|
144
|
+
|
145
|
+
def max_wait_time
|
146
|
+
config[:trigger_timeout] || TRIGGER_TIMEOUT
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
class RefreshLatestTimestamp
|
153
|
+
@queue = :app
|
154
|
+
def self.perform(timestamp_key)
|
155
|
+
Resque.redis.set(timestamp_key, Time.now.to_i)
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'resque_stuck_queue/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "resque_stuck_queue"
|
8
|
+
spec.version = Resque::StuckQueue::VERSION
|
9
|
+
spec.authors = ["Shai Rosenfeld"]
|
10
|
+
spec.email = ["srosenfeld@engineyard.com"]
|
11
|
+
spec.summary = %q{fire a handler when your queues are wonky}
|
12
|
+
spec.description = %q{where the wild things are. err, when resque gets stuck}
|
13
|
+
spec.homepage = "https://github.com/shaiguitar/resque_stuck_queue/"
|
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_development_dependency "bundler", "~> 1.5"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'minitest'
|
2
|
+
require "minitest/autorun"
|
3
|
+
require 'pry'
|
4
|
+
|
5
|
+
$:.unshift(".")
|
6
|
+
require 'resque_stuck_queue'
|
7
|
+
require File.join(File.expand_path(File.dirname(__FILE__)), "resque", "set_redis_key")
|
8
|
+
require File.join(File.expand_path(File.dirname(__FILE__)), "resque", "refresh_latest_timestamp")
|
9
|
+
|
10
|
+
class TestIntegration < Minitest::Test
|
11
|
+
|
12
|
+
# TODODS there's a better way to do this.
|
13
|
+
#
|
14
|
+
#
|
15
|
+
# run test with VVERBOSE=1 DEBUG=1 for more output
|
16
|
+
#
|
17
|
+
#
|
18
|
+
# => sleeping suckc
|
19
|
+
# => resque sleeps 5 between checking enqueed jobs, can be configurable?
|
20
|
+
#
|
21
|
+
# cleanup processes correctly?
|
22
|
+
# ps aux |grep resqu |awk '{print $2}' | xargs kill
|
23
|
+
|
24
|
+
def setup
|
25
|
+
end
|
26
|
+
|
27
|
+
def teardown
|
28
|
+
Process.kill("SIGQUIT", @resque_pid)
|
29
|
+
Resque::StuckQueue.stop
|
30
|
+
Process.waitpid(@resque_pid)
|
31
|
+
end
|
32
|
+
|
33
|
+
def run_resque
|
34
|
+
pid = fork { exec("QUEUE=* bundle exec rake --trace resque:work") }
|
35
|
+
sleep 3 # wait for resque to boot up
|
36
|
+
pid
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_resque_enqueues_a_job_does_not_trigger
|
40
|
+
|
41
|
+
puts '1'
|
42
|
+
Resque::StuckQueue.config[:trigger_timeout] = 100 # wait a while so we don't trigger
|
43
|
+
Resque::StuckQueue.config[:heartbeat] = 2
|
44
|
+
@triggered = false
|
45
|
+
Resque::StuckQueue.config[:handler] = proc { @triggered = true }
|
46
|
+
Thread.new { Resque::StuckQueue.start }
|
47
|
+
|
48
|
+
# job gets enqueued successfully
|
49
|
+
@resque_pid = run_resque
|
50
|
+
Resque.redis.del(SetRedisKey::NAME)
|
51
|
+
Resque.enqueue(SetRedisKey)
|
52
|
+
sleep 6 # let resque pick up the job
|
53
|
+
assert_equal Resque.redis.get(SetRedisKey::NAME), "1"
|
54
|
+
|
55
|
+
# check handler did not get called
|
56
|
+
assert_equal @triggered, false
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_resque_does_not_enqueues_a_job_does_trigger
|
60
|
+
|
61
|
+
puts '2'
|
62
|
+
Resque::StuckQueue.config[:trigger_timeout] = 2 # won't allow waiting too much and will complain (eg trigger) sooner than later
|
63
|
+
Resque::StuckQueue.config[:heartbeat] = 1
|
64
|
+
@triggered = false
|
65
|
+
Resque::StuckQueue.config[:handler] = proc { @triggered = true }
|
66
|
+
Thread.new { Resque::StuckQueue.start }
|
67
|
+
|
68
|
+
# job gets enqueued successfully
|
69
|
+
@resque_pid = run_resque
|
70
|
+
Resque.redis.del(SetRedisKey::NAME)
|
71
|
+
Process.kill("SIGSTOP", @resque_pid) # jic, do not process jobs so we definitely trigger
|
72
|
+
Resque.enqueue(SetRedisKey)
|
73
|
+
assert_equal Resque.redis.get(SetRedisKey::NAME), nil
|
74
|
+
|
75
|
+
# check handler did get called
|
76
|
+
assert_equal @triggered, true
|
77
|
+
|
78
|
+
# unstick the process so we can kill it in teardown
|
79
|
+
Process.kill("SIGCONT", @resque_pid)
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'minitest'
|
2
|
+
require "minitest/autorun"
|
3
|
+
require 'mocha'
|
4
|
+
|
5
|
+
require 'resque/mock'
|
6
|
+
|
7
|
+
$:.unshift(".")
|
8
|
+
require 'resque_stuck_queue'
|
9
|
+
|
10
|
+
class TestResqueStuckQueue < Minitest::Test
|
11
|
+
|
12
|
+
def teardown
|
13
|
+
puts 'teardown'
|
14
|
+
Resque::StuckQueue.unstub(:has_been_used?)
|
15
|
+
Resque::StuckQueue.unstub(:read_from_redis)
|
16
|
+
end
|
17
|
+
|
18
|
+
def setup
|
19
|
+
puts 'setup'
|
20
|
+
# clean previous test runs
|
21
|
+
Resque.redis.flushall
|
22
|
+
Resque.mock!
|
23
|
+
Resque::StuckQueue.config[:heartbeat] = 1 # seconds
|
24
|
+
Resque::StuckQueue.config[:trigger_timeout] = 2
|
25
|
+
Resque::StuckQueue.config[:abort_on_exception] = true
|
26
|
+
end
|
27
|
+
|
28
|
+
# usually the key will be set from previous runs since it will persist (redis) between deploys etc.
|
29
|
+
# so you shouldn't be running into this scenario (nil key) other than
|
30
|
+
# 0) test setup clearing out this key
|
31
|
+
# 1) the VERY first time you use this lib when it first gets set.
|
32
|
+
# 2) redis gets wiped out
|
33
|
+
# 3) resque jobs never get run!
|
34
|
+
# this has the unfortunate meaning that if no jobs are *ever* enqueued, this lib won't catch that problem.
|
35
|
+
# so we split the funcationaliy to raise if no key is there, unless it's the first time it's being used since being started.
|
36
|
+
def test_thread_does_not_trigger_when_no_key_exists_on_first_use
|
37
|
+
puts '1'
|
38
|
+
|
39
|
+
# lib never ran, and key is not there
|
40
|
+
Resque::StuckQueue.stubs(:has_been_used?).returns(nil)
|
41
|
+
Resque::StuckQueue.stubs(:read_from_redis).returns(nil)
|
42
|
+
@triggered = false
|
43
|
+
Resque::StuckQueue.config[:handler] = proc { @triggered = true }
|
44
|
+
start_and_stop_loops_after(2)
|
45
|
+
assert_equal false, @triggered # "handler should not be called"
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_thread_does_trigger_when_no_key_exists_on_any_other_use
|
49
|
+
|
50
|
+
puts '2'
|
51
|
+
# lib already ran, but key is not there
|
52
|
+
Resque::StuckQueue.stubs(:has_been_used?).returns(true)
|
53
|
+
Resque::StuckQueue.stubs(:read_from_redis).returns(nil)
|
54
|
+
|
55
|
+
@triggered = false
|
56
|
+
Resque::StuckQueue.config[:handler] = proc { @triggered = true }
|
57
|
+
start_and_stop_loops_after(2)
|
58
|
+
assert_equal true, @triggered # "handler should be called"
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_configure_global_key
|
62
|
+
puts '3'
|
63
|
+
assert_nil Resque.redis.get("it-is-configurable"), "global key should not be set"
|
64
|
+
Resque::StuckQueue.config[:global_key] = "it-is-configurable"
|
65
|
+
start_and_stop_loops_after(2)
|
66
|
+
refute_nil Resque.redis.get("it-is-configurable"), "global key should be set"
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_it_sets_a_verified_key_to_indicate_first_use
|
70
|
+
puts '4'
|
71
|
+
assert_nil Resque.redis.get(Resque::StuckQueue::VERIFIED_KEY), "should be nil before lib is used"
|
72
|
+
start_and_stop_loops_after(2)
|
73
|
+
refute_nil Resque.redis.get(Resque::StuckQueue::VERIFIED_KEY), "should set verified key after used"
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_it_does_not_trigger_handler_if_under_max_time
|
77
|
+
puts '5'
|
78
|
+
Resque::StuckQueue.stubs(:read_from_redis).returns(Time.now.to_i)
|
79
|
+
@triggered = false
|
80
|
+
Resque::StuckQueue.config[:handler] = proc { @triggered = true }
|
81
|
+
start_and_stop_loops_after(2)
|
82
|
+
assert_equal false, @triggered # "handler should not be called"
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_it_triggers_handler_if_over_trigger_timeout
|
86
|
+
puts '6'
|
87
|
+
last_time_too_old = Time.now.to_i - Resque::StuckQueue::TRIGGER_TIMEOUT
|
88
|
+
Resque::StuckQueue.stubs(:read_from_redis).returns(last_time_too_old.to_s)
|
89
|
+
@triggered = false
|
90
|
+
Resque::StuckQueue.config[:handler] = proc { @triggered = true }
|
91
|
+
start_and_stop_loops_after(2)
|
92
|
+
assert_equal true, @triggered # "handler should be called"
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def start_and_stop_loops_after(secs)
|
98
|
+
ops = []
|
99
|
+
ops << Thread.new { Resque::StuckQueue.start }
|
100
|
+
ops << Thread.new { sleep secs; Resque::StuckQueue.stop }
|
101
|
+
ops.map(&:join)
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: resque_stuck_queue
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Shai Rosenfeld
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-01-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.5'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: where the wild things are. err, when resque gets stuck
|
42
|
+
email:
|
43
|
+
- srosenfeld@engineyard.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- .gitignore
|
49
|
+
- Gemfile
|
50
|
+
- Gemfile.lock
|
51
|
+
- LICENSE.txt
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- THOUGHTS
|
55
|
+
- lib/resque_stuck_queue.rb
|
56
|
+
- lib/resque_stuck_queue/version.rb
|
57
|
+
- resque_stuck_queue.gemspec
|
58
|
+
- test/resque/refresh_latest_timestamp.rb
|
59
|
+
- test/resque/set_redis_key.rb
|
60
|
+
- test/test_integration.rb
|
61
|
+
- test/test_resque_stuck_queue.rb
|
62
|
+
homepage: https://github.com/shaiguitar/resque_stuck_queue/
|
63
|
+
licenses:
|
64
|
+
- MIT
|
65
|
+
metadata: {}
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options: []
|
68
|
+
require_paths:
|
69
|
+
- lib
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ! '>='
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ! '>='
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
requirements: []
|
81
|
+
rubyforge_project:
|
82
|
+
rubygems_version: 2.0.3
|
83
|
+
signing_key:
|
84
|
+
specification_version: 4
|
85
|
+
summary: fire a handler when your queues are wonky
|
86
|
+
test_files:
|
87
|
+
- test/resque/refresh_latest_timestamp.rb
|
88
|
+
- test/resque/set_redis_key.rb
|
89
|
+
- test/test_integration.rb
|
90
|
+
- test/test_resque_stuck_queue.rb
|
91
|
+
has_rdoc:
|