resque-durable 1.0.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b1a9e256ca3afbc5a1605c176de59a028c2546c626f2845c2180bac51dc6b821
4
+ data.tar.gz: f55e471e1139d717ed4002aff1fc01bef8dfe52649c13a3ee0e5e9fd539237d2
5
+ SHA512:
6
+ metadata.gz: de3d4efce9c67ea0dcfb7df3d2f19070c01463de09694135cf56b11fc7dcba5ab335412e3b5c8795e58b9a72e807c1277cde95a8250eb7ac8514d30c3bb641bc
7
+ data.tar.gz: bd6fdfc08faceed5a402a7e7c33f2e2997a82a66d6d4e228dcc3ba34d1ede7f74c890e710572a56ad2b6186436b00a1d8f423cf36771255c568ea9a443fdab5a
@@ -0,0 +1,82 @@
1
+ require 'thread'
2
+
3
+ module Resque
4
+ module Durable
5
+
6
+ # Creates a background thread to regularly heartbeat the queue audit.
7
+ class BackgroundHeartbeat
8
+
9
+ def initialize(queue_audit, interval)
10
+ @queue_audit = queue_audit
11
+ @last_timeout = nil
12
+ @interval = interval
13
+ @mutex = Mutex.new
14
+ @stop = false
15
+ @thread = nil
16
+ end
17
+
18
+ class << self
19
+ # only a separate method for easy stubbing
20
+ def exit_now!
21
+ abort
22
+ end
23
+ end
24
+
25
+ def with_heartbeat
26
+ start!
27
+ yield
28
+ ensure
29
+ stop_and_wait!
30
+ end
31
+
32
+ def heartbeat!
33
+ @last_timeout ||= @queue_audit.timeout_at
34
+ @last_timeout = @queue_audit.optimistic_heartbeat!(@last_timeout)
35
+ rescue StandardError => e
36
+ @queue_audit.logger.error("Exception in BackgroundHeartbeat thread: #{e.class.name}: #{e.message}")
37
+ self.class.exit_now!
38
+ ensure
39
+ ActiveRecord::Base.clear_active_connections!
40
+ end
41
+
42
+ def start!
43
+ raise "Thread is already running!" if @thread
44
+ @stop = false
45
+
46
+ # Perform immediately to reduce heartbeat race condition opportunities
47
+ heartbeat!
48
+
49
+ @thread = Thread.new do
50
+ while !@stop
51
+ heartbeat!
52
+
53
+ @mutex.synchronize do
54
+ @mutex.sleep(@interval)
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ def stop_and_wait!
61
+ return unless @thread
62
+ # Prevent deadlock if called by the `heartbeat` thread, which can't wait for itself to die.
63
+ return signal_stop! if @thread == Thread.current
64
+ while @thread.alive?
65
+ signal_stop!
66
+ sleep 0.01
67
+ end
68
+ @thread.join
69
+ @thread = nil
70
+ end
71
+
72
+ # Signal the `heartbeat` thread to stop looping immediately. Safe to be call from any thread.
73
+ def signal_stop!
74
+ return unless @thread
75
+ @mutex.synchronize do
76
+ @stop = true
77
+ @thread.wakeup
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -1,10 +1,12 @@
1
1
  require 'active_record'
2
- require 'active_support/core_ext/class'
2
+ require 'active_support/core_ext'
3
3
 
4
4
  module Resque
5
5
  module Durable
6
6
  class QueueAudit < ActiveRecord::Base
7
- set_table_name :durable_queue_audits
7
+ JobCollision = Class.new(StandardError)
8
+
9
+ self.table_name = :durable_queue_audits
8
10
  # id
9
11
  # enqueued_id
10
12
  # queue_name
@@ -19,19 +21,18 @@ module Resque
19
21
 
20
22
  validates_length_of :payload_before_type_cast, :in => 1..5000
21
23
 
22
- validates_inclusion_of :duration, :in => 1.minute..3.hours
24
+ validates_inclusion_of :duration, :in => 1.minute.to_i..3.hours.to_i
23
25
 
24
- named_scope :older_than, lambda { |date|
25
- { :conditions => [ 'created_at < ?', date ] }
26
- }
26
+ scope :older_than, ->(date) { where('created_at < ?', date) }
27
27
 
