sidekiq 8.1.2 → 8.1.4

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: 0d21db4cf06c0b4d0b5fb770162e289403a76ed996d6c99613cf45083f0e671d
4
- data.tar.gz: 07cf848cd5de112f3153ecd0014991cbf2abf9e66bc0847d2b7845ec7a726329
3
+ metadata.gz: 44df206459c8280090bcf339baa35fbd6ff2ed2dee6ade615e71caed68e34c77
4
+ data.tar.gz: c3e3238d59e0150d5ab67edf02c4a73eae7b7dfd3a19b72db7354ea4ef8b677c
5
5
  SHA512:
6
- metadata.gz: 395d346f8b5227480e4d830b3c1f219a35bb0e2527283f9c00a6a4043c1ebe271998874ae53240b6e7dabca89ce8ad939d0a3969da2b33c3e4155fccbebae9ba
7
- data.tar.gz: 20f44288fd8990544569af7f2a87d90d4ce898b2ce760cc95dd2f30c5a0c4ffc90bcc087cbfa22dbc188a904cc2e817756a83b5b3ffd648f75dc0413d3bec3ff
6
+ metadata.gz: b6d89797767ce01eb6dc86ca8dc88edb96e66c3b407ba66309228ed81b993dfeaf31bf0dd9c00f1a8a26320e983006629330d28101d9a2c735df4897062bccad
7
+ data.tar.gz: a027b597b9b3446555fdf37d6ec0ec35edb208815c73500febbd3f914dcda4da633aa3c05ce678fa90ed25244bcd0b189ad2adab0b733a131f7f14b4a1463ce1
data/Changes.md CHANGED
@@ -2,6 +2,22 @@
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.1.4
6
+ ----------
7
+
8
+ - The TTIN signal is undeprecated as the INFO signal is not supported on Linux
9
+ - Show iteration job state on Busy page [#6978]
10
+
11
+ 8.1.3
12
+ ----------
13
+
14
+ - Fix edge case leading to duplicate, concurrent execution [#6379]
15
+ If 2 Capsules process jobs from the same queue, long-running
16
+ jobs could run in parallel during process shutdown.
17
+ - [SECURITY] Remove as much YAML usage as possible. [#6950]
18
+ Localization files in `web/locales` are now manually parsed.
19
+ Sidekiq::CLI will now only require YAML if you use a `-C` .yml file.
20
+
5
21
  8.1.2
6
22
  ----------
7
23
 
data/lib/sidekiq/api.rb CHANGED
@@ -478,6 +478,10 @@ module Sidekiq
478
478
  self["jid"]
479
479
  end
480
480
 
481
+ def iterable_state
482
+ @iterable_state ||= Sidekiq::IterableJobQuery.new(jid)[jid]
483
+ end
484
+
481
485
  def bid
482
486
  self["bid"]
483
487
  end
@@ -1397,6 +1401,66 @@ module Sidekiq
1397
1401
  Sidekiq.redis { |c| c.hget(key, "data") }
1398
1402
  end
1399
1403
  end
1404
+
1405
+ # Persisted iteration state from Redis for jobs using Sidekiq::IterableJob.
1406
+ class IterableJobQuery
1407
+ def initialize(jids)
1408
+ @cache = bulk_fetch(jids)
1409
+ end
1410
+
1411
+ def [](jid)
1412
+ @cache[jid]
1413
+ end
1414
+
1415
+ private
1416
+
1417
+ # Bulk-fetch iteration state for multiple JIDs in a single Redis pipeline.
1418
+ # Returns a Hash of { jid => IterableJobState } for JIDs that have iteration state.
1419
+ def bulk_fetch(jids)
1420
+ raise ArgumentError unless jids
1421
+ jids_to_fetch = Array(jids).compact.uniq
1422
+ return {} if jids_to_fetch.empty?
1423
+
1424
+ results = Sidekiq.redis do |conn|
1425
+ conn.pipelined do |pipe|
1426
+ jids_to_fetch.each { |jid| pipe.hgetall("it-#{jid}") }
1427
+ end
1428
+ end
1429
+
1430
+ # TODO Requires Ruby 4
1431
+ # states = ::Hash.new(capacity: jids_to_fetch.size)
1432
+ states = {}
1433
+ jids_to_fetch.each_with_index do |jid, i|
1434
+ raw = results[i]
1435
+ next if raw.nil? || raw.empty?
1436
+
1437
+ states[jid] = State.new(jid, raw)
1438
+ end
1439
+ states
1440
+ end
1441
+
1442
+ State = Struct.new(:jid, :raw) do
1443
+ def executions
1444
+ raw["ex"].to_i
1445
+ end
1446
+
1447
+ def runtime
1448
+ raw["rt"].to_f
1449
+ end
1450
+
1451
+ def cursor
1452
+ @cursor ||= begin
1453
+ Sidekiq.load_json(raw["c"])
1454
+ rescue JSON::ParserError
1455
+ @raw["c"]
1456
+ end
1457
+ end
1458
+
1459
+ def cancelled
1460
+ raw["cancelled"]&.to_i
1461
+ end
1462
+ end
1463
+ end
1400
1464
  end
1401
1465
 
1402
1466
  Sidekiq.loader.run_load_hooks(:api)
@@ -51,7 +51,6 @@ module Sidekiq
51
51
  end
52
52
 
53
53
  def stop
54
- fetcher&.bulk_requeue([])
55
54
  end
56
55
 
57
56
  # Sidekiq checks queues in three modes:
data/lib/sidekiq/cli.rb CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  $stdout.sync = true
4
4
 
5
- require "yaml"
6
5
  require "optparse"
7
6
  require "erb"
8
7
  require "fileutils"
@@ -201,9 +200,7 @@ module Sidekiq # :nodoc:
201
200
  cli.logger.info "Received TSTP, no longer accepting new work"
202
201
  cli.launcher.quiet
203
202
  },
204
- # deprecated, use INFO
205
203
  "TTIN" => ->(cli) {
206
- cli.logger.error { "DEPRECATED: Please use the INFO signal for backtraces, support for TTIN will be removed in Sidekiq 9.0." }
207
204
  Thread.list.each do |thread|
208
205
  cli.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread.name}"
209
206
  if thread.backtrace
@@ -214,6 +211,7 @@ module Sidekiq # :nodoc:
214
211
  end
215
212
  },
216
213
  "INFO" => ->(cli) {
214
+ cli.logger.error { "DEPRECATED: the INFO signal does not work on Linux, use TTIN instead." }
217
215
  Thread.list.each do |thread|
218
216
  cli.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread.name}"
219
217
  if thread.backtrace
@@ -409,6 +407,7 @@ module Sidekiq # :nodoc:
409
407
  def parse_config(path)
410
408
  erb = ERB.new(File.read(path), trim_mode: "-")
411
409
  erb.filename = File.expand_path(path)
410
+ require "yaml"
412
411
  opts = YAML.safe_load(erb.result, permitted_classes: [Symbol], aliases: true) || {}
413
412
 
414
413
  if opts.respond_to? :deep_symbolize_keys!
@@ -117,6 +117,11 @@ 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 `:at` option to schedule the jobs for future execution. It
121
+ # accepts either a single Numeric timestamp (or seconds-from-now) applied
122
+ # to every job, or an Array of Numeric values with the same size as `args`
123
+ # to schedule each job at its corresponding time.
124
+ #
120
125
  # Accepts an additional `:spread_interval` option (in seconds) to randomly spread
121
126
  # the jobs schedule times over the specified interval.
122
127
  #
@@ -255,7 +255,7 @@ module Sidekiq
255
255
  # Register a proc to handle any error which occurs within the Sidekiq process.
256
256
  #
257
257
  # Sidekiq.configure_server do |config|
258
- # config.error_handlers << proc {|ex,ctx_hash| MyErrorService.notify(ex, ctx_hash) }
258
+ # config.error_handlers << proc {|ex,ctx_hash,config| MyErrorService.notify(ex, ctx_hash) }
259
259
  # end
260
260
  #
261
261
  # The default error handler logs errors to @logger.
data/lib/sidekiq/job.rb CHANGED
@@ -313,6 +313,11 @@ module Sidekiq
313
313
  #
314
314
  # +items+ must be an Array of Arrays.
315
315
  #
316
+ # The +:at+ option schedules the jobs for future execution. It accepts
317
+ # either a single Numeric timestamp (or seconds-from-now) applied to every
318
+ # job, or an Array of Numeric values with the same size as +items+ to
319
+ # schedule each job at its corresponding time.
320
+ #
316
321
  # For finer-grained control, use `Sidekiq::Client.push_bulk` directly.
317
322
  #
318
323
  # Example (3 Redis round trips):
@@ -325,6 +330,14 @@ module Sidekiq
325
330
  #
326
331
  # SomeJob.perform_bulk([[1], [2], [3]])
327
332
  #
333
+ # Scheduling every job 60 seconds from now (single Numeric +:at+):
334
+ #
335
+ # SomeJob.perform_bulk([[1], [2], [3]], at: 60)
336
+ #
337
+ # Scheduling each job at its own time (Array +:at+):
338
+ #
339
+ # SomeJob.perform_bulk([[1], [2]], at: [Time.now.to_f + 30, Time.now.to_f + 60])
340
+ #
328
341
  def perform_bulk(*args, **kwargs)
329
342
  Setter.new(self, {}).perform_bulk(*args, **kwargs)
330
343
  end
@@ -117,9 +117,7 @@ module Sidekiq
117
117
  private
118
118
 
119
119
  def wait
120
- @sleeper.pop(timeout: random_poll_interval)
121
- rescue Timeout::Error
122
- # TODO move to exception: false
120
+ @sleeper.pop(timeout: random_poll_interval, exception: false)
123
121
  rescue => ex
124
122
  # if poll_interval_average hasn't been calculated yet, we can
125
123
  # raise an error trying to reach Redis.
@@ -225,8 +223,7 @@ module Sidekiq
225
223
  total += INITIAL_WAIT unless @config[:poll_interval_average]
226
224
  total += (5 * rand)
227
225
 
228
- @sleeper.pop(timeout: total)
229
- rescue Timeout::Error
226
+ @sleeper.pop(timeout: total, exception: false)
230
227
  ensure
231
228
  # periodically clean out the `processes` set in Redis which can collect
232
229
  # references to dead processes over time. The process count affects how
@@ -154,7 +154,7 @@ module Sidekiq
154
154
 
155
155
  # Format keys and values with spacing
156
156
  keys_line = keys.map { |k| t(k).to_s.ljust(12) }.join(" ")
157
- values_line = values.map { |v| v.to_s.ljust(12) }.join(" ")
157
+ values_line = values.map { |v| number_with_delimiter(v).ljust(12) }.join(" ")
158
158
 
159
159
  frame.render_widget(
160
160
  tui.paragraph(
@@ -165,10 +165,27 @@ module Sidekiq
165
165
  )
166
166
  end
167
167
 
168
- # TODO Implement I18n delimiter
168
+ # [thousands_separator, decimal_separator] per locale.
169
+ # Locales not listed here use the English default [",", "."].
170
+ NUMERIC_SEPARATORS = {
171
+ # period thousands, comma decimal
172
+ "da" => [".", ","], "de" => [".", ","], "el" => [".", ","],
173
+ "es" => [".", ","], "it" => [".", ","], "nl" => [".", ","],
174
+ "pt" => [".", ","], "pt-BR" => [".", ","], "tr" => [".", ","],
175
+ "vi" => [".", ","],
176
+ # space thousands, comma decimal
177
+ "cs" => [" ", ","], "fr" => [" ", ","], "lt" => [" ", ","],
178
+ "nb" => [" ", ","], "pl" => [" ", ","], "ru" => [" ", ","],
179
+ "sv" => [" ", ","], "uk" => [" ", ","]
180
+ }.freeze
181
+
169
182
  def number_with_delimiter(number, options = {})
170
183
  precision = options[:precision] || 0
171
- number.round(precision)
184
+ rounded = number.round(precision)
185
+ thousands, decimal = NUMERIC_SEPARATORS.fetch(@parent.lang, [",", "."])
186
+ integer_part, decimal_part = rounded.to_s.split(".")
187
+ integer_with_sep = integer_part.gsub(/(\d)(?=(\d{3})+(?!\d))/, "\\1#{thousands}")
188
+ (precision > 0) ? "#{integer_with_sep}#{decimal}#{(decimal_part || "").ljust(precision, "0")}" : integer_with_sep
172
189
  end
173
190
 
174
191
  def format_memory(rss_kb)
data/lib/sidekiq/tui.rb CHANGED
@@ -21,6 +21,8 @@ module Sidekiq
21
21
  REFRESH_INTERVAL_SECONDS = 2
22
22
  LOCALE_DIRECTORIES = [File.expand_path("#{File.dirname(__FILE__)}/../../web/locales")]
23
23
 
24
+ attr_reader :lang
25
+
24
26
  # language is meant to be a locale code, e.g.
25
27
  # LANG=en_US.utf-8
26
28
  def initialize(cfg, language: ENV["LANG"] || "en")
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "8.1.2"
4
+ VERSION = "8.1.4"
5
5
  MAJOR = 8
6
6
 
7
7
  def self.gem_version
@@ -92,6 +92,8 @@ module Sidekiq
92
92
  @count = (url_params("count") || 100).to_i
93
93
  (@current_page, @total_size, @workset) = page_items(workset, url_params("page"), @count)
94
94
 
95
+ @iterable_states = Sidekiq::IterableJobQuery.new(workset.map { |_, _, work| work.job.jid })
96
+
95
97
  erb(:busy)
96
98
  end
97
99
 
@@ -226,6 +228,8 @@ module Sidekiq
226
228
  @retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
227
229
  end
228
230
 
231
+ @iterable_states = Sidekiq::IterableJobQuery.new(@retries.map(&:jid))
232
+
229
233
  erb(:retries)
230
234
  end
231
235
 
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "uri"
4
- require "yaml"
5
4
  require "cgi/escape"
6
5
 
7
6
  module Sidekiq
@@ -73,12 +72,43 @@ module Sidekiq
73
72
  # so extensions can be localized
74
73
  @@strings[lang] ||= config.locales.each_with_object({}) do |path, global|
75
74
  find_locale_files(lang).each do |file|
76
- strs = YAML.safe_load_file(file)
75
+ strs = parse_yaml_new(file)
77
76
  global.merge!(strs[lang])
78
77
  end
79
78
  end
80
79
  end
81
80
 
81
+ # TODO Remove
82
+ def parse_yaml_old(path)
83
+ require "yaml"
84
+ YAML.safe_load_file(path)
85
+ end
86
+
87
+ def parse_yaml_new(path)
88
+ locale = nil
89
+ map = {}
90
+ IO.readlines(path, chomp: true).each do |line|
91
+ case line
92
+ when /\A\s*\#.*/
93
+ # line comment
94
+ when !locale && /\A([a-zA-Z\-_]+):/
95
+ locale = $1
96
+ map[locale] = {}
97
+ when /\A\s+(\w+):\s+(.+)\z/
98
+ # A few values have double quotes to include special characters in YAML.
99
+ # Strip them off manually as our greedy match will include them.
100
+ key = $1
101
+ s = $2
102
+ s = s[1..] if s[0] == "\""
103
+ s = s[0..-2] if s[-1] == "\""
104
+ map[locale][key] = s
105
+ else
106
+ raise ArgumentError, "unable to parse #{path}: #{line}"
107
+ end
108
+ end
109
+ map
110
+ end
111
+
82
112
  def to_json(x)
83
113
  Sidekiq.dump_json(x)
84
114
  end
@@ -534,6 +534,8 @@ input[type="checkbox"] { accent-color: var(--color-primary); }
534
534
 
535
535
  .pagination a { text-decoration: none; }
536
536
 
537
+ .pagination .active a { color: var(--color-primary); font-weight: 700; }
538
+
537
539
  .pagination .disabled { opacity: 0.3; }
538
540
 
539
541
  div:has(.pagination.pull-right) { margin-left: auto; }
data/web/locales/ar.yml CHANGED
@@ -74,7 +74,7 @@ ar:
74
74
  Stop: إيقاف
75
75
  StopAll: إيقاف الكل
76
76
  StopPolling: إيقاف الاستعلامات
77
- TextDirection: 'rtl'
77
+ TextDirection: rtl
78
78
  Thread: نيسب
79
79
  Threads: نياسب
80
80
  ThreeMonths: ثلاثة أشهر
data/web/locales/fa.yml CHANGED
@@ -69,7 +69,7 @@ fa:
69
69
  Stop: توقف
70
70
  StopAll: توقف همه
71
71
  StopPolling: Stop Polling
72
- TextDirection: 'rtl'
72
+ TextDirection: rtl
73
73
  Thread: رشته
74
74
  Threads: رشته ها
75
75
  ThreeMonths: ۳ ماه
data/web/locales/gd.yml CHANGED
@@ -1,6 +1,6 @@
1
- # elements like %{queue} are variables and should not be translated
1
+ # elements like %{queue} are variables and should not be translated
2
2
  gd:
3
- LanguageName: Gaeilge
3
+ LanguageName: Gàidhlig
4
4
  Actions: Gnìomhan
5
5
  AddToQueue: Cuir ris a’ chiutha
6
6
  AddAllToQueue: Cuir a h-uile ris a’ chiutha
@@ -36,6 +36,8 @@ gd:
36
36
  Jobs: Obraichean
37
37
  Kill: Marbh
38
38
  KillAll: Marbh na h-uile
39
+ Language: Cànan
40
+ LastDashboardUpdateTemplateLiteral: "An t-ath-nuadhachadh mu dheireadh: Air pròiseasadh: PROCESSED_COUNT. Air fàilligeadh: FAILED_COUNT."
39
41
  LastRetry: An oidhirp mu dheireadh
40
42
  Latency: Foillidheachd
41
43
  LivePoll: Ath-nuadhachadh beò
@@ -55,6 +57,7 @@ gd:
55
57
  PeakMemoryUsage: Bàrr cleachdadh a’ chuimhne
56
58
  Plugins: Plugain
57
59
  PollingInterval: Eadaramh an ath-nuadhachaidh
60
+ PollingIntervalMilliseconds: Milidiogan eadaramh an ath-nuadhachaidh
58
61
  Process: Pròiseas
59
62
  Processed: Air pròiseasadh
60
63
  Processes: Pròiseasan
@@ -98,3 +101,10 @@ gd:
98
101
  AvgExecutionTime: Ùine cuibheasach nan gnìomhan
99
102
  Context: Co-theacsa
100
103
  NoJobMetricsFound: Cha deach meatraigeachd o chionn goirid air obair a lorg
104
+ Filter: Criathraich
105
+ AnyJobContent: Susbaint obrach sam bith
106
+ Profiles: Pròifilean
107
+ Data: Dàta
108
+ View: Seall
109
+ Token: Tòcan
110
+ ElapsedTime: An ùine a chaidh seachad
data/web/locales/he.yml CHANGED
@@ -69,7 +69,7 @@ he:
69
69
  Stop: עצור
70
70
  StopAll: עצור הכל
71
71
  StopPolling: עצור תשאול
72
- TextDirection: 'rtl'
72
+ TextDirection: rtl
73
73
  Thread: חוט
74
74
  Threads: חוטים
75
75
  ThreeMonths: 3 חדשים
@@ -1,4 +1,4 @@
1
- "pt-BR":
1
+ pt-BR:
2
2
  LanguageName: Português (Brasil)
3
3
  Actions: Ações
4
4
  AddToQueue: Adicionar à fila
data/web/locales/ur.yml CHANGED
@@ -69,7 +69,7 @@ ur:
69
69
  Stop: بند کرو
70
70
  StopAll: ﺗﻤﺎﻡ ﺑﻨﺪ کﺭﻭ
71
71
  StopPolling: ﺑﺮاﮦ ﺭاﺳﺖ روکيے
72
- TextDirection: 'rtl'
72
+ TextDirection: rtl
73
73
  Thread: موضوع
74
74
  Threads: موضوع
75
75
  ThreeMonths: تین ماہ
@@ -34,6 +34,14 @@
34
34
  <code><%= job.jid %></code>
35
35
  </td>
36
36
  </tr>
37
+ <% if (state = job.iterable_state) %>
38
+ <tr>
39
+ <th>Iteration</th>
40
+ <td>
41
+ <code>cursor=<%= h(state.cursor.inspect) %>; exec=<%= state.executions %>; rt=<%= number_with_delimiter(state.runtime, precision: 3) %>s</code>
42
+ </td>
43
+ </tr>
44
+ <% end %>
37
45
  <% if job.bid %>
38
46
  <tr>
39
47
  <th>BID</th>
@@ -10,7 +10,7 @@
10
10
  <a href="<%= url %>?<%= qparams(page: @current_page - 1) %>"><%= @current_page - 1 %></a>
11
11
  </li>
12
12
  <% end %>
13
- <li class="disabled">
13
+ <li class="active">
14
14
  <a href="<%= url %>?<%= qparams(page: @current_page) %>"><%= @current_page %></a>
15
15
  </li>
16
16
  <% if @total_size > @current_page * @count %>
@@ -138,7 +138,12 @@
138
138
  <td>
139
139
  <code><div class="args"><%= display_args(job.display_args) %></div></code>
140
140
  </td>
141
- <td><%= relative_time(work.run_at) %></td>
141
+ <td>
142
+ <%= relative_time(work.run_at) %>
143
+ <% if (state = @iterable_states[job.jid]) %>
144
+ <div><small>cursor=<%= h(truncate(state.cursor.inspect, 100)) %>; exec=<%= state.executions %>; rt=<%= number_with_delimiter(state.runtime, precision: 3) %>s</small></div>
145
+ <% end %>
146
+ </td>
142
147
  </tr>
143
148
  <% end %>
144
149
  </table>
@@ -52,6 +52,9 @@
52
52
  </td>
53
53
  <td>
54
54
  <div><a href="<%= root_path %>retries/<%= job_params(entry.item, entry.score) %>"><%= h truncate("#{entry['error_class']}: #{entry['error_message']}", 200) %></a></div>
55
+ <% if (state = @iterable_states[entry.jid]) %>
56
+ <div><small>cursor=<%= h(truncate(state.cursor.inspect, 100)) %>; exec=<%= state.executions %>; rt=<%= number_with_delimiter(state.runtime, precision: 3) %>s</small></div>
57
+ <% end %>
55
58
  </td>
56
59
  </tr>
57
60
  <% end %>
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.1.2
4
+ version: 8.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Perham