resque_stuck_queue 0.0.1 → 0.0.2

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 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: