sidekiq 6.4.0 → 7.1.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sidekiq might be problematic. Click here for more details.

Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +232 -12
  3. data/README.md +44 -31
  4. data/bin/sidekiq +4 -9
  5. data/bin/sidekiqload +207 -117
  6. data/bin/sidekiqmon +4 -1
  7. data/lib/sidekiq/api.rb +329 -188
  8. data/lib/sidekiq/capsule.rb +127 -0
  9. data/lib/sidekiq/cli.rb +85 -81
  10. data/lib/sidekiq/client.rb +98 -58
  11. data/lib/sidekiq/component.rb +68 -0
  12. data/lib/sidekiq/config.rb +278 -0
  13. data/lib/sidekiq/deploy.rb +62 -0
  14. data/lib/sidekiq/embedded.rb +61 -0
  15. data/lib/sidekiq/fetch.rb +23 -24
  16. data/lib/sidekiq/job.rb +371 -10
  17. data/lib/sidekiq/job_logger.rb +16 -28
  18. data/lib/sidekiq/job_retry.rb +80 -56
  19. data/lib/sidekiq/job_util.rb +60 -20
  20. data/lib/sidekiq/launcher.rb +103 -95
  21. data/lib/sidekiq/logger.rb +9 -44
  22. data/lib/sidekiq/manager.rb +33 -32
  23. data/lib/sidekiq/metrics/query.rb +153 -0
  24. data/lib/sidekiq/metrics/shared.rb +95 -0
  25. data/lib/sidekiq/metrics/tracking.rb +136 -0
  26. data/lib/sidekiq/middleware/chain.rb +96 -51
  27. data/lib/sidekiq/middleware/current_attributes.rb +58 -20
  28. data/lib/sidekiq/middleware/i18n.rb +6 -4
  29. data/lib/sidekiq/middleware/modules.rb +21 -0
  30. data/lib/sidekiq/monitor.rb +17 -4
  31. data/lib/sidekiq/paginator.rb +17 -9
  32. data/lib/sidekiq/processor.rb +60 -60
  33. data/lib/sidekiq/rails.rb +22 -10
  34. data/lib/sidekiq/redis_client_adapter.rb +96 -0
  35. data/lib/sidekiq/redis_connection.rb +13 -82
  36. data/lib/sidekiq/ring_buffer.rb +29 -0
  37. data/lib/sidekiq/scheduled.rb +66 -38
  38. data/lib/sidekiq/testing/inline.rb +4 -4
  39. data/lib/sidekiq/testing.rb +41 -68
  40. data/lib/sidekiq/transaction_aware_client.rb +44 -0
  41. data/lib/sidekiq/version.rb +2 -1
  42. data/lib/sidekiq/web/action.rb +3 -3
  43. data/lib/sidekiq/web/application.rb +40 -9
  44. data/lib/sidekiq/web/csrf_protection.rb +3 -3
  45. data/lib/sidekiq/web/helpers.rb +35 -21
  46. data/lib/sidekiq/web.rb +10 -17
  47. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  48. data/lib/sidekiq.rb +84 -206
  49. data/sidekiq.gemspec +12 -10
  50. data/web/assets/javascripts/application.js +76 -26
  51. data/web/assets/javascripts/base-charts.js +106 -0
  52. data/web/assets/javascripts/chart.min.js +13 -0
  53. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  54. data/web/assets/javascripts/dashboard-charts.js +166 -0
  55. data/web/assets/javascripts/dashboard.js +3 -240
  56. data/web/assets/javascripts/metrics.js +264 -0
  57. data/web/assets/stylesheets/application-dark.css +4 -0
  58. data/web/assets/stylesheets/application-rtl.css +2 -91
  59. data/web/assets/stylesheets/application.css +66 -297
  60. data/web/locales/ar.yml +70 -70
  61. data/web/locales/cs.yml +62 -62
  62. data/web/locales/da.yml +60 -53
  63. data/web/locales/de.yml +65 -65
  64. data/web/locales/el.yml +43 -24
  65. data/web/locales/en.yml +82 -69
  66. data/web/locales/es.yml +68 -68
  67. data/web/locales/fa.yml +65 -65
  68. data/web/locales/fr.yml +81 -67
  69. data/web/locales/gd.yml +99 -0
  70. data/web/locales/he.yml +65 -64
  71. data/web/locales/hi.yml +59 -59
  72. data/web/locales/it.yml +53 -53
  73. data/web/locales/ja.yml +73 -68
  74. data/web/locales/ko.yml +52 -52
  75. data/web/locales/lt.yml +66 -66
  76. data/web/locales/nb.yml +61 -61
  77. data/web/locales/nl.yml +52 -52
  78. data/web/locales/pl.yml +45 -45
  79. data/web/locales/pt-br.yml +63 -55
  80. data/web/locales/pt.yml +51 -51
  81. data/web/locales/ru.yml +67 -66
  82. data/web/locales/sv.yml +53 -53
  83. data/web/locales/ta.yml +60 -60
  84. data/web/locales/uk.yml +62 -61
  85. data/web/locales/ur.yml +64 -64
  86. data/web/locales/vi.yml +67 -67
  87. data/web/locales/zh-cn.yml +43 -16
  88. data/web/locales/zh-tw.yml +42 -8
  89. data/web/views/_footer.erb +5 -2
  90. data/web/views/_job_info.erb +18 -2
  91. data/web/views/_metrics_period_select.erb +12 -0
  92. data/web/views/_nav.erb +1 -1
  93. data/web/views/_paging.erb +2 -0
  94. data/web/views/_poll_link.erb +1 -1
  95. data/web/views/_summary.erb +1 -1
  96. data/web/views/busy.erb +44 -28
  97. data/web/views/dashboard.erb +36 -4
  98. data/web/views/metrics.erb +82 -0
  99. data/web/views/metrics_for_job.erb +68 -0
  100. data/web/views/morgue.erb +5 -9
  101. data/web/views/queue.erb +15 -15
  102. data/web/views/queues.erb +3 -1
  103. data/web/views/retries.erb +5 -9
  104. data/web/views/scheduled.erb +12 -13
  105. metadata +56 -27
  106. data/lib/sidekiq/delay.rb +0 -43
  107. data/lib/sidekiq/exception_handler.rb +0 -27
  108. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  109. data/lib/sidekiq/extensions/active_record.rb +0 -43
  110. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  111. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  112. data/lib/sidekiq/util.rb +0 -108
  113. data/lib/sidekiq/worker.rb +0 -364
  114. /data/{LICENSE → LICENSE.txt} +0 -0
