rocketjob 5.1.1 → 5.2.0.beta1

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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/bin/rocketjob +2 -2
  3. data/bin/rocketjob_batch_perf +1 -1
  4. data/bin/rocketjob_perf +1 -1
  5. data/lib/rocket_job/active_worker.rb +1 -0
  6. data/lib/rocket_job/batch.rb +16 -17
  7. data/lib/rocket_job/batch/callbacks.rb +1 -2
  8. data/lib/rocket_job/batch/io.rb +10 -6
  9. data/lib/rocket_job/batch/logger.rb +2 -2
  10. data/lib/rocket_job/batch/lower_priority.rb +2 -2
  11. data/lib/rocket_job/batch/model.rb +23 -23
  12. data/lib/rocket_job/batch/performance.rb +19 -21
  13. data/lib/rocket_job/batch/result.rb +1 -1
  14. data/lib/rocket_job/batch/results.rb +1 -1
  15. data/lib/rocket_job/batch/state_machine.rb +5 -6
  16. data/lib/rocket_job/batch/statistics.rb +10 -8
  17. data/lib/rocket_job/batch/tabular.rb +2 -2
  18. data/lib/rocket_job/batch/tabular/input.rb +11 -7
  19. data/lib/rocket_job/batch/tabular/output.rb +1 -1
  20. data/lib/rocket_job/batch/throttle.rb +11 -30
  21. data/lib/rocket_job/batch/{throttle_running_slices.rb → throttle_running_workers.rb} +13 -10
  22. data/lib/rocket_job/batch/worker.rb +102 -85
  23. data/lib/rocket_job/cli.rb +57 -54
  24. data/lib/rocket_job/config.rb +8 -10
  25. data/lib/rocket_job/dirmon_entry.rb +13 -10
  26. data/lib/rocket_job/event.rb +16 -16
  27. data/lib/rocket_job/extensions/mongo/logging.rb +2 -2
  28. data/lib/rocket_job/extensions/mongoid/clients/options.rb +2 -2
  29. data/lib/rocket_job/extensions/mongoid/contextual/mongo.rb +4 -2
  30. data/lib/rocket_job/extensions/mongoid/factory.rb +13 -5
  31. data/lib/rocket_job/extensions/rocket_job_adapter.rb +2 -1
  32. data/lib/rocket_job/job_exception.rb +0 -3
  33. data/lib/rocket_job/jobs/dirmon_job.rb +4 -4
  34. data/lib/rocket_job/jobs/housekeeping_job.rb +7 -7
  35. data/lib/rocket_job/jobs/on_demand_batch_job.rb +14 -4
  36. data/lib/rocket_job/jobs/on_demand_job.rb +3 -3
  37. data/lib/rocket_job/jobs/performance_job.rb +1 -1
  38. data/lib/rocket_job/jobs/re_encrypt/relational_job.rb +11 -10
  39. data/lib/rocket_job/jobs/upload_file_job.rb +9 -5
  40. data/lib/rocket_job/performance.rb +24 -22
  41. data/lib/rocket_job/plugins/cron.rb +7 -3
  42. data/lib/rocket_job/plugins/document.rb +7 -5
  43. data/lib/rocket_job/plugins/job/callbacks.rb +1 -1
  44. data/lib/rocket_job/plugins/job/logger.rb +3 -3
  45. data/lib/rocket_job/plugins/job/model.rb +34 -27
  46. data/lib/rocket_job/plugins/job/persistence.rb +7 -34
  47. data/lib/rocket_job/plugins/job/state_machine.rb +5 -4
  48. data/lib/rocket_job/plugins/job/throttle.rb +12 -28
  49. data/lib/rocket_job/plugins/job/throttle_running_jobs.rb +2 -2
  50. data/lib/rocket_job/plugins/job/worker.rb +22 -70
  51. data/lib/rocket_job/plugins/processing_window.rb +5 -4
  52. data/lib/rocket_job/plugins/restart.rb +3 -3
  53. data/lib/rocket_job/plugins/retry.rb +2 -2
  54. data/lib/rocket_job/plugins/singleton.rb +1 -2
  55. data/lib/rocket_job/plugins/state_machine.rb +4 -4
  56. data/lib/rocket_job/plugins/transaction.rb +1 -1
  57. data/lib/rocket_job/rocket_job.rb +5 -4
  58. data/lib/rocket_job/server.rb +2 -2
  59. data/lib/rocket_job/server/model.rb +14 -13
  60. data/lib/rocket_job/server/state_machine.rb +1 -2
  61. data/lib/rocket_job/sliced/compressed_slice.rb +4 -4
  62. data/lib/rocket_job/sliced/encrypted_slice.rb +4 -4
  63. data/lib/rocket_job/sliced/input.rb +16 -16
  64. data/lib/rocket_job/sliced/output.rb +2 -2
  65. data/lib/rocket_job/sliced/slice.rb +43 -20
  66. data/lib/rocket_job/sliced/slices.rb +14 -11
  67. data/lib/rocket_job/subscriber.rb +6 -6
  68. data/lib/rocket_job/subscribers/logger.rb +3 -3
  69. data/lib/rocket_job/supervisor.rb +12 -12
  70. data/lib/rocket_job/supervisor/shutdown.rb +7 -7
  71. data/lib/rocket_job/throttle_definition.rb +37 -0
  72. data/lib/rocket_job/throttle_definitions.rb +39 -0
  73. data/lib/rocket_job/version.rb +1 -1
  74. data/lib/rocket_job/worker.rb +116 -34
  75. data/lib/rocket_job/worker_pool.rb +6 -6
  76. data/lib/rocketjob.rb +72 -76
  77. metadata +16 -18
  78. data/lib/rocket_job/extensions/mongoid_5/clients/options.rb +0 -38
  79. data/lib/rocket_job/extensions/mongoid_5/contextual/mongo.rb +0 -64
  80. data/lib/rocket_job/extensions/mongoid_5/factory.rb +0 -13