28
- named_scope :failed, lambda {
29
- { :conditions => [ 'completed_at is null AND timeout_at < ?', Time.now.utc ], :order => 'timeout_at asc', :limit => 500 }
28
+ scope :failed, -> {
29
+ where(completed_at: nil)
30
+ .where('timeout_at < ?', Time.now.utc)
31
+ .order('timeout_at asc')
32
+ .limit(500)
30
33
  }
31
34
 
32
- named_scope :complete, lambda {
33
- { :conditions => 'completed_at is not null' }
34
- }
35
+ scope :complete, -> { where('completed_at is not null') }
35
36
 
36
37
  module Recovery
37
38
 
@@ -89,6 +90,18 @@ module Resque
89
90
  update_attribute(:timeout_at, Time.now.utc + duration)
90
91
  end
91
92
 
93
+ # Bumps the `timeout_at` column, but raises a `JobCollision` exception if
94
+ # another process has changed the value, indicating we may have multiple
95
+ # workers processing the same job.
96
+ def optimistic_heartbeat!(last_timeout_at)
97
+ next_timeout_at = Time.now.utc + duration
98
+ nrows = self.class.
99
+ where(id: id, timeout_at: last_timeout_at).
100
+ update_all(timeout_at: next_timeout_at)
101
+ raise JobCollision.new unless nrows == 1
102
+ next_timeout_at
103
+ end
104
+
92
105
  def fail!
93
106
  update_attribute(:timeout_at, Time.now.utc)
94
107
  end
@@ -118,6 +131,13 @@ module Resque
118
131
  (enqueue_count ** 3).minutes
119
132
  end
120
133
 
134
+ def reset_backoff!(timeout_at = Time.now.utc)
135
+ # Set timeout_at = Time.now and enqueue_count = 1 so
136
+ # the job can be picked up by the Durable Monitor asap.
137
+ self.timeout_at = timeout_at
138
+ self.enqueue_count = 1
139
+ save!
140
+ end
121
141
  end
122
142
  end
123
143
  end
@@ -3,11 +3,20 @@ module Resque
3
3
  autoload :GUID, 'resque/durable/guid'
4
4
  autoload :Monitor, 'resque/durable/monitor'
5
5
  autoload :QueueAudit, 'resque/durable/queue_audit'
6
+ autoload :BackgroundHeartbeat, 'resque/durable/background_heartbeat'
6
7
 
7
8
  def self.extended(base)
9
+ # The duration since the last heartbeat that the monitor will wait before
10
+ # re-enqueing the job.
8
11
  base.cattr_accessor :job_timeout
9
12
  base.job_timeout = 10.minutes
10
13
 
14
+ # How frequently a background thread will optimistically heartbeat the
15
+ # QueueAudit. Value must be smaller than job_timeout. Currently opt-in.
16
+ #
17
+ # Recommended value: `15.seconds`
18
+ base.cattr_accessor :background_heartbeat_interval
19
+
11
20
  base.cattr_accessor :auditor
12
21
  base.auditor = QueueAudit
13
22
  end
@@ -24,12 +33,12 @@ module Resque
24
33
  begin
25
34
  audit.enqueued!
26
35
  rescue Exception => e
27
- audit_failed(e)
36
+ audit_failed(e, args)
28
37
  end
29
38
 
30
39
  Resque.enqueue(self, *args)
31
40
  rescue Exception => e
32
- enqueue_failed(e)
41
+ enqueue_failed(e, args)
33
42
  end
34
43
 
35
44
  def audit(args)
@@ -46,24 +55,46 @@ module Resque
46
55
 
47
56
  def around_perform_manage_audit(*args)
48
57
  if a = audit(args)
49
- a.heartbeat!
50
58
  return if a.complete?
51
- yield
52
- a.complete!
59
+ if background_heartbeat_interval
60
+ raise "background_heartbeat_interval (#{background_heartbeat_interval.inspect}) be smaller than job_timeout (#{job_timeout.inspect})" if background_heartbeat_interval >= job_timeout
61
+ BackgroundHeartbeat.new(audit(args), background_heartbeat_interval).with_heartbeat do
62
+ yield
63
+ end
64
+ else
65
+ a.heartbeat!
66
+ yield
67
+ end
68
+
69
+ if requeue_immediately
70
+ a.reset_backoff!
71
+ else
72
+ a.complete!
73
+ end
53
74
  else
54
75
  yield
55
76
  end
77
+ ensure
78
+ @requeue_immediately = false
79
+ end
80
+
81
+ def requeue_immediately
82
+ @requeue_immediately
83
+ end
84
+
85
+ def requeue_immediately!
86
+ @requeue_immediately = true
56
87
  end
57
88
 
58
89
  def build_audit(args)
59
90
  auditor.initialize_by_klass_and_args(self, args)
60
91
  end
61
92
 
62
- def audit_failed(e)
93
+ def audit_failed(e, args)
63
94
  raise e
64
95
  end
65
96
 
66
- def enqueue_failed(e)
97
+ def enqueue_failed(e, args)
67
98
  raise e
68
99
  end
69
100
 
metadata CHANGED
@@ -1,49 +1,63 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resque-durable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
5
- prerelease:
4
+ version: 2.2.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Eric Chapweske
9
8
  - Ben Osheroff
10
- autorequire:
9
+ autorequire:
11
10
  bindir: bin
12
11
  cert_chain: []
13
- date: 2012-07-16 00:00:00.000000000 Z
14
- dependencies: []
15
- description:
16
- email:
12
+ date: 2021-09-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '4.2'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '4.2'
28
+ description:
29
+ email:
17
30
  executables: []
18
31
  extensions: []
19
32
  extra_rdoc_files: []
20
33
  files:
21
34
  - lib/resque/durable.rb
35
+ - lib/resque/durable/background_heartbeat.rb
22
36
  - lib/resque/durable/guid.rb
23
37
  - lib/resque/durable/monitor.rb
24
38
  - lib/resque/durable/queue_audit.rb
25
- homepage:
26
- licenses: []
27
- post_install_message:
39
+ homepage: https://github.com/zendesk/resque-durable
40
+ licenses:
41
+ - MIT
42
+ metadata: {}
43
+ post_install_message:
28
44
  rdoc_options: []
29
45
  require_paths:
30
46
  - lib
31
47
  required_ruby_version: !ruby/object:Gem::Requirement
32
- none: false
33
48
  requirements:
34
- - - ! '>='
49
+ - - ">="
35
50
  - !ruby/object:Gem::Version
36
- version: '0'
51
+ version: '2.4'
37
52
  required_rubygems_version: !ruby/object:Gem::Requirement
38
- none: false
39
53
  requirements:
40
- - - ! '>='
54
+ - - ">="
41
55
  - !ruby/object:Gem::Version
42
56
  version: '0'
43
57
  requirements: []
44
- rubyforge_project:
45
- rubygems_version: 1.8.21
46
- signing_key:
47
- specification_version: 3
58
+ rubyforge_project:
59
+ rubygems_version: 2.7.6.2
60
+ signing_key:
61
+ specification_version: 4
48
62
  summary: Resque queue backed by database audits, with automatic retry
49
63
  test_files: []