sidekiq 6.5.1 → 7.3.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +376 -12
  3. data/README.md +43 -35
  4. data/bin/multi_queue_bench +271 -0
  5. data/bin/sidekiq +3 -8
  6. data/bin/sidekiqload +213 -118
  7. data/bin/sidekiqmon +3 -0
  8. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +88 -0
  9. data/lib/generators/sidekiq/job_generator.rb +2 -0
  10. data/lib/sidekiq/api.rb +378 -173
  11. data/lib/sidekiq/capsule.rb +132 -0
  12. data/lib/sidekiq/cli.rb +61 -63
  13. data/lib/sidekiq/client.rb +89 -40
  14. data/lib/sidekiq/component.rb +6 -2
  15. data/lib/sidekiq/config.rb +305 -0
  16. data/lib/sidekiq/deploy.rb +64 -0
  17. data/lib/sidekiq/embedded.rb +63 -0
  18. data/lib/sidekiq/fetch.rb +11 -14
  19. data/lib/sidekiq/iterable_job.rb +55 -0
  20. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  21. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  22. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  23. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  24. data/lib/sidekiq/job/iterable.rb +294 -0
  25. data/lib/sidekiq/job.rb +382 -10
  26. data/lib/sidekiq/job_logger.rb +8 -7
  27. data/lib/sidekiq/job_retry.rb +89 -46
  28. data/lib/sidekiq/job_util.rb +53 -15
  29. data/lib/sidekiq/launcher.rb +77 -69
  30. data/lib/sidekiq/logger.rb +2 -27
  31. data/lib/sidekiq/manager.rb +9 -11
  32. data/lib/sidekiq/metrics/query.rb +158 -0
  33. data/lib/sidekiq/metrics/shared.rb +106 -0
  34. data/lib/sidekiq/metrics/tracking.rb +148 -0
  35. data/lib/sidekiq/middleware/chain.rb +84 -48
  36. data/lib/sidekiq/middleware/current_attributes.rb +87 -20
  37. data/lib/sidekiq/middleware/modules.rb +2 -0
  38. data/lib/sidekiq/monitor.rb +19 -5
  39. data/lib/sidekiq/paginator.rb +11 -3
  40. data/lib/sidekiq/processor.rb +67 -56
  41. data/lib/sidekiq/rails.rb +22 -16
  42. data/lib/sidekiq/redis_client_adapter.rb +31 -71
  43. data/lib/sidekiq/redis_connection.rb +44 -117
  44. data/lib/sidekiq/ring_buffer.rb +2 -0
  45. data/lib/sidekiq/scheduled.rb +62 -35
  46. data/lib/sidekiq/systemd.rb +2 -0
  47. data/lib/sidekiq/testing.rb +37 -46
  48. data/lib/sidekiq/transaction_aware_client.rb +11 -5
  49. data/lib/sidekiq/version.rb +6 -1
  50. data/lib/sidekiq/web/action.rb +15 -5
  51. data/lib/sidekiq/web/application.rb +94 -24
  52. data/lib/sidekiq/web/csrf_protection.rb +10 -7
  53. data/lib/sidekiq/web/helpers.rb +118 -45
  54. data/lib/sidekiq/web/router.rb +5 -2
  55. data/lib/sidekiq/web.rb +67 -15
  56. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  57. data/lib/sidekiq.rb +78 -266
  58. data/sidekiq.gemspec +12 -10
  59. data/web/assets/javascripts/application.js +46 -1
  60. data/web/assets/javascripts/base-charts.js +106 -0
  61. data/web/assets/javascripts/chart.min.js +13 -0
  62. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  63. data/web/assets/javascripts/dashboard-charts.js +192 -0
  64. data/web/assets/javascripts/dashboard.js +11 -250
  65. data/web/assets/javascripts/metrics.js +298 -0
  66. data/web/assets/stylesheets/application-dark.css +4 -0
  67. data/web/assets/stylesheets/application-rtl.css +10 -89
  68. data/web/assets/stylesheets/application.css +98 -295
  69. data/web/locales/ar.yml +70 -70
  70. data/web/locales/cs.yml +62 -62
  71. data/web/locales/da.yml +60 -53
  72. data/web/locales/de.yml +65 -65
  73. data/web/locales/el.yml +43 -24
  74. data/web/locales/en.yml +83 -69
  75. data/web/locales/es.yml +68 -68
  76. data/web/locales/fa.yml +65 -65
  77. data/web/locales/fr.yml +80 -67
  78. data/web/locales/gd.yml +98 -0
  79. data/web/locales/he.yml +65 -64
  80. data/web/locales/hi.yml +59 -59
  81. data/web/locales/it.yml +85 -54
  82. data/web/locales/ja.yml +72 -68
  83. data/web/locales/ko.yml +52 -52
  84. data/web/locales/lt.yml +66 -66
  85. data/web/locales/nb.yml +61 -61
  86. data/web/locales/nl.yml +52 -52
  87. data/web/locales/pl.yml +45 -45
  88. data/web/locales/pt-br.yml +78 -69
  89. data/web/locales/pt.yml +51 -51
  90. data/web/locales/ru.yml +67 -66
  91. data/web/locales/sv.yml +53 -53
  92. data/web/locales/ta.yml +60 -60
  93. data/web/locales/tr.yml +100 -0
  94. data/web/locales/uk.yml +85 -61
  95. data/web/locales/ur.yml +64 -64
  96. data/web/locales/vi.yml +67 -67
  97. data/web/locales/zh-cn.yml +42 -16
  98. data/web/locales/zh-tw.yml +41 -8
  99. data/web/views/_footer.erb +17 -2
  100. data/web/views/_job_info.erb +18 -2
  101. data/web/views/_metrics_period_select.erb +12 -0
  102. data/web/views/_nav.erb +1 -1
  103. data/web/views/_paging.erb +2 -0
  104. data/web/views/_poll_link.erb +1 -1
  105. data/web/views/_summary.erb +7 -7
  106. data/web/views/busy.erb +49 -33
  107. data/web/views/dashboard.erb +28 -6
  108. data/web/views/filtering.erb +6 -0
  109. data/web/views/layout.erb +6 -6
  110. data/web/views/metrics.erb +90 -0
  111. data/web/views/metrics_for_job.erb +59 -0
  112. data/web/views/morgue.erb +5 -9
  113. data/web/views/queue.erb +15 -15
  114. data/web/views/queues.erb +9 -3
  115. data/web/views/retries.erb +5 -9
  116. data/web/views/scheduled.erb +12 -13
  117. metadata +61 -26
  118. data/lib/sidekiq/.DS_Store +0 -0
  119. data/lib/sidekiq/delay.rb +0 -43
  120. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  121. data/lib/sidekiq/extensions/active_record.rb +0 -43
  122. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  123. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  124. data/lib/sidekiq/worker.rb +0 -367
  125. /data/{LICENSE → LICENSE.txt} +0 -0