data/lib/sidekiq.rb CHANGED
@@ -1,14 +1,39 @@
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
- require "sidekiq/worker"
33
+ require "sidekiq/transaction_aware_client"
9
34
  require "sidekiq/job"
10
- require "sidekiq/redis_connection"
11
- require "sidekiq/delay"
35
+ require "sidekiq/worker_compatibility_alias"
36
+ require "sidekiq/redis_client_adapter"
12
37
 
13
38
  require "json"
14
39
 
@@ -16,253 +41,106 @@ module Sidekiq
16
41
  NAME = "Sidekiq"
17
42
  LICENSE = "See LICENSE and the LGPL-3.0 for licensing details."
18
43
 
19
- DEFAULTS = {
20
- queues: [],
21
- labels: [],
22
- concurrency: 10,
23
- require: ".",
24
- strict: true,
25
- environment: nil,
26
- timeout: 25,
27
- poll_interval_average: nil,
28
- average_scheduled_poll_interval: 5,
29
- on_complex_arguments: :warn,
30
- error_handlers: [],
31
- death_handlers: [],
32
- lifecycle_events: {
33
- startup: [],
34
- quiet: [],
35
- shutdown: [],
36
- heartbeat: []
37
- },
38
- dead_max_jobs: 10_000,
39
- dead_timeout_in_seconds: 180 * 24 * 60 * 60, # 6 months
40
- reloader: proc { |&block| block.call }
41
- }
42
-
43
- DEFAULT_WORKER_OPTIONS = {
44
- "retry" => true,
45
- "queue" => "default"
46
- }
47
-
48
- FAKE_INFO = {
49
- "redis_version" => "9.9.9",
50
- "uptime_in_days" => "9999",
51
- "connected_clients" => "9999",
52
- "used_memory_human" => "9P",
53
- "used_memory_peak_human" => "9P"
54
- }
55
-
56
44
  def self.❨╯°□°❩╯︵┻━┻
57
- puts "Calm down, yo."
58
- end
59
-
60
- def self.options
61
- @options ||= DEFAULTS.dup
62
- end
63
-
64
- def self.options=(opts)
65
- @options = opts
66
- end
67
-
68
- ##
69
- # Configuration for Sidekiq server, use like:
70
- #
71
- # Sidekiq.configure_server do |config|
72
- # config.redis = { :namespace => 'myapp', :size => 25, :url => 'redis://myhost:8877/0' }
73
- # config.server_middleware do |chain|
74
- # chain.add MyServerHook
75
- # end
76
- # end
77
- def self.configure_server
78
- yield self if server?
79
- end
80
-
81
- ##
82
- # Configuration for Sidekiq client, use like:
83
- #
84
- # Sidekiq.configure_client do |config|
85
- # config.redis = { :namespace => 'myapp', :size => 1, :url => 'redis://myhost:8877/0' }
86
- # end
87
- def self.configure_client
88
- yield self unless server?
45
+ puts "Take a deep breath and count to ten..."
89
46
  end
90
47
 
91
48
  def self.server?
92
49
  defined?(Sidekiq::CLI)
93
50
  end
94
51
 
95
- def self.redis
96
- raise ArgumentError, "requires a block" unless block_given?
97
- redis_pool.with do |conn|
98
- retryable = true
99
- begin
100
- yield conn
101
- rescue Redis::BaseError => ex
102
- # 2550 Failover can cause the server to become a replica, need
103
- # to disconnect and reopen the socket to get back to the primary.
104
- # 4495 Use the same logic if we have a "Not enough replicas" error from the primary
105
- # 4985 Use the same logic when a blocking command is force-unblocked
106
- if retryable && ex.message =~ /READONLY|NOREPLICAS|UNBLOCKED/
107
- conn.disconnect!
108
- retryable = false
109
- retry
110
- end
111
- raise
112
- end
113
- end
114
- end
115
-
116
- def self.redis_info
117
- redis do |conn|
118
- # admin commands can't go through redis-namespace starting
119
- # in redis-namespace 2.0
120
- if conn.respond_to?(:namespace)
121
- conn.redis.info
122
- else
123
- conn.info
124
- end
125
- rescue Redis::CommandError => ex
126
- # 2850 return fake version when INFO command has (probably) been renamed
127
- raise unless /unknown command/.match?(ex.message)
128
- FAKE_INFO
129
- end
130
- end
131
-
132
- def self.redis_pool
133
- @redis ||= Sidekiq::RedisConnection.create
134
- end
135
-
136
- def self.redis=(hash)
137
- @redis = if hash.is_a?(ConnectionPool)
138
- hash
139
- else
140
- Sidekiq::RedisConnection.create(hash)
141
- end
142
- end
143
-
144
- def self.client_middleware
145
- @client_chain ||= Middleware::Chain.new
146
- yield @client_chain if block_given?
147
- @client_chain
52
+ def self.load_json(string)
53
+ JSON.parse(string)
148
54
  end
149
55
 
150
- def self.server_middleware
151
- @server_chain ||= default_server_middleware
152
- yield @server_chain if block_given?
153
- @server_chain
56
+ def self.dump_json(object)
57
+ JSON.generate(object)
154
58
  end
155
59
 
156
- def self.default_server_middleware
157
- Middleware::Chain.new
60
+ def self.pro?
61
+ defined?(Sidekiq::Pro)
158
62
  end
159
63
 
160
- def self.default_worker_options=(hash)
161
- # stringify
162
- @default_worker_options = default_worker_options.merge(hash.transform_keys(&:to_s))
64
+ def self.ent?
65
+ defined?(Sidekiq::Enterprise)
163
66
  end
164
67
 
165
- def self.default_worker_options
166
- defined?(@default_worker_options) ? @default_worker_options : DEFAULT_WORKER_OPTIONS
68
+ def self.redis_pool
69
+ (Thread.current[:sidekiq_capsule] || default_configuration).redis_pool
167
70
  end
168
71
 
169
- ##
170
- # Death handlers are called when all retries for a job have been exhausted and
171
- # the job dies. It's the notification to your application
172
- # that this job will not succeed without manual intervention.
173
- #
174
- # Sidekiq.configure_server do |config|
175
- # config.death_handlers << ->(job, ex) do
176
- # end
177
- # end
178
- def self.death_handlers
179
- options[:death_handlers]
72
+ def self.redis(&block)
73
+ (Thread.current[:sidekiq_capsule] || default_configuration).redis(&block)
180
74
  end
181
75
 
182
- def self.load_json(string)
183
- JSON.parse(string)
76
+ def self.strict_args!(mode = :raise)
77
+ Sidekiq::Config::DEFAULTS[:on_complex_arguments] = mode
184
78
  end
185
79
 
186
- def self.dump_json(object)
187
- JSON.generate(object)
80
+ def self.default_job_options=(hash)
81
+ @default_job_options = default_job_options.merge(hash.transform_keys(&:to_s))
188
82
  end
189
83
 
190
- def self.log_formatter
191
- @log_formatter ||= if ENV["DYNO"]
192
- Sidekiq::Logger::Formatters::WithoutTimestamp.new
193
- else
194
- Sidekiq::Logger::Formatters::Pretty.new
195
- end
84
+ def self.default_job_options
85
+ @default_job_options ||= {"retry" => true, "queue" => "default"}
196
86
  end
197
87
 
198
- def self.log_formatter=(log_formatter)
199
- @log_formatter = log_formatter
200
- logger.formatter = log_formatter
88
+ def self.default_configuration
89
+ @config ||= Sidekiq::Config.new
201
90
  end
202
91
 
203
92
  def self.logger
204
- @logger ||= Sidekiq::Logger.new($stdout, level: Logger::INFO)
205
- end
206
-
207
- def self.logger=(logger)
208
- if logger.nil?
209
- self.logger.level = Logger::FATAL
210
- return self.logger
211
- end
212
-
213
- logger.extend(Sidekiq::LoggingUtils)
214
-
215
- @logger = logger
93
+ default_configuration.logger
216
94
  end
217
95
 
218
- def self.pro?
219
- defined?(Sidekiq::Pro)
96
+ def self.configure_server(&block)
97
+ (@config_blocks ||= []) << block
98
+ yield default_configuration if server?
220
99
  end
221
100
 
222
- # How frequently Redis should be checked by a random Sidekiq process for
223
- # scheduled and retriable jobs. Each individual process will take turns by
224
- # waiting some multiple of this value.
225
- #
226
- # See sidekiq/scheduled.rb for an in-depth explanation of this value
227
- def self.average_scheduled_poll_interval=(interval)
228
- options[:average_scheduled_poll_interval] = interval
101
+ def self.freeze!
102
+ @frozen = true
103
+ @config_blocks = nil
229
104
  end
230
105
 
231
- # Register a proc to handle any error which occurs within the Sidekiq process.
106
+ # Creates a Sidekiq::Config instance that is more tuned for embedding
107
+ # within an arbitrary Ruby process. Notably it reduces concurrency by
108
+ # default so there is less contention for CPU time with other threads.
232
109
  #
233
- # Sidekiq.configure_server do |config|
234
- # config.error_handlers << proc {|ex,ctx_hash| MyErrorService.notify(ex, ctx_hash) }
110
+ # inst = Sidekiq.configure_embed do |config|
111
+ # config.queues = %w[critical default low]
235
112
  # end
113
+ # inst.run
114
+ # sleep 10
115
+ # inst.terminate
236
116
  #
