resque-durable 1.0.1 → 2.2.0

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