data/lib/sidekiq.rb CHANGED
@@ -1,15 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "sidekiq/version"
4
- fail "Sidekiq #{Sidekiq::VERSION} does not support Ruby versions below 2.5.0." if RUBY_PLATFORM != "java" && Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.5.0")
4
+ fail "Sidekiq #{Sidekiq::VERSION} does not support Ruby versions below 2.7.0." if RUBY_PLATFORM != "java" && Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.7.0")
5
5
 
6
+ begin
7
+ require "sidekiq-ent/version"
8
+ fail <<~EOM if Gem::Version.new(Sidekiq::Enterprise::VERSION).segments[0] != Sidekiq::MAJOR
9
+
10
+ Sidekiq Enterprise #{Sidekiq::Enterprise::VERSION} does not work with Sidekiq #{Sidekiq::VERSION}.
11
+ Starting with Sidekiq 7, major versions are synchronized so Sidekiq Enterprise 7 works with Sidekiq 7.
12
+ Use `bundle up sidekiq-ent` to upgrade.
13
+
14
+ EOM
15
+ rescue LoadError
16
+ end
17
+
18
+ begin
19
+ require "sidekiq/pro/version"
20
+ fail <<~EOM if Gem::Version.new(Sidekiq::Pro::VERSION).segments[0] != Sidekiq::MAJOR
21
+
22
+ Sidekiq Pro #{Sidekiq::Pro::VERSION} does not work with Sidekiq #{Sidekiq::VERSION}.
23
+ Starting with Sidekiq 7, major versions are synchronized so Sidekiq Pro 7 works with Sidekiq 7.
24
+ Use `bundle up sidekiq-pro` to upgrade.
25
+
26
+ EOM
27
+ rescue LoadError
28
+ end
29
+
30
+ require "sidekiq/config"
6
31
  require "sidekiq/logger"
