resque_stuck_queue 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- Zjg2N2FjNjcwNjA2MTZiNTQ1NGJjZjQwZmNiZjI5YzcyYjcxZDdmYw==
4
+ YWNkZWJjMzVlMTZlNmI4ZDNiZjk5MTk2ZjFiOTVhZTIxYjA4ZDY1Ng==
5
5
  data.tar.gz: !binary |-
6
- ZDAxNGUwYzUxYTg5NmY2N2Q0OGQ0ZTI0MzQ2MzVlNmJiNDJjY2I3OA==
6
+ NDlmNDlmZGI3OThiMDk2NjJiZGZhZDlmY2YxNzZhOGY0ZmU1YzMxNQ==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- MWMxODVkOTM5NGJlMGQwZWQ5YWU1YTNjN2Q0NWM1NGUzYmExZjBmZjFkN2Yx
10
- MjA5MGIzNDliY2YwYzU5NGEwNTY4NmYxMDNhZGY5MWNkYjU0ZGM1MDhjZmNk
11
- ZGU4YzU1NGM2ZTc5Y2Y0OTIyYWU0NGE0OTQwNmM1ZjdmZTZhZWM=
9
+ ODgyNjJhMjNiYjNhNWRjNjBhYzhiNGUxODNmZDhiNWJiNDViODQ1ZTBlNTdk
10
+ MzU2ZjU3M2MyMWY0OGE5ZWIzYTBlMGI4OTEzZGFkY2RlNjkyNWU2MmI0NjYy
11
+ NDQ3M2IyY2E3YTAwYzUyODExZDNkY2E2ZmI3ZDVjYzAwYWVlN2Q=
12
12
  data.tar.gz: !binary |-
13
- M2VmNmZmY2YwMDExNzdlYzZjM2ZhMWUxZTM4NDViNjBjYjc2NmYzOWI1NDkz
14
- MjRjNmZlNmM1MjA0MGJhYzAzNWUyNWIwMjVhMGVjZTFhNjM4MjkzMDA0NjQx
15
- Yjg2M2VmZDU3ZWM0MWUyODFlNTI1YWVmOWQ4NjlhNzNmM2Y0NWU=
13
+ NzM4ZjQyMDQzOTI2MDQyNTExMzQyNzEwNTAxM2M0NmRiYzM2YWJmMmQyYmFk
14
+ MTMxZWM1YmY3Nzc2ODM0N2M3MTM3NjI2ZmYxNDFjNDc5ZmUyNmVmZmIxMGQ5
15
+ MjkyNWNhOWZkNTNmNWVmM2ExNjg5ZGNkOGRmOTk0ZGU5NGQwYjA=
data/Gemfile CHANGED
@@ -1,6 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gem 'resque'
4
+ gem 'redis-mutex'
4
5
 
5
6
  # TEST
6
7
  gem 'minitest'
data/Gemfile.lock CHANGED
@@ -21,6 +21,10 @@ GEM
21
21
  rack
22
22
  rake (10.1.0)
23
23
  redis (3.0.6)
24
+ redis-classy (1.2.0)
25
+ redis-namespace (~> 1.0)
26
+ redis-mutex (2.1.1)
27
+ redis-classy (~> 1.2)
24
28
  redis-namespace (1.4.1)
25
29
  redis (~> 3.0.4)
26
30
  resque (1.25.1)
@@ -56,6 +60,7 @@ DEPENDENCIES
56
60
  mocha
57
61
  pry
58
62
  rake
63
+ redis-mutex
59
64
  resque
60
65
  resque-mock
61
66
  resque-scheduler
data/README.md CHANGED
@@ -31,6 +31,9 @@ Resque::StuckQueue.config[:heartbeat] = 5.minutes
31
31
  Resque::StuckQueue.config[:trigger_timeout] = 10.hours
32
32
 
33
33
  # what gets triggered when resque-stuck-queue will detect the latest heartbeat is older than the trigger_timeout time set above.
34
+ #
35
+ # triggering will update the key, so you'll have to wait the trigger_timeout again
36
+ # in order for it to trigger again even if workers are still stale.
34
37
  Resque::StuckQueue.config[:handler] = proc { send_email }
35
38
 
36
39
  # optional, in case you want to set your own name for the key that will be used as the last good hearbeat time
data/Rakefile CHANGED
@@ -1,6 +1,12 @@
1
1
  require 'rake/testtask'