237
- # The default error handler logs errors to Sidekiq.logger.
238
- def self.error_handlers
239
- options[:error_handlers]
240
- end
241
-
242
- # Register a block to run at a point in the Sidekiq lifecycle.
243
- # :startup, :quiet or :shutdown are valid events.
117
+ # NB: it is really easy to overload a Ruby process with threads due to the GIL.
118
+ # I do not recommend setting concurrency higher than 2-3.
244
119
  #
245
- # Sidekiq.configure_server do |config|
246
- # config.on(:shutdown) do
247
- # puts "Goodbye cruel world!"
248
- # end
249
- # end
250
- def self.on(event, &block)
251
- raise ArgumentError, "Symbols only please: #{event}" unless event.is_a?(Symbol)
252
- raise ArgumentError, "Invalid event name: #{event}" unless options[:lifecycle_events].key?(event)
253
- options[:lifecycle_events][event] << block
120
+ # NB: Sidekiq only supports one instance in memory. You will get undefined behavior
121
+ # if you try to embed Sidekiq twice in the same process.
122
+ def self.configure_embed(&block)
123
+ raise "Sidekiq global configuration is frozen, you must create all embedded instances BEFORE calling `run`" if @frozen
124
+
125
+ require "sidekiq/embedded"
126
+ cfg = default_configuration
127
+ cfg.concurrency = 2
128
+ @config_blocks&.each { |block| block.call(cfg) }
129
+ yield cfg
130
+
131
+ Sidekiq::Embedded.new(cfg)
254
132
  end
255
133
 
256
- def self.strict_args!(mode = :raise)
257
- options[:on_complex_arguments] = mode
134
+ def self.configure_client
135
+ yield default_configuration unless server?
258
136
  end
259
137
 
260
- # We are shutting down Sidekiq but what about workers that
138
+ # We are shutting down Sidekiq but what about threads that
261
139
  # are working on some long job? This error is
262
- # raised in workers that have not finished within the hard
140
+ # raised in jobs that have not finished within the hard
263
141
  # timeout limit. This is needed to rollback db transactions,
264
142
  # otherwise Ruby's Thread#kill will commit. See #377.
265
- # DO NOT RESCUE THIS ERROR IN YOUR WORKERS
143
+ # DO NOT RESCUE THIS ERROR IN YOUR JOBS
266
144
  class Shutdown < Interrupt; end
267
145
  end
268
146
 
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.14.0"
27
+ gem.add_dependency "connection_pool", ">= 2.3.0"
28
+ gem.add_dependency "rack", ">= 2.2.4"
29
+ gem.add_dependency "concurrent-ruby", "< 2"
28
30
  end
@@ -9,7 +9,9 @@ var ready = (callback) => {
9
9
  else document.addEventListener("DOMContentLoaded", callback);
10
10
  }
11
11
 
12
- ready(() => {
12
+ ready(addListeners)
13
+
14
+ function addListeners() {
13
15
  document.querySelectorAll(".check_all").forEach(node => {
14
16
  node.addEventListener("click", event => {
15
17
  node.closest('table').querySelectorAll('input[type=checkbox]').forEach(inp => { inp.checked = !!node.checked; });
@@ -26,42 +28,66 @@ ready(() => {
26
28
  })
27
29
 
28
30
  document.querySelectorAll("[data-toggle]").forEach(node => {
29
- node.addEventListener("click", event => {
30
- var targName = node.getAttribute("data-toggle");
31
- var full = document.getElementById(targName + "_full");
32
- if (full.style.display == "block") {
33
- full.style.display = 'none';
34
- } else {
35
- full.style.display = 'block';
36
- }
37
- })
31
+ node.addEventListener("click", addDataToggleListeners)
38
32
  })
39
33
 
34
+ addShiftClickListeners()
40
35
  updateFuzzyTimes();
36
+ setLivePollFromUrl();
41
37
 
42
38
  var buttons = document.querySelectorAll(".live-poll");
43
39
  if (buttons.length > 0) {
44
40
  buttons.forEach(node => {
45
- node.addEventListener("click", event => {
46
- if (localStorage.sidekiqLivePoll == "enabled") {
47
- localStorage.sidekiqLivePoll = "disabled";
48
- clearTimeout(livePollTimer);
49
- livePollTimer = null;
50
- } else {
51
- localStorage.sidekiqLivePoll = "enabled";
52
- livePollCallback();
53
- }
54
-
55
- updateLivePollButton();
56
- })
41
+ node.addEventListener("click", addPollingListeners)
57
42
  });
58
43
 
59
44
  updateLivePollButton();
60
- if (localStorage.sidekiqLivePoll == "enabled") {
45
+ if (localStorage.sidekiqLivePoll == "enabled" && !livePollTimer) {
61
46
  scheduleLivePoll();
62
47
  }
63
48
  }
64
- })
49
+ }
50
+
51
+ function addPollingListeners(_event) {
52
+ if (localStorage.sidekiqLivePoll == "enabled") {
53
+ localStorage.sidekiqLivePoll = "disabled";
54
+ clearTimeout(livePollTimer);
55
+ livePollTimer = null;
56
+ } else {
57
+ localStorage.sidekiqLivePoll = "enabled";
58
+ livePollCallback();
59
+ }
60
+
61
+ updateLivePollButton();
62
+ }
63
+
64
+ function addDataToggleListeners(event) {
65
+ var source = event.target || event.srcElement;
66
+ var targName = source.getAttribute("data-toggle");
67
+ var full = document.getElementById(targName);
68
+ if (full.style.display == "block") {
69
+ full.style.display = 'none';
70
+ } else {
71
+ full.style.display = 'block';
72
+ }
73
+ }
74
+
75
+ function addShiftClickListeners() {
76
+ let checkboxes = Array.from(document.querySelectorAll(".shift_clickable"));
77
+ let lastChecked = null;
78
+ checkboxes.forEach(checkbox => {
79
+ checkbox.addEventListener("click", (e) => {
80
+ if (e.shiftKey && lastChecked) {
81
+ let myIndex = checkboxes.indexOf(checkbox);
82
+ let lastIndex = checkboxes.indexOf(lastChecked);
83
+ let [min, max] = [myIndex, lastIndex].sort();
84
+ let newState = checkbox.checked;
85
+ checkboxes.slice(min, max).forEach(c => c.checked = newState);
86
+ }
87
+ lastChecked = checkbox;
88
+ });
89
+ });
90
+ }
65
91
 
66
92
  function updateFuzzyTimes() {
67
93
  var locale = document.body.getAttribute("data-locale");
@@ -76,6 +102,14 @@ function updateFuzzyTimes() {
76
102
  t.cancel();
77
103
  }
78
104
 
105
+ function setLivePollFromUrl() {
106
+ var url_params = new URL(window.location.href).searchParams
107
+
108
+ if (url_params.get("poll") == "true") {
109
+ localStorage.sidekiqLivePoll = "enabled";
110
+ }
111
+ }
112
+
79
113
  function updateLivePollButton() {
80
114
  if (localStorage.sidekiqLivePoll == "enabled") {
81
115
  document.querySelectorAll('.live-poll-stop').forEach(box => { box.style.display = "inline-block" })
@@ -89,7 +123,19 @@ function updateLivePollButton() {
89
123
  function livePollCallback() {
90
124
  clearTimeout(livePollTimer);
91
125
 
92
- fetch(window.location.href).then(resp => resp.text()).then(replacePage).finally(scheduleLivePoll)
126
+ fetch(window.location.href)
127
+ .then(checkResponse)
128
+ .then(resp => resp.text())
129
+ .then(replacePage)
130
+ .catch(showError)
131
+ .finally(scheduleLivePoll)
132
+ }
133
+
134
+ function checkResponse(resp) {
135
+ if (!resp.ok) {
136
+ throw response.error();
137
+ }
138
+ return resp
93
139
  }
94
140
 
95
141
  function scheduleLivePoll() {
@@ -107,5 +153,9 @@ function replacePage(text) {
107
153
  var header_status = doc.querySelector('.status')
108
154
  document.querySelector('.status').replaceWith(header_status)
109
155
 
110
- updateFuzzyTimes();
156
+ addListeners();
157
+ }
158
+
159
+ function showError(error) {
160
+ console.error(error)
111
161
  }
@@ -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
+ }