7
32
  require "sidekiq/client"
8
33
  require "sidekiq/transaction_aware_client"
9
- require "sidekiq/worker"
10
34
  require "sidekiq/job"
11
- require "sidekiq/redis_connection"
12
- require "sidekiq/delay"
35
+ require "sidekiq/iterable_job"
36
+ require "sidekiq/worker_compatibility_alias"
37
+ require "sidekiq/redis_client_adapter"
13
38
 
14
39
  require "json"
15
40
 
@@ -17,312 +42,99 @@ module Sidekiq
17
42
  NAME = "Sidekiq"
18
43
  LICENSE = "See LICENSE and the LGPL-3.0 for licensing details."
19
44
 
20
- DEFAULTS = {
21
- queues: [],
22
- labels: [],
23
- concurrency: 10,
24
- require: ".",
25
- strict: true,
26
- environment: nil,
27
- timeout: 25,
28
- poll_interval_average: nil,
29
- average_scheduled_poll_interval: 5,
30
- on_complex_arguments: :warn,
31
- error_handlers: [],
32
- death_handlers: [],
33
- lifecycle_events: {
34
- startup: [],
35
- quiet: [],
36
- shutdown: [],
37
- heartbeat: []
38
- },
39
- dead_max_jobs: 10_000,
40
- dead_timeout_in_seconds: 180 * 24 * 60 * 60, # 6 months
41
- reloader: proc { |&block| block.call }
42
- }
43
-
44
- FAKE_INFO = {
45
- "redis_version" => "9.9.9",
46
- "uptime_in_days" => "9999",
47
- "connected_clients" => "9999",
48
- "used_memory_human" => "9P",
49
- "used_memory_peak_human" => "9P"
50
- }
51
-
52
45
  def self.❨╯°□°❩╯︵┻━┻
