sidekiq 8.0.7 → 8.0.8

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,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6411cfbee23ece1d53e775bf2726c85d236469d655eedfc8bc808277693fa1b
4
- data.tar.gz: eef531b98d9f9e6fd5dc2fe2d2d59ac4a033134417be773b453dbf159f125156
3
+ metadata.gz: 3a1e8c90888b4135f21e4dfef66fd9ad9ac8c72ab267599a8452f4bd172234a2
4
+ data.tar.gz: fa83e70c818fc3b25441e946c1a093a2d9a5f4515ee2bd88522cbafcb8097973
5
5
  SHA512:
6
- metadata.gz: 8cd9e7e77116f9a9d07ac4cf9f805ffd58d6d81fe53fa52cf5c293fb33ae6bdaff6fce351527bf87df056225794016157949c6dcb9f1d3ddc37169c105cc15ce
7
- data.tar.gz: f1b0fceebc6a94e2d4ca441fe834f3e82a1d0702da0cd70d14cf8fb0eb2f9490d6b1842e0e5c34e4a5f4e6a13a193806920826bd8b6f816297db471fc7464313
6
+ metadata.gz: 0ea8ac9585538723d941af6ce9150933669705cf1e3459dcea8989d073d234c4e233e74314a5a682e3111e1290a69671051e847b5ee97970c1edd08293e14bbc
7
+ data.tar.gz: 0b253cd035132f786613fbb97440f5d16f48717aedaa7f50865e0e261ce0fc17b64885bea3cdeb5aabe72258c06ed013891d83655bdcf842fc1a261d1b0aa985
data/Changes.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  [Sidekiq Changes](https://github.com/sidekiq/sidekiq/blob/main/Changes.md) | [Sidekiq Pro Changes](https://github.com/sidekiq/sidekiq/blob/main/Pro-Changes.md) | [Sidekiq Enterprise Changes](https://github.com/sidekiq/sidekiq/blob/main/Ent-Changes.md)
4
4
 
5
+ 8.0.8
6
+ ----------
7
+
8
+ - Allow an optional global iteration max runtime. After executing for this length of time,
9
+ Sidekiq will re-queue the job to continue execution at a later time [#6819, fatkodima]
10
+ ```ruby
11
+ Sidekiq.configure_server do |cfg|
12
+ cfg[:max_iteration_runtime] = 600 # ten minutes
13
+ end
14
+ ```
15
+ - Add `discarded_at` attribute when discarding a job so death handlers can distinguish between
16
+ a job which was killed and one that was discarded. [#6820, gstokkink]
17
+ - `perform_bulk` now accepts an `:at` array of times to schedule each job at the corresponding time.
18
+ `perform_bulk(args: [[1], [2]], at: [Time.now, Time.now + 1])` [#6790, fatkodima]
19
+ - `perform_bulk` now accepts a `:spread_interval` value to schedule jobs over
20
+ the next N seconds. `perform_bulk(..., spread_interval: 60)` [#6792, fatkodima]
21
+ - Fix unintended display of flash messages in the Web UI due to session key collision
22
+ - Add support for lazy load hooks [#6825]
23
+
5
24
  8.0.7
6
25
  ----------
7
26
 
data/lib/sidekiq/api.rb CHANGED
@@ -1168,7 +1168,6 @@ module Sidekiq
1168
1168
  # # thread_id is a unique identifier per thread
1169
1169
  # # work is a `Sidekiq::Work` instance that has the following accessor methods.
1170
1170
  # # [work.queue, work.run_at, work.payload]
1171
- # # run_at is an epoch Integer.
1172
1171
  # end
1173
1172
  #
1174
1173
  class WorkSet
@@ -1322,3 +1321,5 @@ module Sidekiq
1322
1321
  end
1323
1322
  end
1324
1323
  end
1324
+
1325
+ Sidekiq.loader.run_load_hooks(:api)
@@ -117,6 +117,9 @@ module Sidekiq
117
117
  # larger than 1000 but YMMV based on network quality, size of job args, etc.
118
118
  # A large number of jobs can cause a bit of Redis command processing latency.
119
119
  #
120
+ # Accepts an additional `:spread_interval` option (in seconds) to randomly spread
121
+ # the jobs schedule times over the specified interval.
122
+ #
120
123
  # Takes the same arguments as #push except that args is expected to be
121
124
  # an Array of Arrays. All other keys are duplicated for each job. Each job
122
125
  # is run through the client middleware pipeline and each job gets its own Job ID
@@ -131,13 +134,24 @@ module Sidekiq
131
134
  def push_bulk(items)
132
135
  batch_size = items.delete(:batch_size) || items.delete("batch_size") || 1_000
133
136
  args = items["args"]
134
- at = items.delete("at")
137
+ at = items.delete("at") || items.delete(:at)
135
138
  raise ArgumentError, "Job 'at' must be a Numeric or an Array of Numeric timestamps" if at && (Array(at).empty? || !Array(at).all? { |entry| entry.is_a?(Numeric) })
136
139
  raise ArgumentError, "Job 'at' Array must have same size as 'args' Array" if at.is_a?(Array) && at.size != args.size
137
140
 
138
141
  jid = items.delete("jid")
139
142
  raise ArgumentError, "Explicitly passing 'jid' when pushing more than one job is not supported" if jid && args.size > 1
140
143
 
144
+ spread_interval = items.delete(:spread_interval) || items.delete("spread_interval")
145
+ raise ArgumentError, "Jobs 'spread_interval' must be a positive Numeric" if spread_interval && (!spread_interval.is_a?(Numeric) || spread_interval <= 0)
146
+ raise ArgumentError, "Only one of 'at' or 'spread_interval' can be provided" if at && spread_interval
147
+
148
+ if !at && spread_interval
149
+ # Do not use spread interval smaller than pooling interval.
150
+ spread_interval = [spread_interval, 5].max
151
+ now = Time.now.to_f
152
+ at = args.map { now + rand * spread_interval }
153
+ end
154
+
141
155
  normed = normalize_item(items)
142
156
  slice_index = 0
143
157
  result = args.each_slice(batch_size).flat_map do |slice|
@@ -19,7 +19,8 @@ module Sidekiq
19
19
  DEFAULT_THREAD_PRIORITY = -1
20
20
 
21
21
  ##
22
- # Sidekiq::Component assumes a config instance is available at @config
22
+ # Sidekiq::Component provides a set of utility methods depending only
23
+ # on Sidekiq::Config. It assumes a config instance is available at @config.
23
24
  module Component # :nodoc:
24
25
  attr_reader :config
25
26
 
@@ -17,10 +17,9 @@ module Sidekiq
17
17
  poll_interval_average: nil,
18
18
  average_scheduled_poll_interval: 5,
19
19
  on_complex_arguments: :raise,
20
- iteration: {
21
- max_job_runtime: nil,
22
- retry_backoff: 0
23
- },
20
+ # if the Iterable job runs longer than this value (in seconds), then the job
21
+ # will be interrupted after the current iteration and re-enqueued at the back of the queue
22
+ max_iteration_runtime: nil,
24
23
  error_handlers: [],
25
24
  death_handlers: [],
26
25
  lifecycle_events: {
@@ -143,7 +143,7 @@ module Sidekiq
143
143
  fetch_previous_iteration_state
144
144
 
145
145
  @_executions += 1
146
- @_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
146
+ @_start_time = mono_now
147
147
 
148
148
  enumerator = build_enumerator(*args, cursor: @_cursor)
149
149
  unless enumerator
@@ -204,17 +204,17 @@ module Sidekiq
204
204
 
205
205
  time_limit = Sidekiq.default_configuration[:timeout]
206
206
  found_record = false
207
- state_flushed_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
207
+ state_flushed_at = mono_now
208
208
 
209
209
  enumerator.each do |object, cursor|
210
210
  found_record = true
211
211
  @_cursor = cursor
212
212
  @current_object = object
213
213
 
214
- is_interrupted = interrupted?
215
- if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - state_flushed_at >= STATE_FLUSH_INTERVAL || is_interrupted
214
+ interrupt_job = interrupted? || should_interrupt?
215
+ if mono_now - state_flushed_at >= STATE_FLUSH_INTERVAL || interrupt_job
216
216
  _, _, cancelled = flush_state
217
- state_flushed_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
217
+ state_flushed_at = mono_now
218
218
  if cancelled
219
219
  @_cancelled = true
220
220
  on_cancel
@@ -223,9 +223,9 @@ module Sidekiq
223
223
  end
224
224
  end
225
225
 
226
- return false if is_interrupted
226
+ return false if interrupt_job
227
227
 
228
- verify_iteration_time(time_limit, object) do
228
+ verify_iteration_time(time_limit) do
229
229
  around_iteration do
230
230
  each_iteration(object, *arguments)
231
231
  rescue Exception
@@ -238,16 +238,16 @@ module Sidekiq
238
238
  logger.debug("Enumerator found nothing to iterate!") unless found_record
239
239
  true
240
240
  ensure
241
- @_runtime += (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @_start_time)
241
+ @_runtime += (mono_now - @_start_time)
242
242
  end
243
243
 
244
- def verify_iteration_time(time_limit, object)
245
- start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
244
+ def verify_iteration_time(time_limit)
245
+ start = mono_now
246
246
  yield
247
- finish = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
247
+ finish = mono_now
248
248
  total = finish - start
249
249
  if total > time_limit
250
- logger.warn { "Iteration took longer (%.2f) than Sidekiq's shutdown timeout (%d) when processing `%s`. This can lead to job processing problems during deploys" % [total, time_limit, object] }
250
+ logger.warn { "Iteration took longer (%.2f) than Sidekiq's shutdown timeout (%d). This can lead to job processing problems during deploys" % [total, time_limit] }
251
251
  end
252
252
  end
253
253
 
@@ -273,6 +273,11 @@ module Sidekiq
273
273
  end
274
274
  end
275
275
 
276
+ def should_interrupt?
277
+ max_iteration_runtime = Sidekiq.default_configuration[:max_iteration_runtime]
278
+ max_iteration_runtime && (mono_now - @_start_time > max_iteration_runtime)
279
+ end
280
+
276
281
  def flush_state
277
282
  key = iteration_key
278
283
  state = {
@@ -308,6 +313,10 @@ module Sidekiq
308
313
  raise "Unexpected thrown value: #{completed.inspect}"
309
314
  end
310
315
  end
316
+
317
+ def mono_now
318
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
319
+ end
311
320
  end
312
321
  end
313
322
  end
data/lib/sidekiq/job.rb CHANGED
@@ -248,9 +248,9 @@ module Sidekiq
248
248
  end
249
249
  alias_method :perform_sync, :perform_inline
250
250
 
251
- def perform_bulk(args, batch_size: 1_000)
251
+ def perform_bulk(args, **options)
252
252
  client = @klass.build_client
253
- client.push_bulk(@opts.merge("class" => @klass, "args" => args, :batch_size => batch_size))
253
+ client.push_bulk(@opts.merge({"class" => @klass, "args" => args}, options))
254
254
  end
255
255
 
256
256
  # +interval+ must be a timestamp, numeric or something that acts
@@ -186,6 +186,8 @@ module Sidekiq
186
186
  strategy, delay = delay_for(jobinst, count, exception, msg)
187
187
  case strategy
188
188
  when :discard
189
+ msg["discarded_at"] = now_ms
190
+
189
191
  return run_death_handlers(msg, exception)
190
192
  when :kill
191
193
  return retries_exhausted(jobinst, msg, exception)
@@ -255,8 +257,14 @@ module Sidekiq
255
257
  handle_exception(e, {context: "Error calling retries_exhausted", job: msg})
256
258
  end
257
259
 
258
- to_morgue = !(msg["dead"] == false || rv == :discard)
259
- send_to_morgue(msg) if to_morgue
260
+ discarded = msg["dead"] == false || rv == :discard
261
+
262
+ if discarded
263
+ msg["discarded_at"] = now_ms
264
+ else
265
+ send_to_morgue(msg)
266
+ end
267
+
260
268
  run_death_handlers(msg, exception)
261
269
  end
262
270
 
@@ -0,0 +1,57 @@
1
+ module Sidekiq
2
+ require "sidekiq/component"
3
+
4
+ class Loader
5
+ include Sidekiq::Component
6
+
7
+ def initialize(cfg = Sidekiq.default_configuration)
8
+ @config = cfg
9
+ @load_hooks = Hash.new { |h, k| h[k] = [] }
10
+ @loaded = Set.new
11
+ @lock = Mutex.new
12
+ end
13
+
14
+ # Declares a block that will be executed when a Sidekiq component is fully
15
+ # loaded. If the component has already loaded, the block is executed
16
+ # immediately.
17
+ #
18
+ # Sidekiq.loader.on_load(:api) do
19
+ # # extend the sidekiq API
20
+ # end
21
+ #
22
+ def on_load(name, &block)
23
+ # we don't want to hold the lock while calling the block
24
+ to_run = nil
25
+
26
+ @lock.synchronize do
27
+ if @loaded.include?(name)
28
+ to_run = block
29
+ else
30
+ @load_hooks[name] << block
31
+ end
32
+ end
33
+
34
+ to_run&.call
35
+ nil
36
+ end
37
+
38
+ # Executes all blocks registered to +name+ via on_load.
39
+ #
40
+ # Sidekiq.loader.run_load_hooks(:api)
41
+ #
42
+ # In the case of the above example, it will execute all hooks registered for +:api+.
43
+ #
44
+ def run_load_hooks(name)
45
+ hks = @lock.synchronize do
46
+ @loaded << name
47
+ @load_hooks.delete(name)
48
+ end
49
+
50
+ hks&.each do |blk|
51
+ blk.call
52
+ rescue => ex
53
+ handle_exception(ex, hook: name)
54
+ end
55
+ end
56
+ end
57
+ end
data/lib/sidekiq/rails.rb CHANGED
@@ -48,8 +48,10 @@ module Sidekiq
48
48
  unless ::Rails.logger == config.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
49
49
  if ::Rails.logger.respond_to?(:broadcast_to)
50
50
  ::Rails.logger.broadcast_to(config.logger)
51
- else
51
+ elsif ::ActiveSupport::Logger.respond_to?(:broadcast)
52
52
  ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
53
+ else
54
+ ::Rails.logger = ::ActiveSupport::BroadcastLogger.new(::Rails.logger, config.logger)
53
55
  end
54
56
  end
55
57
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "8.0.7"
4
+ VERSION = "8.0.8"
5
5
  MAJOR = 8
6
6
 
7
7
  def self.gem_version
@@ -102,15 +102,15 @@ module Sidekiq
102
102
  def flash
103
103
  msg = yield
104
104
  logger.info msg
105
- session[:flash] = msg
105
+ session[:skq_flash] = msg
106
106
  end
107
107
 
108
108
  def flash?
109
- session&.[](:flash)
109
+ session&.[](:skq_flash)
110
110
  end
111
111
 
112
112
  def get_flash
113
- @flash ||= session.delete(:flash)
113
+ @flash ||= session.delete(:skq_flash)
114
114
  end
115
115
 
116
116
  def erb(content, options = {})
data/lib/sidekiq.rb CHANGED
@@ -29,6 +29,7 @@ end
29
29
 
30
30
  require "sidekiq/config"
31
31
  require "sidekiq/logger"
32
+ require "sidekiq/loader"
32
33
  require "sidekiq/client"
33
34
  require "sidekiq/transaction_aware_client"
34
35
  require "sidekiq/job"
@@ -94,6 +95,10 @@ module Sidekiq
94
95
  default_configuration.logger
95
96
  end
96
97
 
98
+ def self.loader
99
+ @loader ||= Loader.new
100
+ end
101
+
97
102
  def self.configure_server(&block)
98
103
  (@config_blocks ||= []) << block
99
104
  yield default_configuration if server?
@@ -58,7 +58,7 @@ function addPollingListeners(_event) {
58
58
 
59
59
  function addDataToggleListeners(event) {
60
60
  var source = event.target || event.srcElement;
61
- var targName = source.getAttribute("data-toggle");
61
+ var targName = source.dataset.toggle;
62
62
  var full = document.getElementById(targName);
63
63
  full.classList.toggle("is-open");
64
64
  }
@@ -81,7 +81,7 @@ function addShiftClickListeners() {
81
81
  }
82
82
 
83
83
  function updateFuzzyTimes() {
84
- var locale = document.body.getAttribute("data-locale");
84
+ var locale = document.body.dataset.locale;
85
85
  var parts = locale.split('-');
86
86
  if (typeof parts[1] !== 'undefined') {
87
87
  parts[1] = parts[1].toUpperCase();
@@ -96,7 +96,7 @@ function updateFuzzyTimes() {
96
96
  function updateNumbers() {
97
97
  document.querySelectorAll("[data-nwp]").forEach(node => {
98
98
  let number = parseFloat(node.textContent);
99
- let precision = parseInt(node.dataset["nwp"] || 0);
99
+ let precision = parseInt(node.dataset.nwp || 0);
100
100
  if (typeof number === "number") {
101
101
  let formatted = number.toLocaleString(undefined, {
102
102
  minimumFractionDigits: precision,
@@ -175,9 +175,9 @@ function handleConfirmDialog (event) {
175
175
  const target = event.target
176
176
 
177
177
  if (target.localName !== "input") { return }
178
- if (!target.hasAttribute("data-confirm")) { return }
178
+ const confirmMessage = target.dataset.confirm
179
179
 
180
- const confirmMessage = target.getAttribute("data-confirm")
180
+ if (confirmMessage === undefined) { return }
181
181
 
182
182
  if (!window.confirm(confirmMessage)) {
183
183
  event.preventDefault()
@@ -29,8 +29,6 @@
29
29
 
30
30
  *, *::before, *::after { box-sizing: border-box; }
31
31
 
32
- ::selection { background: var(--color-selected); }
33
-
34
32
  :focus-visible {
35
33
  outline: 1px solid oklch(from var(--color-primary) l c h / 50%);
36
34
  }
@@ -567,7 +565,6 @@ body > footer .nav {
567
565
  --color-border: oklch(25% 0.01 256);
568
566
  --color-input-border: oklch(31% 0.01 256);
569
567
  --color-selected: oklch(27% 0.01 256);
570
- --color-selected-text: oklch(55% 0.11 45);
571
568
  --color-table-bg-alt: oklch(24% 0.01 256);
572
569
  --color-shadow: oklch(9% 0.01 256 / 10%);
573
570
  --color-text: oklch(75% 0.01 256);
@@ -616,10 +613,6 @@ body > footer .nav {
616
613
  .label-info { background: var(--color-info); }
617
614
  .label-danger { background: var(--color-danger); }
618
615
  .label-warning { background: var(--color-warning); }
619
-
620
- td.box::selection {
621
- background-color: var(--color-selected-text);
622
- }
623
616
  }
624
617
 
625
618
  @media (max-width: 800px) { :root { --font-size: 14px; } }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.0.7
4
+ version: 8.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Perham
@@ -122,6 +122,7 @@ files:
122
122
  - lib/sidekiq/job_retry.rb
123
123
  - lib/sidekiq/job_util.rb
124
124
  - lib/sidekiq/launcher.rb
125
+ - lib/sidekiq/loader.rb
125
126
  - lib/sidekiq/logger.rb
126
127
  - lib/sidekiq/manager.rb
127
128
  - lib/sidekiq/metrics/query.rb