sidekiq 6.0.0 → 6.3.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +258 -2
  3. data/LICENSE +1 -1
  4. data/README.md +6 -8
  5. data/bin/sidekiq +26 -2
  6. data/bin/sidekiqload +8 -4
  7. data/bin/sidekiqmon +4 -5
  8. data/lib/generators/sidekiq/worker_generator.rb +11 -1
  9. data/lib/sidekiq/api.rb +220 -145
  10. data/lib/sidekiq/cli.rb +64 -27
  11. data/lib/sidekiq/client.rb +31 -14
  12. data/lib/sidekiq/extensions/action_mailer.rb +3 -2
  13. data/lib/sidekiq/extensions/active_record.rb +4 -3
  14. data/lib/sidekiq/extensions/class_methods.rb +5 -4
  15. data/lib/sidekiq/extensions/generic_proxy.rb +3 -1
  16. data/lib/sidekiq/fetch.rb +36 -27
  17. data/lib/sidekiq/job.rb +13 -0
  18. data/lib/sidekiq/job_logger.rb +13 -5
  19. data/lib/sidekiq/job_retry.rb +27 -17
  20. data/lib/sidekiq/launcher.rb +110 -28
  21. data/lib/sidekiq/logger.rb +109 -12
  22. data/lib/sidekiq/manager.rb +4 -4
  23. data/lib/sidekiq/middleware/chain.rb +17 -6
  24. data/lib/sidekiq/middleware/current_attributes.rb +48 -0
  25. data/lib/sidekiq/monitor.rb +3 -18
  26. data/lib/sidekiq/paginator.rb +7 -2
  27. data/lib/sidekiq/processor.rb +22 -24
  28. data/lib/sidekiq/rails.rb +27 -18
  29. data/lib/sidekiq/redis_connection.rb +19 -13
  30. data/lib/sidekiq/scheduled.rb +37 -11
  31. data/lib/sidekiq/sd_notify.rb +149 -0
  32. data/lib/sidekiq/systemd.rb +24 -0
  33. data/lib/sidekiq/testing.rb +14 -4
  34. data/lib/sidekiq/util.rb +28 -2
  35. data/lib/sidekiq/version.rb +1 -1
  36. data/lib/sidekiq/web/action.rb +2 -2
  37. data/lib/sidekiq/web/application.rb +37 -30
  38. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  39. data/lib/sidekiq/web/helpers.rb +51 -33
  40. data/lib/sidekiq/web/router.rb +6 -5
  41. data/lib/sidekiq/web.rb +37 -73
  42. data/lib/sidekiq/worker.rb +78 -14
  43. data/lib/sidekiq.rb +24 -8
  44. data/sidekiq.gemspec +13 -6
  45. data/web/assets/images/apple-touch-icon.png +0 -0
  46. data/web/assets/javascripts/application.js +83 -64
  47. data/web/assets/javascripts/dashboard.js +53 -53
  48. data/web/assets/stylesheets/application-dark.css +147 -0
  49. data/web/assets/stylesheets/application-rtl.css +0 -4
  50. data/web/assets/stylesheets/application.css +43 -230
  51. data/web/locales/ar.yml +8 -2
  52. data/web/locales/de.yml +14 -2
  53. data/web/locales/en.yml +6 -1
  54. data/web/locales/es.yml +18 -2
  55. data/web/locales/fr.yml +10 -3
  56. data/web/locales/ja.yml +5 -0
  57. data/web/locales/lt.yml +83 -0
  58. data/web/locales/pl.yml +4 -4
  59. data/web/locales/ru.yml +4 -0
  60. data/web/locales/vi.yml +83 -0
  61. data/web/views/_footer.erb +1 -1
  62. data/web/views/_job_info.erb +3 -2
  63. data/web/views/_poll_link.erb +2 -5
  64. data/web/views/_summary.erb +7 -7
  65. data/web/views/busy.erb +54 -20
  66. data/web/views/dashboard.erb +22 -14
  67. data/web/views/dead.erb +3 -3
  68. data/web/views/layout.erb +3 -1
  69. data/web/views/morgue.erb +9 -6
  70. data/web/views/queue.erb +19 -10
  71. data/web/views/queues.erb +10 -2
  72. data/web/views/retries.erb +11 -8
  73. data/web/views/retry.erb +3 -3
  74. data/web/views/scheduled.erb +5 -2
  75. metadata +29 -50
  76. data/.circleci/config.yml +0 -61
  77. data/.github/contributing.md +0 -32
  78. data/.github/issue_template.md +0 -11
  79. data/.gitignore +0 -13
  80. data/.standard.yml +0 -20
  81. data/3.0-Upgrade.md +0 -70
  82. data/4.0-Upgrade.md +0 -53
  83. data/5.0-Upgrade.md +0 -56
  84. data/6.0-Upgrade.md +0 -70
  85. data/COMM-LICENSE +0 -97
  86. data/Ent-2.0-Upgrade.md +0 -37
  87. data/Ent-Changes.md +0 -250
  88. data/Gemfile +0 -24
  89. data/Gemfile.lock +0 -196
  90. data/Pro-2.0-Upgrade.md +0 -138
  91. data/Pro-3.0-Upgrade.md +0 -44
  92. data/Pro-4.0-Upgrade.md +0 -35
  93. data/Pro-5.0-Upgrade.md +0 -25
  94. data/Pro-Changes.md +0 -768
  95. data/Rakefile +0 -10
  96. data/code_of_conduct.md +0 -50