53
- puts "Calm down, yo."
54
- end
55
-
56
- # config.concurrency = 5
57
- def self.concurrency=(val)
58
- self[:concurrency] = Integer(val)
59
- end
60
-
61
- # config.queues = %w( high default low ) # strict
62
- # config.queues = %w( high,3 default,2 low,1 ) # weighted
63
- # config.queues = %w( feature1,1 feature2,1 feature3,1 ) # random
64
- #
65
- # With weighted priority, queue will be checked first (weight / total) of the time.
66
- # high will be checked first (3/6) or 50% of the time.
67
- # I'd recommend setting weights between 1-10. Weights in the hundreds or thousands
68
- # are ridiculous and unnecessarily expensive. You can get random queue ordering
69
- # by explicitly setting all weights to 1.
70
- def self.queues=(val)
71
- self[:queues] = Array(val).each_with_object([]) do |qstr, memo|
72
- name, weight = qstr.split(",")
73
- self[:strict] = false if weight.to_i > 0
74
- [weight.to_i, 1].max.times do
75
- memo << name
76
- end
77
- end
78
- end
79
-
80
- ### Private APIs
81
- def self.default_error_handler(ex, ctx)
82
- logger.warn(dump_json(ctx)) unless ctx.empty?
83
- logger.warn("#{ex.class.name}: #{ex.message}")
84
- logger.warn(ex.backtrace.join("\n")) unless ex.backtrace.nil?
85
- end
86
-
87
- @config = DEFAULTS.dup
88
- def self.options
89
- logger.warn "`config.options[:key] = value` is deprecated, use `config[:key] = value`: #{caller(1..2)}"
90
- @config
91
- end
92
-
93
- def self.options=(opts)
94
- logger.warn "config.options = hash` is deprecated, use `config.merge!(hash)`: #{caller(1..2)}"
95
- @config = opts
96
- end
97
-
98
- def self.[](key)
99
- @config[key]
100
- end
101
-
102
- def self.[]=(key, val)
103
- @config[key] = val
104
- end
105
-
106
- def self.merge!(hash)
107
- @config.merge!(hash)
108
- end
109
-
110
- def self.fetch(*args, &block)
111
- @config.fetch(*args, &block)
112
- end
113
-
114
- def self.handle_exception(ex, ctx = {})
115
- self[:error_handlers].each do |handler|
116
- handler.call(ex, ctx)
117
- rescue => ex
118
- logger.error "!!! ERROR HANDLER THREW AN ERROR !!!"
119
- logger.error ex
120
- logger.error ex.backtrace.join("\n") unless ex.backtrace.nil?
121
- end
122
- end
123
- ###
124
-
125
- ##
126
- # Configuration for Sidekiq server, use like:
127
- #
128
- # Sidekiq.configure_server do |config|
129
- # config.server_middleware do |chain|
130
- # chain.add MyServerHook
131
- # end
132
- # end
133
- def self.configure_server
134
- yield self if server?
135
- end
136
-
137
- ##
138
- # Configuration for Sidekiq client, use like:
139
- #
140
- # Sidekiq.configure_client do |config|
141
- # config.redis = { size: 1, url: 'redis://myhost:8877/0' }
142
- # end
143
- def self.configure_client
144
- yield self unless server?
46
+ puts "Take a deep breath and count to ten..."
145
47
  end
146
48
 
147
49
  def self.server?
148
50
  defined?(Sidekiq::CLI)
149
51
  end
150
52
 
151
- def self.redis
152
- raise ArgumentError, "requires a block" unless block_given?
153
- redis_pool.with do |conn|
154
- retryable = true
155
- begin
156
- yield conn
157
- rescue RedisConnection.adapter::BaseError => ex
158
- # 2550 Failover can cause the server to become a replica, need
159
- # to disconnect and reopen the socket to get back to the primary.
160
- # 4495 Use the same logic if we have a "Not enough replicas" error from the primary
161
- # 4985 Use the same logic when a blocking command is force-unblocked
162
- # The same retry logic is also used in client.rb
163
- if retryable && ex.message =~ /READONLY|NOREPLICAS|UNBLOCKED/
164
- conn.disconnect!
165
- retryable = false
166
- retry
167
- end
168
- raise
169
- end
170
- end
171
- end
172
-
173
- def self.redis_info
174
- redis do |conn|
175
- # admin commands can't go through redis-namespace starting
176
- # in redis-namespace 2.0
177
- if conn.respond_to?(:namespace)
178
- conn.redis.info
179
- else
180
- conn.info
181
- end
182
- rescue RedisConnection.adapter::CommandError => ex
183
- # 2850 return fake version when INFO command has (probably) been renamed
184
- raise unless /unknown command/.match?(ex.message)
185
- FAKE_INFO
186
- end
53
+ def self.load_json(string)
54
+ JSON.parse(string)
187
55
  end
188
56
 
189
- def self.redis_pool
190
- @redis ||= RedisConnection.create
57
+ def self.dump_json(object)
58
+ JSON.generate(object)
191
59
  end
192
60
 
193
- def self.redis=(hash)
194
- @redis = if hash.is_a?(ConnectionPool)
195
- hash
196
- else
197
- RedisConnection.create(hash)
198
- end
61
+ def self.pro?
62
+ defined?(Sidekiq::Pro)
199
63
  end
200
64
 