@@ -81,9 +81,10 @@ module RocketJob
81
81
 
82
82
  begin
83
83
  slice.save!
84
- rescue Mongo::Error::OperationFailure => exc
84
+ rescue Mongo::Error::OperationFailure => e
85
85
  # Ignore duplicates since it means the job was restarted
86
- raise(exc) unless exc.message.include?('E11000')
86
+ raise(e) unless e.message.include?("E11000")
87
+
87
88
  logger.warn "Skipped already processed slice# #{slice.id}"
88
89
  end
89
90
  slice
@@ -93,7 +94,7 @@ module RocketJob
93
94
 
94
95
  # Index for find_and_modify only if it is not already present
95
96
  def create_indexes
96
- all.collection.indexes.create_one(state: 1, _id: 1) if all.collection.indexes.none? { |i| i['name'] == 'state_1__id_1' }
97
+ all.collection.indexes.create_one(state: 1, _id: 1) if all.collection.indexes.none? { |i| i["name"] == "state_1__id_1" }
97
98
  rescue Mongo::Error::OperationFailure
98
99
  all.collection.indexes.create_one(state: 1, _id: 1)
99
100
  end
@@ -124,13 +125,15 @@ module RocketJob
124
125
  end
125
126
 
126
127
  # Mongoid does not apply ordering, add sort
128
+ # rubocop:disable Style/RedundantSort
127
129
  def first
128
- all.sort('_id' => 1).first
130
+ all.sort("_id" => 1).first
129
131
  end
130
132
 
131
133
  def last
132
- all.sort('_id' => -1).first
134
+ all.sort("_id" => -1).first
133
135
  end
136
+ # rubocop:enable Style/RedundantSort
134
137
 
135
138
  # Returns [Array<Struct>] grouped exceptions by class name,
136
139
  # and unique exception messages by exception class.