@@ -28,15 +28,15 @@ module Sidekiq
28
28
  attr_reader :thread
29
29
  attr_reader :job
30
30
 
31
- def initialize(mgr)
31
+ def initialize(mgr, options)
32
32
  @mgr = mgr
33
33
  @down = false
34
34
  @done = false
35
35
  @job = nil
36
36
  @thread = nil
37
- @strategy = (mgr.options[:fetch] || Sidekiq::BasicFetch).new(mgr.options)
38
- @reloader = Sidekiq.options[:reloader]
39
- @job_logger = (mgr.options[:job_logger] || Sidekiq::JobLogger).new
37
+ @strategy = options[:fetch]
38
+ @reloader = options[:reloader] || proc { |&block| block.call }
39
+ @job_logger = (options[:job_logger] || Sidekiq::JobLogger).new
40
40
  @retrier = Sidekiq::JobRetry.new
41
41
  end
42
42
 
@@ -111,16 +111,19 @@ module Sidekiq
111
111
  nil
112
112
  end
113
113
 
114
- def dispatch(job_hash, queue)
114
+ def dispatch(job_hash, queue, jobstr)
115
115
  # since middleware can mutate the job hash
116
- # we clone here so we report the original
116
+ # we need to clone it to report the original
117
117
  # job structure to the Web UI
118
- pristine = cloned(job_hash)
118
+ # or to push back to redis when retrying.
119
+ # To avoid costly and, most of the time, useless cloning here,
120
+ # we pass original String of JSON to respected methods
121
+ # to re-parse it there if we need access to the original, untouched job
119
122
 
120
- @job_logger.with_job_hash_context(job_hash) do
121
- @retrier.global(pristine, queue) do
123
+ @job_logger.prepare(job_hash) do
124
+ @retrier.global(jobstr, queue) do
122
125
  @job_logger.call(job_hash, queue) do
123
- stats(pristine, queue) do
126
+ stats(jobstr, queue) do
124
127
  # Rails 5 requires a Reloader to wrap code execution. In order to
125
128
  # constantize the worker and instantiate an instance, we have to call
126
129
  # the Reloader. It handles code loading, db connection management, etc.
@@ -129,7 +132,7 @@ module Sidekiq
129
132
  klass = constantize(job_hash["class"])
130
133
  worker = klass.new
131
134
  worker.jid = job_hash["jid"]
132
- @retrier.local(worker, pristine, queue) do
135
+ @retrier.local(worker, jobstr, queue) do
133
136
  yield worker
134
137
  end
135
138
  end
@@ -156,9 +159,9 @@ module Sidekiq
156
159
 
157
160
  ack = false
158
161
  begin
159
- dispatch(job_hash, queue) do |worker|
162
+ dispatch(job_hash, queue, jobstr) do |worker|
160
163
  Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
161
- execute_job(worker, cloned(job_hash["args"]))
164
+ execute_job(worker, job_hash["args"])
162
165
  end
