sidekiq 6.3.1 → 7.0.0

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