sidekiq 6.5.12 → 7.0.0.beta1

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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +5 -24
  3. data/README.md +13 -12
  4. data/bin/sidekiq +3 -8
  5. data/bin/sidekiqload +14 -23
  6. data/lib/sidekiq/api.rb +51 -127
  7. data/lib/sidekiq/capsule.rb +110 -0
  8. data/lib/sidekiq/cli.rb +44 -59
  9. data/lib/sidekiq/client.rb +30 -17
  10. data/lib/sidekiq/component.rb +1 -0
  11. data/lib/sidekiq/config.rb +270 -0
  12. data/lib/sidekiq/deploy.rb +62 -0
  13. data/lib/sidekiq/embedded.rb +61 -0
  14. data/lib/sidekiq/fetch.rb +10 -11
  15. data/lib/sidekiq/job.rb +375 -10
  16. data/lib/sidekiq/job_logger.rb +1 -1
  17. data/lib/sidekiq/job_retry.rb +8 -8
  18. data/lib/sidekiq/job_util.rb +4 -4
  19. data/lib/sidekiq/launcher.rb +36 -46
  20. data/lib/sidekiq/logger.rb +1 -26
  21. data/lib/sidekiq/manager.rb +9 -11
  22. data/lib/sidekiq/metrics/query.rb +3 -3
  23. data/lib/sidekiq/metrics/shared.rb +4 -3
  24. data/lib/sidekiq/metrics/tracking.rb +18 -18
  25. data/lib/sidekiq/middleware/chain.rb +7 -9
  26. data/lib/sidekiq/middleware/current_attributes.rb +3 -8
  27. data/lib/sidekiq/monitor.rb +1 -1
  28. data/lib/sidekiq/paginator.rb +1 -9
  29. data/lib/sidekiq/pool.rb +7 -0
  30. data/lib/sidekiq/processor.rb +17 -26
  31. data/lib/sidekiq/rails.rb +11 -10
  32. data/lib/sidekiq/redis_client_adapter.rb +9 -45
  33. data/lib/sidekiq/redis_connection.rb +11 -111
  34. data/lib/sidekiq/scheduled.rb +19 -20
  35. data/lib/sidekiq/testing.rb +4 -32
  36. data/lib/sidekiq/transaction_aware_client.rb +4 -5
  37. data/lib/sidekiq/version.rb +1 -1
  38. data/lib/sidekiq/web/application.rb +1 -4
  39. data/lib/sidekiq/web/csrf_protection.rb +1 -1
  40. data/lib/sidekiq/web/helpers.rb +21 -22
  41. data/lib/sidekiq/web.rb +2 -17
  42. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  43. data/lib/sidekiq.rb +52 -274
  44. data/sidekiq.gemspec +29 -5
  45. data/web/assets/javascripts/application.js +0 -1
  46. data/web/assets/javascripts/base-charts.js +106 -0
  47. data/web/assets/javascripts/dashboard-charts.js +166 -0
  48. data/web/assets/javascripts/dashboard.js +3 -223
  49. data/web/assets/javascripts/metrics.js +90 -116
  50. data/web/assets/stylesheets/application-rtl.css +2 -91
  51. data/web/assets/stylesheets/application.css +21 -296
  52. data/web/locales/ar.yml +70 -70
  53. data/web/locales/cs.yml +62 -62
  54. data/web/locales/da.yml +52 -52
  55. data/web/locales/de.yml +65 -65
  56. data/web/locales/el.yml +2 -7
  57. data/web/locales/en.yml +76 -70
  58. data/web/locales/es.yml +68 -68
  59. data/web/locales/fa.yml +65 -65
  60. data/web/locales/fr.yml +67 -67
  61. data/web/locales/he.yml +65 -64
  62. data/web/locales/hi.yml +59 -59
  63. data/web/locales/it.yml +53 -53
  64. data/web/locales/ja.yml +64 -68
  65. data/web/locales/ko.yml +52 -52
  66. data/web/locales/lt.yml +66 -66
  67. data/web/locales/nb.yml +61 -61
  68. data/web/locales/nl.yml +52 -52
  69. data/web/locales/pl.yml +45 -45
  70. data/web/locales/pt-br.yml +59 -69
  71. data/web/locales/pt.yml +51 -51
  72. data/web/locales/ru.yml +67 -66
  73. data/web/locales/sv.yml +53 -53
  74. data/web/locales/ta.yml +60 -60
  75. data/web/locales/uk.yml +62 -61
  76. data/web/locales/ur.yml +64 -64
  77. data/web/locales/vi.yml +67 -67
  78. data/web/locales/zh-cn.yml +1 -0
  79. data/web/locales/zh-tw.yml +10 -1
  80. data/web/views/_footer.erb +5 -2
  81. data/web/views/busy.erb +1 -6
  82. data/web/views/dashboard.erb +36 -5
  83. data/web/views/metrics.erb +30 -19
  84. data/web/views/metrics_for_job.erb +16 -34
  85. metadata +60 -37
  86. data/lib/sidekiq/delay.rb +0 -43
  87. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  88. data/lib/sidekiq/extensions/active_record.rb +0 -43
  89. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  90. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  91. data/lib/sidekiq/metrics/deploy.rb +0 -47
  92. data/lib/sidekiq/worker.rb +0 -370
  93. data/web/assets/javascripts/graph.js +0 -16
  94. /data/{LICENSE → LICENSE.txt} +0 -0
data/lib/sidekiq.rb CHANGED
@@ -1,15 +1,15 @@
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
+ require "sidekiq/config"
6
7
  require "sidekiq/logger"
7
8
  require "sidekiq/client"
8
9
  require "sidekiq/transaction_aware_client"
9
- require "sidekiq/worker"
10
10
  require "sidekiq/job"
11
- require "sidekiq/redis_connection"
12
- require "sidekiq/delay"
11
+ require "sidekiq/worker_compatibility_alias"
12
+ require "sidekiq/redis_client_adapter"
13
13
 
14
14
  require "json"
15
15
 
@@ -17,320 +17,98 @@ module Sidekiq
17
17
  NAME = "Sidekiq"
18
18
  LICENSE = "See LICENSE and the LGPL-3.0 for licensing details."
19
19
 
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
- # triggers when we fire the first heartbeat on startup OR repairing a network partition
38
- heartbeat: [],
39
- # triggers on EVERY heartbeat call, every 10 seconds
40
- beat: []
41
- },
42
- dead_max_jobs: 10_000,
43
- dead_timeout_in_seconds: 180 * 24 * 60 * 60, # 6 months
44
- reloader: proc { |&block| block.call }
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
20
  def self.❨╯°□°❩╯︵┻━┻
56
- puts "Calm down, yo."
57
- end
58
-
59
- # config.concurrency = 5
60
- def self.concurrency=(val)
61
- self[:concurrency] = Integer(val)
62
- end
63
-
64
- # config.queues = %w( high default low ) # strict
65
- # config.queues = %w( high,3 default,2 low,1 ) # weighted
66
- # config.queues = %w( feature1,1 feature2,1 feature3,1 ) # random
67
- #
68
- # With weighted priority, queue will be checked first (weight / total) of the time.
69
- # high will be checked first (3/6) or 50% of the time.
70
- # I'd recommend setting weights between 1-10. Weights in the hundreds or thousands
71
- # are ridiculous and unnecessarily expensive. You can get random queue ordering
72
- # by explicitly setting all weights to 1.
73
- def self.queues=(val)
74
- self[:queues] = Array(val).each_with_object([]) do |qstr, memo|
75
- name, weight = qstr.split(",")
76
- self[:strict] = false if weight.to_i > 0
77
- [weight.to_i, 1].max.times do
78
- memo << name
79
- end
80
- end
81
- end
82
-
83
- ### Private APIs
84
- def self.default_error_handler(ex, ctx)
85
- logger.warn(dump_json(ctx)) unless ctx.empty?
86
- logger.warn("#{ex.class.name}: #{ex.message}")
87
- logger.warn(ex.backtrace.join("\n")) unless ex.backtrace.nil?
88
- end
89
-
90
- # DEFAULT_ERROR_HANDLER is a constant that allows the default error handler to
91
- # be referenced. It must be defined here, after the default_error_handler
92
- # method is defined.
93
- DEFAULT_ERROR_HANDLER = method(:default_error_handler)
94
-
95
- @config = DEFAULTS.dup
96
- def self.options
97
- logger.warn "`config.options[:key] = value` is deprecated, use `config[:key] = value`: #{caller(1..2)}"
98
- @config
99
- end
100
-
101
- def self.options=(opts)
102
- logger.warn "config.options = hash` is deprecated, use `config.merge!(hash)`: #{caller(1..2)}"
103
- @config = opts
104
- end
105
-
106
- def self.[](key)
107
- @config[key]
108
- end
109
-
110
- def self.[]=(key, val)
111
- @config[key] = val
112
- end
113
-
114
- def self.merge!(hash)
115
- @config.merge!(hash)
116
- end
117
-
118
- def self.fetch(*args, &block)
119
- @config.fetch(*args, &block)
120
- end
121
-
122
- def self.handle_exception(ex, ctx = {})
123
- self[:error_handlers].each do |handler|
124
- handler.call(ex, ctx)
125
- rescue => ex
126
- logger.error "!!! ERROR HANDLER THREW AN ERROR !!!"
127
- logger.error ex
128
- logger.error ex.backtrace.join("\n") unless ex.backtrace.nil?
129
- end
130
- end
131
- ###
132
-
133
- ##
134
- # Configuration for Sidekiq server, use like:
135
- #
136
- # Sidekiq.configure_server do |config|
137
- # config.server_middleware do |chain|
138
- # chain.add MyServerHook
139
- # end
140
- # end
141
- def self.configure_server
142
- yield self if server?
143
- end
144
-
145
- ##
146
- # Configuration for Sidekiq client, use like:
147
- #
148
- # Sidekiq.configure_client do |config|
149
- # config.redis = { size: 1, url: 'redis://myhost:8877/0' }
150
- # end
151
- def self.configure_client
152
- yield self unless server?
21
+ puts "Take a deep breath and count to ten..."
153
22
  end
154
23
 
155
24
  def self.server?
156
25
  defined?(Sidekiq::CLI)
157
26
  end
158
27
 
159
- def self.redis
160
- raise ArgumentError, "requires a block" unless block_given?
161
- redis_pool.with do |conn|
162
- retryable = true
163
- begin
164
- yield conn
165
- rescue RedisConnection.adapter::BaseError => ex
166
- # 2550 Failover can cause the server to become a replica, need
167
- # to disconnect and reopen the socket to get back to the primary.
168
- # 4495 Use the same logic if we have a "Not enough replicas" error from the primary
169
- # 4985 Use the same logic when a blocking command is force-unblocked
170
- # The same retry logic is also used in client.rb
171
- if retryable && ex.message =~ /READONLY|NOREPLICAS|UNBLOCKED/
172
- conn.disconnect!
173
- retryable = false
174
- retry
175
- end
176
- raise
177
- end
178
- end
179
- end
180
-
181
- def self.redis_info
182
- redis do |conn|
183
- # admin commands can't go through redis-namespace starting
184
- # in redis-namespace 2.0
185
- if conn.respond_to?(:namespace)
186
- conn.redis.info
187
- else
188
- conn.info
189
- end
190
- rescue RedisConnection.adapter::CommandError => ex
191
- # 2850 return fake version when INFO command has (probably) been renamed
192
- raise unless /unknown command/.match?(ex.message)
193
- FAKE_INFO
194
- end
28
+ def self.load_json(string)
29
+ JSON.parse(string)
195
30
  end