163
166
  end
164
167
  ack = true
@@ -178,7 +181,7 @@ module Sidekiq
178
181
  # the retry subsystem (e.g. network partition). We won't acknowledge the job
179
182
  # so it can be rescued when using Sidekiq Pro.
180
183
  handle_exception(ex, {context: "Internal exception!", job: job_hash, jobstr: jobstr})
181
- raise e
184
+ raise ex
182
185
  ensure
183
186
  if ack
184
187
  # We don't want a shutdown signal to interrupt job acknowledgment.
@@ -247,8 +250,8 @@ module Sidekiq
247
250
  FAILURE = Counter.new
248
251
  WORKER_STATE = SharedWorkerState.new
249
252
 
250
- def stats(job_hash, queue)
251
- WORKER_STATE.set(tid, {queue: queue, payload: job_hash, run_at: Time.now.to_i})
253
+ def stats(jobstr, queue)
254
+ WORKER_STATE.set(tid, {queue: queue, payload: jobstr, run_at: Time.now.to_i})
252
255
 
253
256
  begin
254
257
  yield
@@ -261,21 +264,16 @@ module Sidekiq
261
264
  end
262
265
  end
263
266
 
264
- # Deep clone the arguments passed to the worker so that if
265
- # the job fails, what is pushed back onto Redis hasn't
266
- # been mutated by the worker.
267
- def cloned(thing)
268
- Marshal.load(Marshal.dump(thing))
269
- end
270
-
271
267
  def constantize(str)
268
+ return Object.const_get(str) unless str.include?("::")
269
+
272
270
  names = str.split("::")
273
271
  names.shift if names.empty? || names.first.empty?
274
272
 
275
273
  names.inject(Object) do |constant, name|
276
274
  # the false flag limits search for name to under the constant namespace
277
275
  # which mimics Rails' behaviour
278
- constant.const_defined?(name, false) ? constant.const_get(name, false) : constant.const_missing(name)
276
+ constant.const_get(name, false)
279
277
  end
280
278
  end
281
279
  end
data/lib/sidekiq/rails.rb CHANGED
@@ -4,6 +4,22 @@ require "sidekiq/worker"
4
4
 
5
5
  module Sidekiq
6
6
  class Rails < ::Rails::Engine
7
+ class Reloader
8
+ def initialize(app = ::Rails.application)
9
+ @app = app
10
+ end
11
+
12
+ def call
13
+ @app.reloader.wrap do
14
+ yield
15
+ end
16
+ end
17
+
18
+ def inspect
19
+ "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
20
+ end
21
+ end
22
+
7
23
  # By including the Options module, we allow AJs to directly control sidekiq features
8
24
  # via the *sidekiq_options* class method and, for instance, not use AJ's retry system.
9
25
  # AJ retries don't show up in the Sidekiq UI Retries tab, save any error data, can't be
@@ -21,10 +37,19 @@ module Sidekiq
21
37
  end
22
38
  end
23
39
 
40
+ initializer "sidekiq.rails_logger" do
41
+ Sidekiq.configure_server do |_|
42
+ # This is the integration code necessary so that if code uses `Rails.logger.info "Hello"`,
43
+ # it will appear in the Sidekiq console with all of the job context. See #5021 and
44
+ # https://github.com/rails/rails/blob/b5f2b550f69a99336482739000c58e4e04e033aa/railties/lib/rails/commands/server/server_command.rb#L82-L84
45
+ unless ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
46
+ ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(::Sidekiq.logger))
47
+ end
48
+ end
49
+ end
50
+
24
51
  # This hook happens after all initializers are run, just before returning
25
52
  # from config/environment.rb back to sidekiq/cli.rb.
26
- # We have to add the reloader after initialize to see if cache_classes has
27
- # been turned on.
28
53
  #
29
54
  # None of this matters on the client-side, only within the Sidekiq process itself.
30
55
  config.after_initialize do
@@ -32,21 +57,5 @@ module Sidekiq
32
57
  Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
33
58
  end
34
59
  end
35
-
36
- class Reloader
37
- def initialize(app = ::Rails.application)
38
- @app = app
39
- end
40
-
41
- def call
42
- @app.reloader.wrap do
43
- yield
44
- end
45
- end
46
-
47
- def inspect
48
- "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
49
- end
50
- end
51
60
  end
52
61
  end
@@ -8,15 +8,14 @@ module Sidekiq
8
8
  class RedisConnection
9
9
  class << self
10
10
  def create(options = {})
11
- options.keys.each do |key|
12
- options[key.to_sym] = options.delete(key)
13
- end
11
+ symbolized_options = options.transform_keys(&:to_sym)
14
12
 
15
- options[:id] = "Sidekiq-#{Sidekiq.server? ? "server" : "client"}-PID-#{::Process.pid}" unless options.key?(:id)
16
- options[:url] ||= determine_redis_provider
13
+ if !symbolized_options[:url] && (u = determine_redis_provider)
14
+ symbolized_options[:url] = u
15
+ end
17
16
 
18
- size = if options[:size]
19
- options[:size]
17
+ size = if symbolized_options[:size]
18
+ symbolized_options[:size]
20
19
  elsif Sidekiq.server?
21
20
  # Give ourselves plenty of connections. pool is lazy
22
21
  # so we won't create them until we need them.
@@ -29,11 +28,11 @@ module Sidekiq
29
28
 
30
29
  verify_sizing(size, Sidekiq.options[:concurrency]) if Sidekiq.server?
31
30
 
32
- pool_timeout = options[:pool_timeout] || 1
33
- log_info(options)
31
+ pool_timeout = symbolized_options[:pool_timeout] || 1
32
+ log_info(symbolized_options)
34
33
 
35
34
  ConnectionPool.new(timeout: pool_timeout, size: size) do
36
- build_client(options)
35
+ build_client(symbolized_options)
37
36
  end
38
37
  end
39
38
 
@@ -93,9 +92,13 @@ module Sidekiq
93
92
  end
94
93
 
95
94
  def log_info(options)
96
- # Don't log Redis AUTH password
97
95
  redacted = "REDACTED"
98
- scrubbed_options = options.dup
96
+
97
+ # Deep clone so we can muck with these options all we want and exclude
98
+ # params from dump-and-load that may contain objects that Marshal is
99
+ # unable to safely dump.
100
+ keys = options.keys - [:logger, :ssl_params]
101
+ scrubbed_options = Marshal.load(Marshal.dump(options.slice(*keys)))
99
102
  if scrubbed_options[:url] && (uri = URI.parse(scrubbed_options[:url])) && uri.password
100
103
  uri.password = redacted
101
104
  scrubbed_options[:url] = uri.to_s
@@ -103,6 +106,9 @@ module Sidekiq
103
106
  if scrubbed_options[:password]
104
107
  scrubbed_options[:password] = redacted
105
108
  end
109
+ scrubbed_options[:sentinels]&.each do |sentinel|
110
+ sentinel[:password] = redacted if sentinel[:password]
111
+ end
106
112
  if Sidekiq.server?
107
113
  Sidekiq.logger.info("Booting Sidekiq #{Sidekiq::VERSION} with redis options #{scrubbed_options}")
108
114
  else
@@ -119,7 +125,7 @@ module Sidekiq
119
125
  # initialization code at all.
120
126
  #
121
127
  p = ENV["REDIS_PROVIDER"]
122
- if p && p =~ /\:/
128
+ if p && p =~ /:/
123
129
  raise <<~EOM
124
130
  REDIS_PROVIDER should be set to the name of the variable which contains the Redis URL, not a URL itself.
125
131
  Platforms like Heroku will sell addons that publish a *_URL variable. You need to tell Sidekiq with REDIS_PROVIDER, e.g.:
@@ -9,28 +9,48 @@ module Sidekiq
9
9
  SETS = %w[retry schedule]
10
10
 
11
11
  class Enq
12
+ LUA_ZPOPBYSCORE = <<~LUA
13
+ local key, now = KEYS[1], ARGV[1]
14
+ local jobs = redis.call("zrangebyscore", key, "-inf", now, "limit", 0, 1)
15
+ if jobs[1] then
16
+ redis.call("zrem", key, jobs[1])
17
+ return jobs[1]
18
+ end
19
+ LUA
20
+
21
+ def initialize
22
+ @lua_zpopbyscore_sha = nil
23
+ end
24
+
12
25
  def enqueue_jobs(now = Time.now.to_f.to_s, sorted_sets = SETS)