201
- def self.client_middleware
202
- @client_chain ||= Middleware::Chain.new(self)
203
- yield @client_chain if block_given?
204
- @client_chain
65
+ def self.ent?
66
+ defined?(Sidekiq::Enterprise)
205
67
  end
206
68
 
207
- def self.server_middleware
208
- @server_chain ||= default_server_middleware
209
- yield @server_chain if block_given?
210
- @server_chain
69
+ def self.redis_pool
70
+ (Thread.current[:sidekiq_capsule] || default_configuration).redis_pool
211
71
  end
212
72
 
213
- def self.default_server_middleware
214
- Middleware::Chain.new(self)
73
+ def self.redis(&block)
74
+ (Thread.current[:sidekiq_capsule] || default_configuration).redis(&block)
215
75
  end
216
76
 
217
- def self.default_worker_options=(hash) # deprecated
218
- @default_job_options = default_job_options.merge(hash.transform_keys(&:to_s))
77
+ def self.strict_args!(mode = :raise)
78
+ Sidekiq::Config::DEFAULTS[:on_complex_arguments] = mode
219
79
  end
220
80
 
221
81
  def self.default_job_options=(hash)
222
82
  @default_job_options = default_job_options.merge(hash.transform_keys(&:to_s))
223
83
  end
224
84
 
225
- def self.default_worker_options # deprecated
226
- @default_job_options ||= {"retry" => true, "queue" => "default"}
227
- end
228
-
229
85
  def self.default_job_options
230
86
  @default_job_options ||= {"retry" => true, "queue" => "default"}
231
87
  end
232
88
 
233
- ##
234
- # Death handlers are called when all retries for a job have been exhausted and
235
- # the job dies. It's the notification to your application
236
- # that this job will not succeed without manual intervention.
237
- #
238
- # Sidekiq.configure_server do |config|
239
- # config.death_handlers << ->(job, ex) do
240
- # end
241
- # end
242
- def self.death_handlers
243
- self[:death_handlers]
244
- end
245
-
246
- def self.load_json(string)
247
- JSON.parse(string)
248
- end
249
-
250
- def self.dump_json(object)
251
- JSON.generate(object)
252
- end
253
-
254
- def self.log_formatter
255
- @log_formatter ||= if ENV["DYNO"]
256
- Sidekiq::Logger::Formatters::WithoutTimestamp.new
257
- else
258
- Sidekiq::Logger::Formatters::Pretty.new
259
- end
260
- end
261
-
262
- def self.log_formatter=(log_formatter)
263
- @log_formatter = log_formatter
264
- logger.formatter = log_formatter
89
+ def self.default_configuration
90
+ @config ||= Sidekiq::Config.new
265
91
  end
266
92
 
267
93
  def self.logger
268
- @logger ||= Sidekiq::Logger.new($stdout, level: :info)
269
- end
270
-
271
- def self.logger=(logger)
272
- if logger.nil?
273
- self.logger.level = Logger::FATAL
274
- return self.logger
275
- end
276
-
277
- logger.extend(Sidekiq::LoggingUtils)
278
-
279
- @logger = logger
280
- end
281
-
282
- def self.pro?
283
- defined?(Sidekiq::Pro)
94
+ default_configuration.logger
284
95
  end
285
96
 
286
- def self.ent?
287
- defined?(Sidekiq::Enterprise)
97
+ def self.configure_server(&block)
98
+ (@config_blocks ||= []) << block
99
+ yield default_configuration if server?
288
100
  end
289
101
 
290
- # How frequently Redis should be checked by a random Sidekiq process for
291
- # scheduled and retriable jobs. Each individual process will take turns by
292
- # waiting some multiple of this value.
293
- #
294
- # See sidekiq/scheduled.rb for an in-depth explanation of this value
295
- def self.average_scheduled_poll_interval=(interval)
296
- self[:average_scheduled_poll_interval] = interval
102
+ def self.freeze!
103
+ @frozen = true
104
+ @config_blocks = nil
105
+ default_configuration.freeze!
297
106
  end
298
107
 
299
- # Register a proc to handle any error which occurs within the Sidekiq process.
108
+ # Creates a Sidekiq::Config instance that is more tuned for embedding
109
+ # within an arbitrary Ruby process. Notably it reduces concurrency by
110
+ # default so there is less contention for CPU time with other threads.
300
111
  #
301
- # Sidekiq.configure_server do |config|
302
- # config.error_handlers << proc {|ex,ctx_hash| MyErrorService.notify(ex, ctx_hash) }
112
+ # instance = Sidekiq.configure_embed do |config|
113
+ # config.queues = %w[critical default low]
303
114
  # end
115
+ # instance.run
116
+ # sleep 10
117
+ # instance.stop
304
118
  #
305
- # The default error handler logs errors to Sidekiq.logger.
306
- def self.error_handlers
307
- self[:error_handlers]
308
- end
309
-
310
- # Register a block to run at a point in the Sidekiq lifecycle.
311
- # :startup, :quiet or :shutdown are valid events.
119
+ # NB: it is really easy to overload a Ruby process with threads due to the GIL.
120
+ # I do not recommend setting concurrency higher than 2-3.
312
121
  #
313
- # Sidekiq.configure_server do |config|
314
- # config.on(:shutdown) do
315
- # puts "Goodbye cruel world!"
316
- # end
317
- # end
318
- def self.on(event, &block)
319
- raise ArgumentError, "Symbols only please: #{event}" unless event.is_a?(Symbol)
320
- raise ArgumentError, "Invalid event name: #{event}" unless self[:lifecycle_events].key?(event)
321
- self[:lifecycle_events][event] << block
122
+ # NB: Sidekiq only supports one instance in memory. You will get undefined behavior
123
+ # if you try to embed Sidekiq twice in the same process.
124
+ def self.configure_embed(&block)
125
+ raise "Sidekiq global configuration is frozen, you must create all embedded instances BEFORE calling `run`" if @frozen
126
+
127
+ require "sidekiq/embedded"
128
+ cfg = default_configuration
129
+ cfg.concurrency = 2
130
+ @config_blocks&.each { |block| block.call(cfg) }
131
+ yield cfg
132
+
133
+ Sidekiq::Embedded.new(cfg)
322
134
  end
323
135
 
324
- def self.strict_args!(mode = :raise)
325
- self[:on_complex_arguments] = mode
136
+ def self.configure_client
137
+ yield default_configuration unless server?
326
138
  end
327
139
 
328
140
  # We are shutting down Sidekiq but what about threads that
data/sidekiq.gemspec CHANGED
@@ -2,27 +2,29 @@ require_relative "lib/sidekiq/version"
2
2
 
3
3
  Gem::Specification.new do |gem|
4
4
  gem.authors = ["Mike Perham"]
5
- gem.email = ["mperham@gmail.com"]
5
+ gem.email = ["info@contribsys.com"]
6
6
  gem.summary = "Simple, efficient background processing for Ruby"
7
7
  gem.description = "Simple, efficient background processing for Ruby."
8
8
  gem.homepage = "https://sidekiq.org"
9
9
  gem.license = "LGPL-3.0"
10
10
 
11
11
  gem.executables = ["sidekiq", "sidekiqmon"]
12
- gem.files = ["sidekiq.gemspec", "README.md", "Changes.md", "LICENSE"] + `git ls-files | grep -E '^(bin|lib|web)'`.split("\n")
12
+ gem.files = %w[sidekiq.gemspec README.md Changes.md LICENSE.txt] + `git ls-files | grep -E '^(bin|lib|web)'`.split("\n")
13
13
  gem.name = "sidekiq"
14
14
  gem.version = Sidekiq::VERSION
15
- gem.required_ruby_version = ">= 2.5.0"
15
+ gem.required_ruby_version = ">= 2.7.0"
16
16
 