196
31
 
197
- def self.redis_pool
198
- @redis ||= RedisConnection.create
32
+ def self.dump_json(object)
33
+ JSON.generate(object)
199
34
  end
200
35
 
201
- def self.redis=(hash)
202
- @redis = if hash.is_a?(ConnectionPool)
203
- hash
204
- else
205
- RedisConnection.create(hash)
206
- end
36
+ def self.pro?
37
+ defined?(Sidekiq::Pro)
207
38
  end
208
39
 
209
- def self.client_middleware
210
- @client_chain ||= Middleware::Chain.new(self)
211
- yield @client_chain if block_given?
212
- @client_chain
40
+ def self.ent?
41
+ defined?(Sidekiq::Enterprise)
213
42
  end
214
43
 
215
- def self.server_middleware
216
- @server_chain ||= default_server_middleware
217
- yield @server_chain if block_given?
218
- @server_chain
44
+ def self.redis_pool
45
+ (Thread.current[:sidekiq_capsule] || default_configuration).redis_pool
219
46
  end
220
47
 
221
- def self.default_server_middleware
222
- Middleware::Chain.new(self)
48
+ def self.redis(&block)
49
+ (Thread.current[:sidekiq_capsule] || default_configuration).redis(&block)
223
50
  end
224
51
 
225
- def self.default_worker_options=(hash) # deprecated
226
- @default_job_options = default_job_options.merge(hash.transform_keys(&:to_s))
52
+ def self.strict_args!(mode = :raise)
53
+ Sidekiq::Config::DEFAULTS[:on_complex_arguments] = mode
227
54
  end
228
55
 
229
56
  def self.default_job_options=(hash)
230
57
  @default_job_options = default_job_options.merge(hash.transform_keys(&:to_s))
231
58
  end
232
59
 
233
- def self.default_worker_options # deprecated
234
- @default_job_options ||= {"retry" => true, "queue" => "default"}
235
- end
236
-
237
60
  def self.default_job_options
238
61
  @default_job_options ||= {"retry" => true, "queue" => "default"}
239
62
  end
240
63
 
241
- ##
242
- # Death handlers are called when all retries for a job have been exhausted and
243
- # the job dies. It's the notification to your application
244
- # that this job will not succeed without manual intervention.
245
- #
246
- # Sidekiq.configure_server do |config|
247
- # config.death_handlers << ->(job, ex) do
248
- # end
249
- # end
250
- def self.death_handlers
251
- self[:death_handlers]
252
- end
253
-
254
- def self.load_json(string)
255
- JSON.parse(string)
256
- end
257
-
258
- def self.dump_json(object)
259
- JSON.generate(object)
260
- end
261
-
262
- def self.log_formatter
263
- @log_formatter ||= if ENV["DYNO"]
264
- Sidekiq::Logger::Formatters::WithoutTimestamp.new
265
- else
266
- Sidekiq::Logger::Formatters::Pretty.new
267
- end
268
- end
269
-
270
- def self.log_formatter=(log_formatter)
271
- @log_formatter = log_formatter
272
- logger.formatter = log_formatter
64
+ def self.default_configuration
65
+ @config ||= Sidekiq::Config.new
273
66
  end
274
67
 
275
68
  def self.logger
276
- @logger ||= Sidekiq::Logger.new($stdout, level: :info)
277
- end
278
-
279
- def self.logger=(logger)
280
- if logger.nil?
281
- self.logger.level = Logger::FATAL
282
- return self.logger
283
- end
284
-
285
- logger.extend(Sidekiq::LoggingUtils)
286
-
287
- @logger = logger
288
- end
289
-
290
- def self.pro?
291
- defined?(Sidekiq::Pro)
69
+ default_configuration.logger
292
70
  end