13
26
  # A job's "score" in Redis is the time at which it should be processed.
14
27
  # Just check Redis for the set of jobs with a timestamp before now.
15
28
  Sidekiq.redis do |conn|
16
29
  sorted_sets.each do |sorted_set|
17
- # Get the next item in the queue if it's score (time to execute) is <= now.
30
+ # Get next item in the queue with score (time to execute) <= now.
18
31
  # We need to go through the list one at a time to reduce the risk of something
19
32
  # going wrong between the time jobs are popped from the scheduled queue and when
20
33
  # they are pushed onto a work queue and losing the jobs.
21
- while (job = conn.zrangebyscore(sorted_set, "-inf", now, limit: [0, 1]).first)
22
-
23
- # Pop item off the queue and add it to the work queue. If the job can't be popped from
24
- # the queue, it's because another process already popped it so we can move on to the
25
- # next one.
26
- if conn.zrem(sorted_set, job)
27
- Sidekiq::Client.push(Sidekiq.load_json(job))
28
- Sidekiq.logger.debug { "enqueued #{sorted_set}: #{job}" }
29
- end
34
+ while (job = zpopbyscore(conn, keys: [sorted_set], argv: [now]))
35
+ Sidekiq::Client.push(Sidekiq.load_json(job))
36
+ Sidekiq.logger.debug { "enqueued #{sorted_set}: #{job}" }
30
37
  end
31
38
  end
32
39
  end
33
40
  end
41
+
42
+ private
43
+
44
+ def zpopbyscore(conn, keys: nil, argv: nil)
45
+ @lua_zpopbyscore_sha = conn.script(:load, LUA_ZPOPBYSCORE) if @lua_zpopbyscore_sha.nil?
46
+
47
+ conn.evalsha(@lua_zpopbyscore_sha, keys: keys, argv: argv)
48
+ rescue Redis::CommandError => e
49
+ raise unless e.message.start_with?("NOSCRIPT")
50
+
51
+ @lua_zpopbyscore_sha = nil
52
+ retry
53
+ end
34
54
  end
35
55
 
36
56
  ##
@@ -48,6 +68,7 @@ module Sidekiq
48
68
  @sleeper = ConnectionPool::TimedStack.new
49
69
  @done = false
50
70
  @thread = nil
71
+ @count_calls = 0
51
72
  end
52
73
 
53
74
  # Shut down this instance, will pause until the thread is dead.
@@ -151,8 +172,13 @@ module Sidekiq
151
172
  end
152
173
 
153
174
  def process_count
154
- pcount = Sidekiq::ProcessSet.new.size
175
+ # The work buried within Sidekiq::ProcessSet#cleanup can be
176
+ # expensive at scale. Cut it down by 90% with this counter.
177
+ # NB: This method is only called by the scheduler thread so we
178
+ # don't need to worry about the thread safety of +=.
179
+ pcount = Sidekiq::ProcessSet.new(@count_calls % 10 == 0).size
155
180
  pcount = 1 if pcount == 0
181
+ @count_calls += 1
156
182
  pcount
157
183
  end
158
184
 
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The MIT License
4
+ #
5
+ # Copyright (c) 2017, 2018, 2019, 2020 Agis Anastasopoulos
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of
8
+ # this software and associated documentation files (the "Software"), to deal in
9
+ # the Software without restriction, including without limitation the rights to
10
+ # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
11
+ # the Software, and to permit persons to whom the Software is furnished to do so,
12
+ # subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included in all
15
+ # copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
19
+ # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
20
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
21
+ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
22
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ # This is a copy of https://github.com/agis/ruby-sdnotify as of commit a7d52ee
25
+ # The only changes made was "rehoming" it within the Sidekiq module to avoid
26
+ # namespace collisions and applying standard's code formatting style.
27
+
28
+ require "socket"
29
+
30
+ # SdNotify is a pure-Ruby implementation of sd_notify(3). It can be used to
31
+ # notify systemd about state changes. Methods of this package are no-op on
32
+ # non-systemd systems (eg. Darwin).
33
+ #
34
+ # The API maps closely to the original implementation of sd_notify(3),
35
+ # therefore be sure to check the official man pages prior to using SdNotify.
36
+ #
37
+ # @see https://www.freedesktop.org/software/systemd/man/sd_notify.html
38
+ module Sidekiq
39
+ module SdNotify
40
+ # Exception raised when there's an error writing to the notification socket
41
+ class NotifyError < RuntimeError; end
42
+
43
+ READY = "READY=1"
44
+ RELOADING = "RELOADING=1"
45
+ STOPPING = "STOPPING=1"
46
+ STATUS = "STATUS="
47
+ ERRNO = "ERRNO="
48
+ MAINPID = "MAINPID="
49
+ WATCHDOG = "WATCHDOG=1"
50
+ FDSTORE = "FDSTORE=1"
51
+
52
+ def self.ready(unset_env = false)
53
+ notify(READY, unset_env)
54
+ end
55
+
56
+ def self.reloading(unset_env = false)
57
+ notify(RELOADING, unset_env)
58
+ end
59
+
60
+ def self.stopping(unset_env = false)
61
+ notify(STOPPING, unset_env)
62
+ end
63
+
64
+ # @param status [String] a custom status string that describes the current
65
+ # state of the service
66
+ def self.status(status, unset_env = false)
67
+ notify("#{STATUS}#{status}", unset_env)
68
+ end
69
+
70
+ # @param errno [Integer]
71
+ def self.errno(errno, unset_env = false)
72
+ notify("#{ERRNO}#{errno}", unset_env)
73
+ end
74
+
75
+ # @param pid [Integer]
76
+ def self.mainpid(pid, unset_env = false)
77
+ notify("#{MAINPID}#{pid}", unset_env)
78
+ end
79
+
80
+ def self.watchdog(unset_env = false)
81
+ notify(WATCHDOG, unset_env)
82
+ end
83
+
84
+ def self.fdstore(unset_env = false)
85
+ notify(FDSTORE, unset_env)
86
+ end
87
+
88
+ # @return [Boolean] true if the service manager expects watchdog keep-alive
89
+ # notification messages to be sent from this process.
90
+ #
91
+ # If the $WATCHDOG_USEC environment variable is set,
92
+ # and the $WATCHDOG_PID variable is unset or set to the PID of the current
93
+ # process
94
+ #
95
+ # @note Unlike sd_watchdog_enabled(3), this method does not mutate the
96
+ # environment.
97
+ def self.watchdog?
98
+ wd_usec = ENV["WATCHDOG_USEC"]
99
+ wd_pid = ENV["WATCHDOG_PID"]
100
+
101
+ return false unless wd_usec
102
+
103
+ begin
104
+ wd_usec = Integer(wd_usec)
105
+ rescue
106
+ return false
107
+ end
108
+
109
+ return false if wd_usec <= 0
110
+ return true if !wd_pid || wd_pid == $$.to_s
111
+
112
+ false
113
+ end
114
+
115
+ # Notify systemd with the provided state, via the notification socket, if
116
+ # any.
117
+ #
118
+ # Generally this method will be used indirectly through the other methods
119
+ # of the library.
120
+ #
121
+ # @param state [String]
122
+ # @param unset_env [Boolean]
123
+ #
124
+ # @return [Fixnum, nil] the number of bytes written to the notification
125
+ # socket or nil if there was no socket to report to (eg. the program wasn't
126
+ # started by systemd)
127
+ #
128
+ # @raise [NotifyError] if there was an error communicating with the systemd
129
+ # socket
130
+ #
131
+ # @see https://www.freedesktop.org/software/systemd/man/sd_notify.html
132
+ def self.notify(state, unset_env = false)
133
+ sock = ENV["NOTIFY_SOCKET"]
134
+
135
+ return nil unless sock
136
+
137
+ ENV.delete("NOTIFY_SOCKET") if unset_env
138
+
139
+ begin
140
+ Addrinfo.unix(sock, :DGRAM).connect do |s|
141
+ s.close_on_exec = true
142
+ s.write(state)
143
+ end
144
+ rescue => e
145
+ raise NotifyError, "#{e.class}: #{e.message}", e.backtrace
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,24 @@
1
+ #
2
+ # Sidekiq's systemd integration allows Sidekiq to inform systemd:
3
+ # 1. when it has successfully started
4
+ # 2. when it is starting shutdown
5
+ # 3. periodically for a liveness check with a watchdog thread
6
+ #
7
+ module Sidekiq
8
+ def self.start_watchdog
9
+ usec = Integer(ENV["WATCHDOG_USEC"])
10
+ return Sidekiq.logger.error("systemd Watchdog too fast: " + usec) if usec < 1_000_000
11
+
12
+ sec_f = usec / 1_000_000.0
13
+ # "It is recommended that a daemon sends a keep-alive notification message
14
+ # to the service manager every half of the time returned here."
15
+ ping_f = sec_f / 2
16
+ Sidekiq.logger.info "Pinging systemd watchdog every #{ping_f.round(1)} sec"
17
+ Thread.new do
18
+ loop do
19
+ sleep ping_f
20
+ Sidekiq::SdNotify.watchdog
21
+ end
22
+ end
23
+ end
24
+ end
@@ -323,10 +323,20 @@ module Sidekiq
323
323
  end
324
324
  end
325
325
  end
326
+
327
+ module TestingExtensions
328
+ def jobs_for(klass)
329
+ jobs.select do |job|
330
+ marshalled = job["args"][0]
331
+ marshalled.index(klass.to_s) && YAML.load(marshalled)[0] == klass
332
+ end
333
+ end
334
+ end
335
+
336
+ Sidekiq::Extensions::DelayedMailer.extend(TestingExtensions) if defined?(Sidekiq::Extensions::DelayedMailer)
337
+ Sidekiq::Extensions::DelayedModel.extend(TestingExtensions) if defined?(Sidekiq::Extensions::DelayedModel)
326
338
  end
327
339
 
328
- if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test?
329
- puts("**************************************************")
330
- puts("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.")
331
- puts("**************************************************")
340
+ if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test? && !$TESTING
341
+ warn("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.", uplevel: 1)
332
342
  end
data/lib/sidekiq/util.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
3
4
  require "socket"
4
5
  require "securerandom"
5
6
  require "sidekiq/exception_handler"
@@ -8,11 +9,36 @@ module Sidekiq
8
9
  ##
9
10
  # This module is part of Sidekiq core and not intended for extensions.
10
11
  #
12
+
13
+ class RingBuffer
14
+ include Enumerable
15
+ extend Forwardable
16
+ def_delegators :@buf, :[], :each, :size
17
+
18
+ def initialize(size, default = 0)
19
+ @size = size
20
+ @buf = Array.new(size, default)
21
+ @index = 0
22
+ end
23
+
24
+ def <<(element)
25
+ @buf[@index % @size] = element
26
+ @index += 1
27
+ element
28
+ end
29
+
30
+ def buffer
31
+ @buf
32
+ end
33
+
34
+ def reset(default = 0)
35
+ @buf.fill(default)
36
+ end
37
+ end
38
+
11
39
  module Util
12
40
  include ExceptionHandler
13
41
 
14
- EXPIRY = 60 * 60 * 24
15
-
16
42
  def watchdog(last_words)
17
43
  yield
18
44
  rescue Exception => ex
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "6.0.0"
4
+ VERSION = "6.3.0"
5
5
  end
@@ -15,7 +15,7 @@ module Sidekiq
15
15
  end
16
16
 
17
17
  def halt(res)
18
- throw :halt, res
18
+ throw :halt, [res, {"Content-Type" => "text/plain"}, [res.to_s]]
19
19
  end
20
20
 
21
21
  def redirect(location)
@@ -68,7 +68,7 @@ module Sidekiq
68
68
  end
69
69
 
70
70
  def json(payload)
71
- [200, {"Content-Type" => "application/json", "Cache-Control" => "no-cache"}, [Sidekiq.dump_json(payload)]]
71
+ [200, {"Content-Type" => "application/json", "Cache-Control" => "private, no-store"}, [Sidekiq.dump_json(payload)]]
72
72
  end
73
73
 
74
74
  def initialize(env, block)