2
2
 
3
3
  task :default => :test
4
+ #task :test do
5
+ ## forking and what not. keep containted in each own process?
6
+ #Dir['./test/test_*.rb'].each do |file|
7
+ #system("ruby -I. -I lib/ #{file}")
8
+ #end
9
+ #end
4
10
  Rake::TestTask.new do |t|
5
11
  t.pattern = "test/test_*rb"
6
12
  end
data/THOUGHTS CHANGED
@@ -19,3 +19,13 @@ reproduce the error and integrate test like that
19
19
  ## TODOS
20
20
 
21
21
  verify @config options, raise if no handler, etc.
22
+
23
+ - use locks to only notify/trigger once in case this is used in a server-cluster type environment adn you only want the handler triggered once
24
+
25
+ - resque in stuck (t state process) SIGSTOP sticks up the checker thread?
26
+ or if not running
27
+
28
+ - ensure it only runs from the server box and not the resque box??
29
+ (the deploy restarts the server but not resque workers)
30
+
31
+
@@ -1,5 +1,5 @@
1
1
  module Resque
2
2
  module StuckQueue
3
- VERSION = "0.0.1"
3
+ VERSION = "0.0.2"
4
4
  end
5
5
  end
@@ -1,5 +1,9 @@
1
1
  require "resque_stuck_queue/version"
2
2
 
3
+ # TODO rm redis-mutex dep and just do the setnx locking here
4
+ require 'redis-mutex'
5
+ Redis::Classy.db = Resque.redis
6
+
3
7
  # TODO move this require into a configurable?
4
8
  require 'resque'
5
9
 
@@ -7,8 +11,7 @@ module Resque
7
11
  module StuckQueue
8
12
 
9
13
  GLOBAL_KEY = "resque-stuck-queue"
10
- VERIFIED_KEY = "resque-stuck-queue-ran-once"
11
- HEARTBEAT = 60 * 60 # check/refresh every hour
14
+ HEARTBEAT = 60 * 60 # check/refresh every hour
12
15
  TRIGGER_TIMEOUT = 5 * 60 * 60 # warn/trigger 5 hours
13
16
  HANDLER = proc { $stderr.puts("Shit gone bad with them queues.") }
14
17
 
@@ -54,8 +57,6 @@ module Resque
54
57
  @threads = []
55
58
  config.freeze
56
59
 
57
- mark_first_use
58
-
59
60
  Thread.abort_on_exception = config[:abort_on_exception]
60
61
 
61
62
  enqueue_repeating_refresh_job
@@ -101,8 +102,15 @@ module Resque
101
102
  @threads << Thread.new do
102
103
  while @running
103
104
  wait_for_it
104
- if Time.now.to_i - last_time_worked > max_wait_time
105
- trigger_handler
105
+ mutex = Redis::Mutex.new('resque_stuck_queue_lock', block: 0)
106
+ if mutex.lock
107
+ begin
108
+ if Time.now.to_i - last_time_worked > max_wait_time
109
+ trigger_handler
110
+ end
111
+ ensure
112
+ mutex.unlock
113
+ end
106
114
  end
107
115
  end
108
116
  end
@@ -110,30 +118,28 @@ module Resque
110
118
 
111
119
  def last_time_worked
112
120
  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
121
+ if time_set
122
+ time_set
123
+ else
124
+ manual_refresh
125
+ end.to_i
126
+ end
127
+
128
+ def manual_refresh
129
+ time = Time.now.to_i
130
+ Resque.redis.set(global_key, time)
131
+ time
119
132
  end
120
133
 
121
134
  def trigger_handler
122
135
  (config[:handler] || HANDLER).call
136
+ manual_refresh
123
137
  end
124
138
 
125
139
  def read_from_redis
126
140
  Resque.redis.get(global_key)
127
141
  end
128
142
 
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
143
  def wait_for_it
138
144
  sleep config[:heartbeat] || HEARTBEAT
139
145
  end
@@ -18,6 +18,8 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
+ spec.add_runtime_dependency "redis-mutex"
22
+
21
23
  spec.add_development_dependency "bundler", "~> 1.5"
22
24
  spec.add_development_dependency "rake"
23
25
  end