293
71
 
294
- def self.ent?
295
- defined?(Sidekiq::Enterprise)
72
+ def self.configure_server(&block)
73
+ (@config_blocks ||= []) << block
74
+ yield default_configuration if server?
296
75
  end
297
76
 
298
- # How frequently Redis should be checked by a random Sidekiq process for
299
- # scheduled and retriable jobs. Each individual process will take turns by
300
- # waiting some multiple of this value.
301
- #
302
- # See sidekiq/scheduled.rb for an in-depth explanation of this value
303
- def self.average_scheduled_poll_interval=(interval)
304
- self[:average_scheduled_poll_interval] = interval
77
+ def self.freeze!
78
+ @frozen = true
79
+ @config_blocks = nil
305
80
  end
306
81
 
307
- # Register a proc to handle any error which occurs within the Sidekiq process.
82
+ # Creates a Sidekiq::Config instance that is more tuned for embedding
83
+ # within an arbitrary Ruby process. Noteably it reduces concurrency by
84
+ # default so there is less contention for CPU time with other threads.
308
85
  #
309
- # Sidekiq.configure_server do |config|
310
- # config.error_handlers << proc {|ex,ctx_hash| MyErrorService.notify(ex, ctx_hash) }
86
+ # inst = Sidekiq.configure_embed do |config|
87
+ # config.queues = %w[critical default low]
311
88
  # end
89
+ # inst.run
90
+ # sleep 10
91
+ # inst.terminate
312
92
  #
313
- # The default error handler logs errors to Sidekiq.logger.
314
- def self.error_handlers
315
- self[:error_handlers]
316
- end
317
-
318
- # Register a block to run at a point in the Sidekiq lifecycle.
319
- # :startup, :quiet or :shutdown are valid events.
93
+ # NB: it is really easy to overload a Ruby process with threads due to the GIL.
94
+ # I do not recommend setting concurrency higher than 2-3.
320
95
  #
321
- # Sidekiq.configure_server do |config|
322
- # config.on(:shutdown) do
323
- # puts "Goodbye cruel world!"
324
- # end
325
- # end
326
- def self.on(event, &block)
327
- raise ArgumentError, "Symbols only please: #{event}" unless event.is_a?(Symbol)
328
- raise ArgumentError, "Invalid event name: #{event}" unless self[:lifecycle_events].key?(event)
329
- self[:lifecycle_events][event] << block
96
+ # NB: Sidekiq only supports one instance in memory. You will get undefined behavior
97
+ # if you try to embed Sidekiq twice in the same process.
98
+ def self.configure_embed(&block)
99
+ raise "Sidekiq global configuration is frozen, you must create all embedded instances BEFORE calling `run`" if @frozen
100
+
101
+ require "sidekiq/embedded"
102
+ cfg = default_configuration
103
+ cfg.concurrency = 2
104
+ @config_blocks&.each { |block| block.call(cfg) }
105
+ yield cfg
106
+
107
+ Sidekiq::Embedded.new(cfg)
330
108
  end
331
109
 
332
- def self.strict_args!(mode = :raise)
333
- self[:on_complex_arguments] = mode
110
+ def self.configure_client
111
+ yield default_configuration unless server?
334
112
  end
335
113
 
336
114
  # We are shutting down Sidekiq but what about threads that
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", ["<5", ">= 4.5.0"]
26
- gem.add_dependency "connection_pool", ["<3", ">= 2.2.5"]
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/7-0/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
@@ -122,7 +122,6 @@ function checkResponse(resp) {
122
122
 
123
123
  function scheduleLivePoll() {
124
124
  let ti = parseInt(localStorage.sidekiqTimeInterval) || 5000;
125
- if (ti < 2000) { ti = 2000 }
126
125
  livePollTimer = setTimeout(livePollCallback, ti);
127
126
  }
128
127
 
@@ -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
+ }