17
17
  gem.metadata = {
18
18
  "homepage_uri" => "https://sidekiq.org",
19
- "bug_tracker_uri" => "https://github.com/mperham/sidekiq/issues",
20
- "documentation_uri" => "https://github.com/mperham/sidekiq/wiki",
21
- "changelog_uri" => "https://github.com/mperham/sidekiq/blob/main/Changes.md",
22
- "source_code_uri" => "https://github.com/mperham/sidekiq"
19
+ "bug_tracker_uri" => "https://github.com/sidekiq/sidekiq/issues",
20
+ "documentation_uri" => "https://github.com/sidekiq/sidekiq/wiki",
21
+ "changelog_uri" => "https://github.com/sidekiq/sidekiq/blob/main/Changes.md",
22
+ "source_code_uri" => "https://github.com/sidekiq/sidekiq",
23
+ "rubygems_mfa_required" => "true"
23
24
  }
24
25
 
25
- gem.add_dependency "redis", ">= 4.2.0"
26
- gem.add_dependency "connection_pool", ">= 2.2.2"
27
- gem.add_dependency "rack", "~> 2.0"
26
+ gem.add_dependency "redis-client", ">= 0.22.2"
27
+ gem.add_dependency "connection_pool", ">= 2.3.0"
28
+ gem.add_dependency "rack", ">= 2.2.4"
29
+ gem.add_dependency "logger"
28
30
  end
@@ -31,7 +31,10 @@ function addListeners() {
31
31
  node.addEventListener("click", addDataToggleListeners)
32
32
  })
33
33
 
34
+ addShiftClickListeners()
34
35
  updateFuzzyTimes();
36
+ updateNumbers();
37
+ updateProgressBars();
35
38
  setLivePollFromUrl();
36
39
 
37
40
  var buttons = document.querySelectorAll(".live-poll");
@@ -45,6 +48,8 @@ function addListeners() {
45
48
  scheduleLivePoll();
46
49
  }
47
50
  }
51
+
52
+ document.getElementById("locale-select").addEventListener("change", updateLocale);
48
53
  }
49
54
 
50
55
  function addPollingListeners(_event) {
@@ -63,7 +68,7 @@ function addPollingListeners(_event) {
63
68
  function addDataToggleListeners(event) {
64
69
  var source = event.target || event.srcElement;
65
70
  var targName = source.getAttribute("data-toggle");
66
- var full = document.getElementById(targName + "_full");
71
+ var full = document.getElementById(targName);
67
72
  if (full.style.display == "block") {
68
73
  full.style.display = 'none';
69
74
  } else {
@@ -71,6 +76,23 @@ function addDataToggleListeners(event) {
71
76
  }
72
77
  }
73
78
 
79
+ function addShiftClickListeners() {
80
+ let checkboxes = Array.from(document.querySelectorAll(".shift_clickable"));
81
+ let lastChecked = null;
82
+ checkboxes.forEach(checkbox => {
83
+ checkbox.addEventListener("click", (e) => {
84
+ if (e.shiftKey && lastChecked) {
85
+ let myIndex = checkboxes.indexOf(checkbox);
86
+ let lastIndex = checkboxes.indexOf(lastChecked);
87
+ let [min, max] = [myIndex, lastIndex].sort();
88
+ let newState = checkbox.checked;
89
+ checkboxes.slice(min, max).forEach(c => c.checked = newState);
90
+ }
91
+ lastChecked = checkbox;
92
+ });
93
+ });
94
+ }
95
+
74
96
  function updateFuzzyTimes() {
75
97
  var locale = document.body.getAttribute("data-locale");
76
98
  var parts = locale.split('-');
@@ -84,6 +106,20 @@ function updateFuzzyTimes() {
84
106
  t.cancel();
85
107
  }
86
108
 
109
+ function updateNumbers() {
110
+ document.querySelectorAll("[data-nwp]").forEach(node => {
111
+ let number = parseFloat(node.textContent);
112
+ let precision = parseInt(node.dataset["nwp"] || 0);
113
+ if (typeof number === "number") {
114
+ let formatted = number.toLocaleString(undefined, {
115
+ minimumFractionDigits: precision,
116
+ maximumFractionDigits: precision,
117
+ });
118
+ node.textContent = formatted;
119
+ }
120
+ });
121
+ }
122
+
87
123
  function setLivePollFromUrl() {
88
124
  var url_params = new URL(window.location.href).searchParams
89
125
 
@@ -122,6 +158,7 @@ function checkResponse(resp) {
122
158
 
123
159
  function scheduleLivePoll() {
124
160
  let ti = parseInt(localStorage.sidekiqTimeInterval) || 5000;
161
+ if (ti < 2000) { ti = 2000 }
125
162
  livePollTimer = setTimeout(livePollCallback, ti);
126
163
  }
127
164
 
@@ -141,3 +178,11 @@ function replacePage(text) {
141
178
  function showError(error) {
142
179
  console.error(error)
143
180
  }
181
+
182
+ function updateLocale(event) {
183
+ event.target.form.submit();
184
+ }
185
+
186
+ function updateProgressBars() {
187
+ document.querySelectorAll('.progress-bar').forEach(bar => { bar.style.width = bar.dataset.width + "%"})
188
+ }
@@ -0,0 +1,106 @@
1
+ if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
2
+ Chart.defaults.borderColor = "#333";
3
+ Chart.defaults.color = "#aaa";
4
+ }
5
+
6
+ class Colors {
7
+ constructor() {
8
+ this.assignments = {};
9
+ this.success = "#006f68";
10
+ this.failure = "#af0014";
11
+ this.fallback = "#999";
12
+ this.primary = "#537bc4";
13
+ this.available = [
14
+ // Colors taken from https://www.chartjs.org/docs/latest/samples/utils.html
15
+ "#537bc4",
16
+ "#4dc9f6",
17
+ "#f67019",
18
+ "#f53794",
19
+ "#acc236",
20
+ "#166a8f",
21
+ "#00a950",
22
+ "#58595b",
23
+ "#8549ba",
24
+ "#991b1b",
25
+ ];
26
+ }
27
+
28
+ checkOut(assignee) {
29
+ const color =
30
+ this.assignments[assignee] || this.available.shift() || this.fallback;
31
+ this.assignments[assignee] = color;
32
+ return color;
33
+ }
34
+
35
+ checkIn(assignee) {
36
+ const color = this.assignments[assignee];
37
+ delete this.assignments[assignee];
38
+
39
+ if (color && color != this.fallback) {
40
+ this.available.unshift(color);
41
+ }
42
+ }
43
+ }
44
+
45
+ class BaseChart {
46
+ constructor(el, options) {
47
+ this.el = el;
48
+ this.options = options;
49
+ this.colors = new Colors();
50
+ }
51
+
52
+ init() {
53
+ this.chart = new Chart(this.el, {
54
+ type: this.options.chartType,
55
+ data: { labels: this.options.labels, datasets: this.datasets },
56
+ options: this.chartOptions,
57
+ });
58
+ }
59
+
60
+ update() {
61
+ this.chart.options = this.chartOptions;
62
+ this.chart.update();
63
+ }
64
+
65
+ get chartOptions() {
66
+ let chartOptions = {
67
+ interaction: {
68
+ mode: "nearest",
69
+ axis: "x",
70
+ intersect: false,
71
+ },
72
+ scales: {
73
+ x: {
74
+ ticks: {
75
+ autoSkipPadding: 10,
76
+ },
77
+ },
78
+ },
79
+ plugins: {
80
+ legend: {
81
+ display: false,
82
+ },
83
+ annotation: {
84
+ annotations: {},
85
+ },
86
+ tooltip: {
87
+ animation: false,
88
+ },
89
+ },
90
+ };
91
+
92
+ if (this.options.marks) {
93
+ this.options.marks.forEach(([bucket, label], i) => {
94
+ chartOptions.plugins.annotation.annotations[`deploy-${i}`] = {
95
+ type: "line",
96
+ xMin: bucket,
97
+ xMax: bucket,
98
+ borderColor: "rgba(220, 38, 38, 0.4)",
99
+ borderWidth: 2,
100
+ };
101
+ });
102
+ }
103
+
104
+ return chartOptions;
105
+ }
106
+ }