@@ -0,0 +1,41 @@
1
+ require File.join(File.expand_path(File.dirname(__FILE__)), "test_helper")
2
+
3
+ class TestCollision < Minitest::Test
4
+
5
+ include TestHelper
6
+
7
+ def test_two_processes_interacting
8
+ puts "#{__method__}"
9
+ # no resque should be running here so timeouts will be reached + trigger
10
+ Resque.redis.del("test-incr-key")
11
+
12
+ p1 = fork { Resque.redis.client.reconnect; run_resque_stuck_daemon; }
13
+ p2 = fork { Resque.redis.client.reconnect; run_resque_stuck_daemon; }
14
+ p3 = fork { Resque.redis.client.reconnect; run_resque_stuck_daemon; }
15
+ p4 = fork { Resque.redis.client.reconnect; run_resque_stuck_daemon; }
16
+
17
+ Thread.new {
18
+ sleep 5 # let test run and trigger once occur (according to time below)
19
+ `kill -9 #{p1}`
20
+ `kill -9 #{p2}`
21
+ `kill -9 #{p3}`
22
+ `kill -9 #{p4}`
23
+ Process.waitpid # reap
24
+ }
25
+
26
+ Process.waitall
27
+
28
+ assert_equal 1, Resque.redis.get("test-incr-key").to_i
29
+ end
30
+
31
+ private
32
+
33
+ def run_resque_stuck_daemon
34
+ Resque::StuckQueue.config[:heartbeat] = 1
35
+ Resque::StuckQueue.config[:abort_on_exception] = true
36
+ Resque::StuckQueue.config[:trigger_timeout] = 4
37
+ Resque::StuckQueue.config[:handler] = proc { Resque.redis.incr("test-incr-key") }
38
+ Resque::StuckQueue.start
39
+ end
40
+
41
+ end
@@ -0,0 +1,26 @@
1
+ require 'minitest'
2
+ require "minitest/autorun"
3
+ require 'pry'
4
+ require 'mocha'
5
+ require 'resque/mock'
6
+ $:.unshift(".")
7
+ require 'resque_stuck_queue'
8
+ require File.join(File.expand_path(File.dirname(__FILE__)), "resque", "set_redis_key")
9
+ require File.join(File.expand_path(File.dirname(__FILE__)), "resque", "refresh_latest_timestamp")
10
+
11
+ module TestHelper
12
+
13
+ def run_resque
14
+ pid = fork { exec("QUEUE=* bundle exec rake --trace resque:work") }
15
+ sleep 3 # wait for resque to boot up
16
+ pid
17
+ end
18
+
19
+ def start_and_stop_loops_after(secs)
20
+ ops = []
21
+ ops << Thread.new { Resque::StuckQueue.start }
22
+ ops << Thread.new { sleep secs; Resque::StuckQueue.stop }
23
+ ops.map(&:join)
24
+ end
25
+
26
+ end
@@ -25,7 +25,7 @@ class TestIntegration < Minitest::Test
25
25
  end
26
26
 
27
27
  def teardown
28
- Process.kill("SIGQUIT", @resque_pid)
28
+ `kill -9 #{@resque_pid}` # CONT falls throughs sometimes? hax, rm this and SIGSTOP/SIGCONT
29
29
  Resque::StuckQueue.stop
30
30
  Process.waitpid(@resque_pid)
31
31
  end
@@ -37,8 +37,7 @@ class TestIntegration < Minitest::Test
37
37
  end
38
38
 
39
39
  def test_resque_enqueues_a_job_does_not_trigger
40
-
41
- puts '1'
40
+ puts "#{__method__}"
42
41
  Resque::StuckQueue.config[:trigger_timeout] = 100 # wait a while so we don't trigger
43
42
  Resque::StuckQueue.config[:heartbeat] = 2
44
43
  @triggered = false
@@ -57,8 +56,7 @@ class TestIntegration < Minitest::Test
57
56
  end
58
57
 
59
58
  def test_resque_does_not_enqueues_a_job_does_trigger
60
-
61
- puts '2'
59
+ puts "#{__method__}"
62
60
  Resque::StuckQueue.config[:trigger_timeout] = 2 # won't allow waiting too much and will complain (eg trigger) sooner than later
63
61
  Resque::StuckQueue.config[:heartbeat] = 1
64
62
  @triggered = false
@@ -71,6 +69,7 @@ class TestIntegration < Minitest::Test
71
69
  Process.kill("SIGSTOP", @resque_pid) # jic, do not process jobs so we definitely trigger