@@ -149,19 +152,19 @@ module RocketJob
149
152
  result = all.collection.aggregate(
150
153
  [
151
154
  {
152
- '$match' => {state: 'failed'}
155
+ "$match" => {state: "failed"}
153
156
  },
154
157
  {
155
- '$group' => {
156
- _id: {error_class: '$exception.class_name'},
157
- messages: {'$addToSet' => '$exception.message'},
158
- count: {'$sum' => 1}
158
+ "$group" => {
159
+ _id: {error_class: "$exception.class_name"},
160
+ messages: {"$addToSet" => "$exception.message"},
161
+ count: {"$sum" => 1}
159
162
  }
160
163
  }
161
164
  ]
162
165
  )
163
166
  result.collect do |errors|
164
- result_struct.new(errors['_id']['error_class'], errors['count'], errors['messages'])
167
+ result_struct.new(errors["_id"]["error_class"], errors["count"], errors["messages"])
165
168
  end
166
169
  end
167
170
  end
@@ -1,4 +1,4 @@
1
- require 'active_support/concern'
1
+ require "active_support/concern"
2
2
 
3
3
  module RocketJob
4
4
  # Mix-in to publish and subscribe to events.
@@ -66,12 +66,12 @@ module RocketJob
66
66
  return
67
67
  end
68
68
 
69
- args = (method(action).arity == 0) || parameters.nil? ? nil : parameters.symbolize_keys
69
+ args = method(action).arity.zero? || parameters.nil? ? nil : parameters.symbolize_keys
70
70
  args ? public_send(action, **args) : public_send(action)
71
- rescue ArgumentError => exc
72
- logger.error("##{action}: Invalid Arguments. Resuming..", exc)
73
- rescue StandardError => exc
74
- logger.error("##{action}: Exception caught. Resuming..", exc)
71
+ rescue ArgumentError => e
72
+ logger.error("##{action}: Invalid Arguments. Resuming..", e)
73
+ rescue StandardError => e
74
+ logger.error("##{action}: Exception caught. Resuming..", e)
75
75
  end
76
76
 
77
77
  def process_event(name, action, parameters)
@@ -1,4 +1,4 @@
1
- require 'socket'
1
+ require "socket"
2
2
 
3
3
  module RocketJob
4
4
  module Subscribers
@@ -9,8 +9,8 @@ module RocketJob
9
9
  @host_name ||= Socket.gethostname
10
10
  end
11
11
 
12
- def self.host_name=(host_name)
13
- @host_name = host_name
12
+ class << self
13
+ attr_writer :host_name
14
14
  end
15
15
 
16
16
  # Change the log level
@@ -1,4 +1,4 @@
1
- require 'rocket_job/supervisor/shutdown'
1
+ require "rocket_job/supervisor/shutdown"
2
2
 
3
3
  module RocketJob
4
4
  # Starts a server instance, along with the workers and ensures workers remain running until they need to shutdown.
@@ -11,7 +11,7 @@ module RocketJob
11
11
 
12
12
  # Start the Supervisor, using the supplied attributes to create a new Server instance.
13
13
  def self.run
14
- Thread.current.name = 'rocketjob main'
14
+ Thread.current.name = "rocketjob main"
15
15
  RocketJob.create_indexes
16
16
  register_signal_handlers
17
17
 
@@ -29,9 +29,9 @@ module RocketJob
29
29
 
30
30
  def run
31
31
  logger.info "Using MongoDB Database: #{RocketJob::Job.collection.database.name}"
32
- logger.info('Running with filter', Config.filter) if Config.filter
32
+ logger.info("Running with filter", Config.filter) if Config.filter
33
33
  server.started!
34
- logger.info 'Rocket Job Server started'
34
+ logger.info "Rocket Job Server started"
35
35
 
36
36
  event_listener = Thread.new { Event.listener }
37
37
  Subscribers::Server.subscribe(self) do
@@ -43,21 +43,21 @@ module RocketJob
43
43
  end
44
44
  end
45
45
  rescue ::Mongoid::Errors::DocumentNotFound
46
- logger.info('Server has been destroyed. Going down hard!')
47
- rescue Exception => exc
48
- logger.error('RocketJob::Server is stopping due to an exception', exc)
46
+ logger.info("Server has been destroyed. Going down hard!")
47
+ rescue Exception => e
48
+ logger.error("RocketJob::Server is stopping due to an exception", e)
49
49
  ensure
50
- event_listener.kill if event_listener
50
+ event_listener&.kill
51
51
  # Logs the backtrace for each running worker
52
52
  worker_pool.log_backtraces
53
- logger.info('Shutdown Complete')
53
+ logger.info("Shutdown Complete")
54
54
  end
55
55
 
56
56
  def stop!
57
57
  server.stop! if server.may_stop?
58
58
  worker_pool.stop
59
- while !worker_pool.join
60
- logger.info 'Waiting for workers to finish processing ...'
59
+ until worker_pool.join
60
+ logger.info "Waiting for workers to finish processing ..."
61
61
  # One or more workers still running so update heartbeat so that server reports "alive".
62
62
  server.refresh(worker_pool.living_count)
63
63
  end
@@ -65,7 +65,7 @@ module RocketJob
65
65
 
66
66
  def supervise_pool
67
67
  stagger = true
68
- while !self.class.shutdown?
68
+ until self.class.shutdown?
69
69
  synchronize do
70
70
  if server.running?
71
71
  worker_pool.prune
@@ -1,5 +1,5 @@
1
- require 'active_support/concern'
2
- require 'concurrent'
1
+ require "active_support/concern"
2
+ require "concurrent"
3
3
 
4
4
  module RocketJob
5
5
  class Supervisor
@@ -37,23 +37,23 @@ module RocketJob
37
37
  # Perform clean shutdown
38
38
  #
39
39
  def self.register_signal_handlers
40
- Signal.trap 'SIGTERM' do
40
+ Signal.trap "SIGTERM" do
41
41
  Thread.new do
42
42
  shutdown!
43
- message = 'Shutdown signal (SIGTERM) received. Will shutdown as soon as active jobs/slices have completed.'
43
+ message = "Shutdown signal (SIGTERM) received. Will shutdown as soon as active jobs/slices have completed."
44
44
  logger.info(message)
45
45
  end
46
46
  end
47
47
 
48
- Signal.trap 'INT' do
48
+ Signal.trap "INT" do
49
49
  Thread.new do
50
50
  shutdown!
51
- message = 'Shutdown signal (INT) received. Will shutdown as soon as active jobs/slices have completed.'
51
+ message = "Shutdown signal (INT) received. Will shutdown as soon as active jobs/slices have completed."
52
52
  logger.info(message)
53
53
  end
54
54
  end
55
55
  rescue StandardError
56
- logger.warn 'SIGTERM handler not installed. Not able to shutdown gracefully'
56
+ logger.warn "SIGTERM handler not installed. Not able to shutdown gracefully"
57
57
  end
58
58
 
59
59
  private_class_method :register_signal_handlers
@@ -0,0 +1,37 @@
1
+ module RocketJob
2
+ class ThrottleDefinition
3
+ attr_reader :method_name, :filter
4
+
5
+ def initialize(method_name, filter)
6
+ @method_name = method_name.to_sym
7
+ @filter = filter
8
+ end
9
+
10
+ # Returns [true|false] whether the throttle was triggered.
11
+ def throttled?(job, *args)
12
+ # Throttle exceeded?
13
+ # Throttle methods can be private.
14
+ throttled =
15
+ if args.size.positive?
16
+ job.method(method_name).arity.zero? ? job.send(method_name) : job.send(method_name, *args)
17
+ else
18
+ job.send(method_name)
19
+ end
20
+ return false unless throttled
21
+
22
+ job.logger.debug { "Throttle: #{method_name} has been exceeded." }
23
+ true
24
+ end
25
+
26
+ # Returns the filter to apply to the job when the above throttle returns true.
27
+ def extract_filter(job, *args)
28
+ return filter.call(job, *args) if filter.is_a?(Proc)
29
+
30
+ if args.size.positive?
31
+ job.method(filter).arity.zero? ? job.send(filter) : job.send(filter, *args)
32
+ else
33
+ job.send(filter)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,39 @@
1
+ module RocketJob
2
+ class ThrottleDefinitions
3
+ attr_reader :throttles
4
+
5
+ def initialize
6
+ @throttles = []
7
+ end
8
+
9
+ def add(method_name, filter)
10
+ unless filter.is_a?(Symbol) || filter.is_a?(Proc)
11
+ raise(ArgumentError, "Filter for #{method_name} must be a Symbol or Proc")
12
+ end
13
+ raise(ArgumentError, "Cannot define #{method_name} twice, undefine previous throttle first") if exist?(method_name)
14
+
15
+ @throttles += [ThrottleDefinition.new(method_name, filter)]
16
+ end
17
+
18
+ # Undefine a previously defined throttle
19
+ def remove(method_name)
20
+ throttles.delete_if { |throttle| throttle.method_name == method_name }
21
+ end
22
+
23
+ # Has a throttle been defined?
24
+ def exist?(method_name)
25
+ throttles.any? { |throttle| throttle.method_name == method_name }
26
+ end
27
+
28
+ # Returns the matching filter,
29
+ # or nil if no throttles were triggered.
30
+ def matching_filter(job, *args)
31
+ throttles.each do |throttle|
32
+ next unless throttle.throttled?(job, *args)
33
+
34
+ return throttle.extract_filter(job, *args)
35
+ end
36
+ nil
37
+ end
38
+ end
39
+ end
@@ -1,3 +1,3 @@
1
1
  module RocketJob
2
- VERSION = '5.1.1'.freeze
2
+ VERSION = "5.2.0.beta1".freeze
3
3
  end
@@ -1,5 +1,5 @@
1
- require 'concurrent'
2
- require 'forwardable'
1
+ require "concurrent"
2
+ require "forwardable"
3
3
  module RocketJob
4
4
  # Worker
5
5
  #
@@ -12,13 +12,13 @@ module RocketJob
12
12
  define_callbacks :running
13
13
 
14
14
  attr_accessor :id, :current_filter
15
- attr_reader :thread, :name, :inline
15
+ attr_reader :thread, :name, :inline, :server_name
16
16
 
17
17
  # Raised when a worker is killed so that it shutdown immediately, yet cleanly.
18
18
  #
19
19
  # Note:
20
20
  # - It is not recommended to catch this exception since it is to shutdown workers quickly.
21
- class Shutdown < Interrupt
21
+ class Shutdown < RuntimeError
22
22
  end
23
23
 
24
24
  def self.before_running(*filters, &blk)
@@ -33,7 +33,7 @@ module RocketJob
33
33
  set_callback(:running, :around, *filters, &blk)
34
34
  end
35
35
 
36
- def initialize(id: 0, server_name: 'inline:0', inline: false)
36
+ def initialize(id: 0, server_name: "inline:0", inline: false)
37
37
  @id = id
38
38
  @server_name = server_name
39
39
  @shutdown = Concurrent::Event.new
@@ -76,49 +76,37 @@ module RocketJob
76
76
  @shutdown.wait(timeout)
77
77
  end
78
78
 
79
- private
80
-
81
79
  # Process jobs until it shuts down
82
80
  #
83
81
  # Params
84
82
  # worker_id [Integer]
85
83
  # The number of this worker for logging purposes
86
84
  def run
87
- Thread.current.name = format('rocketjob %03i', id)
88
- logger.info 'Started'
89
- until shutdown?
90
- wait = Config.max_poll_seconds
91
- if process_available_jobs
92
- # Keeps workers staggered across the poll interval so that
93
- # all workers don't poll at the same time
94
- wait = rand(wait * 1000) / 1000
95
- end
96
- break if wait_for_shutdown?(wait)
97
- end
98
- logger.info 'Stopping'
99
- rescue Exception => exc
100
- logger.fatal('Unhandled exception in job processing thread', exc)
101
- ensure
102
- ActiveRecord::Base.clear_active_connections! if defined?(ActiveRecord::Base)
103
- end
85
+ Thread.current.name = format("rocketjob %03i", id)
86
+ logger.info "Started"
104
87
 
105
- # Process the next available job
106
- # Returns [Boolean] whether any job was actually processed
107
- def process_available_jobs
108
- processed = false
109
88
  until shutdown?
89
+ sleep_seconds = Config.max_poll_seconds
110
90
  reset_filter_if_expired
111
- job = Job.rocket_job_next_job(name, current_filter)
112
- break unless job
113
-
114
- SemanticLogger.named_tagged(job: job.id.to_s) do
115
- processed = true unless job.rocket_job_work(self, false, current_filter)
91
+ job = next_available_job
116
92
 
93
+ # Returns true when work was completed, but no other work is available
94
+ if job&.rocket_job_work(self, false)
117
95
  # Return the database connections for this thread back to the connection pool
118
96
  ActiveRecord::Base.clear_active_connections! if defined?(ActiveRecord::Base)
97
+
98
+ # Stagger workers so that they don't all poll at the same time.
99
+ sleep_seconds = random_wait_interval
119
100
  end
101
+
102
+ wait_for_shutdown?(sleep_seconds)
120
103
  end
121
- processed
104
+
105
+ logger.info "Stopping"
106
+ rescue Exception => e
107
+ logger.fatal("Unhandled exception in job processing thread", e)
108
+ ensure
109
+ ActiveRecord::Base.clear_active_connections! if defined?(ActiveRecord::Base)
122
110
  end
123
111
 
124
112
  # Resets the current job filter if the relevant time interval has passed
@@ -130,5 +118,99 @@ module RocketJob
130
118
  @re_check_start = time
131
119
  self.current_filter = Config.filter || {}
132
120
  end
121
+
122
+ # Returns [RocketJob::Job] the next job available for processing.
123
+ # Returns [nil] if no job is available for processing.
124
+ #
125
+ # Notes:
126
+ # - Destroys expired jobs
127
+ # - Runs job throttles and skips the job if it is throttled.
128
+ # - Adding that filter to the current filter to exclude from subsequent polling.
129
+ def next_available_job
130
+ until shutdown?
131
+ job = find_and_assign_job
132
+ return unless job
133
+
134
+ if job.expired?
135
+ job.fail_on_exception! do
136
+ job.worker_name = name
137
+ job.destroy
138
+ logger.info("Destroyed expired job.")
139
+ end
140
+ next
141
+ end
142
+
143
+ # Batch Job that is already started?
144
+ # Batch has its own throttles for slices.
145
+ return job if job.running?
146
+
147
+ # Should this job be throttled?
148
+ next if job.fail_on_exception! { throttled_job?(job) }
149
+
150
+ # Start this job!
151
+ job.fail_on_exception! { job.start!(name) }
152
+ return job if job.running?
153
+ end
154
+ end
155
+
156
+ # Whether the supplied job has been throttled and should be ignored.
157
+ def throttled_job?(job)
158
+ # Evaluate job throttles, if any.
159
+ filter = job.rocket_job_throttles.matching_filter(job)
160
+ return false unless filter
161
+
162
+ add_to_current_filter(filter)
163
+ # Restore retrieved job so that other workers can process it later
164
+ job.set(worker_name: nil, state: :queued)
165
+ true
166
+ end
167
+
168
+ # Finds the next job to work on in priority based order
169
+ # and assigns it to this worker.
170
+ #
171
+ # Applies the current filter to exclude filtered jobs.
172
+ #
173
+ # Returns nil if no jobs are available for processing.
174
+ if Mongoid::VERSION.to_f >= 7.1
175
+ def find_and_assign_job
176
+ SemanticLogger.silence(:info) do
177
+ scheduled = RocketJob::Job.where(run_at: nil).or(:run_at.lte => Time.now)
178
+ working = RocketJob::Job.queued.or(state: :running, sub_state: :processing)
179
+ query = RocketJob::Job.and(working, scheduled)
180
+ query = query.and(current_filter) unless current_filter.blank?
181
+ update = {"$set" => {"worker_name" => name, "state" => "running"}}
182
+ query.sort(priority: 1, _id: 1).find_one_and_update(update, bypass_document_validation: true)
183
+ end
184
+ end
185
+ else
186
+ def find_and_assign_job
187
+ SemanticLogger.silence(:info) do
188
+ scheduled = {"$or" => [{run_at: nil}, {:run_at.lte => Time.now}]}
189
+ working = {"$or" => [{state: :queued}, {state: :running, sub_state: :processing}]}
190
+ query = RocketJob::Job.and(working, scheduled)
191
+ query = query.where(current_filter) unless current_filter.blank?
192
+ update = {"$set" => {"worker_name" => name, "state" => "running"}}
193
+ query.sort(priority: 1, _id: 1).find_one_and_update(update, bypass_document_validation: true)
194
+ end
195
+ end
196
+ end
197
+
198
+ # Add the supplied filter to the current filter.
199
+ def add_to_current_filter(filter)
200
+ filter.each_pair do |k, v|
201
+ current_filter[k] =
202
+ if (previous = current_filter[k])
203
+ v.is_a?(Array) ? previous + v : v
204
+ else
205
+ v
206
+ end
207
+ end
208
+ current_filter
209
+ end
210
+
211
+ # Returns [Float] a randomized poll interval in seconds up to the maximum configured poll interval.
212
+ def random_wait_interval
213
+ rand(Config.max_poll_seconds * 1000) / 1000
214
+ end
133
215
  end
134
216
  end