72
70
  Resque.enqueue(SetRedisKey)
73
71
  assert_equal Resque.redis.get(SetRedisKey::NAME), nil
72
+ sleep 2 # allow timeout to trigger
74
73
 
75
74
  # check handler did get called
76
75
  assert_equal @triggered, true
@@ -1,105 +1,53 @@
1
- require 'minitest'
2
- require "minitest/autorun"
3
- require 'mocha'
4
-
5
- require 'resque/mock'
6
-
7
- $:.unshift(".")
8
- require 'resque_stuck_queue'
1
+ require File.join(File.expand_path(File.dirname(__FILE__)), "test_helper")
9
2
 
10
3
  class TestResqueStuckQueue < Minitest::Test
11
4
 
5
+ include TestHelper
6
+
12
7
  def teardown
13
- puts 'teardown'
14
- Resque::StuckQueue.unstub(:has_been_used?)
8
+ puts "#{__method__}"
15
9
  Resque::StuckQueue.unstub(:read_from_redis)
16
10
  end
17
11
 
18
12
  def setup
19
- puts 'setup'
13
+ puts "#{__method__}"
20
14
  # clean previous test runs
21
15
  Resque.redis.flushall
22
16
  Resque.mock!
23
17
  Resque::StuckQueue.config[:heartbeat] = 1 # seconds
24
- Resque::StuckQueue.config[:trigger_timeout] = 2
25
18
  Resque::StuckQueue.config[:abort_on_exception] = true
26
19
  end
27
20
 
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
21
  def test_configure_global_key
62
- puts '3'
22
+ puts "#{__method__}"
63
23
  assert_nil Resque.redis.get("it-is-configurable"), "global key should not be set"
64
24
  Resque::StuckQueue.config[:global_key] = "it-is-configurable"
65
25
  start_and_stop_loops_after(2)
66
26
  refute_nil Resque.redis.get("it-is-configurable"), "global key should be set"
67
27
  end
68
28
 
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
29
  def test_it_does_not_trigger_handler_if_under_max_time
77
- puts '5'
30
+ puts "#{__method__}"
31
+ Resque::StuckQueue.config[:trigger_timeout] = 5
78
32
  Resque::StuckQueue.stubs(:read_from_redis).returns(Time.now.to_i)
33
+
79
34
  @triggered = false
80
35
  Resque::StuckQueue.config[:handler] = proc { @triggered = true }
81
- start_and_stop_loops_after(2)
36
+ start_and_stop_loops_after(3)
82
37
  assert_equal false, @triggered # "handler should not be called"
83
38
  end
84
39
 
85
40
  def test_it_triggers_handler_if_over_trigger_timeout
86
- puts '6'
41
+ puts "#{__method__}"
42
+ Resque::StuckQueue.config[:trigger_timeout] = 2
87
43
  last_time_too_old = Time.now.to_i - Resque::StuckQueue::TRIGGER_TIMEOUT
88
44
  Resque::StuckQueue.stubs(:read_from_redis).returns(last_time_too_old.to_s)
45
+
89
46
  @triggered = false
90
47
  Resque::StuckQueue.config[:handler] = proc { @triggered = true }
91
48
  start_and_stop_loops_after(2)
92
49
  assert_equal true, @triggered # "handler should be called"
93
50
  end
94
51
 
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
52
  end
105
53
 
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resque_stuck_queue
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shai Rosenfeld
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-01-04 00:00:00.000000000 Z
11
+ date: 2014-01-06 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis-mutex
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ! '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ! '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -57,6 +71,8 @@ files:
57
71
  - resque_stuck_queue.gemspec
58
72
  - test/resque/refresh_latest_timestamp.rb
59
73
  - test/resque/set_redis_key.rb
74
+ - test/test_collision.rb
75
+ - test/test_helper.rb
60
76
  - test/test_integration.rb
61
77
  - test/test_resque_stuck_queue.rb
62
78
  homepage: https://github.com/shaiguitar/resque_stuck_queue/
@@ -86,6 +102,8 @@ summary: fire a handler when your queues are wonky
86
102
  test_files:
87
103
  - test/resque/refresh_latest_timestamp.rb
88
104
  - test/resque/set_redis_key.rb
105
+ - test/test_collision.rb
106
+ - test/test_helper.rb
89
107
  - test/test_integration.rb
90
108
  - test/test_resque_stuck_queue.rb
91
109
  has_rdoc: