sidekiq 5.2.7 → 8.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +845 -8
  3. data/LICENSE.txt +9 -0
  4. data/README.md +54 -54
  5. data/bin/multi_queue_bench +271 -0
  6. data/bin/sidekiq +22 -3
  7. data/bin/sidekiqload +219 -112
  8. data/bin/sidekiqmon +11 -0
  9. data/bin/webload +69 -0
  10. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +120 -0
  11. data/lib/generators/sidekiq/job_generator.rb +59 -0
  12. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  13. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  14. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  15. data/lib/sidekiq/api.rb +757 -373
  16. data/lib/sidekiq/capsule.rb +132 -0
  17. data/lib/sidekiq/cli.rb +210 -233
  18. data/lib/sidekiq/client.rb +145 -103
  19. data/lib/sidekiq/component.rb +128 -0
  20. data/lib/sidekiq/config.rb +315 -0
  21. data/lib/sidekiq/deploy.rb +64 -0
  22. data/lib/sidekiq/embedded.rb +64 -0
  23. data/lib/sidekiq/fetch.rb +49 -42
  24. data/lib/sidekiq/iterable_job.rb +56 -0
  25. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  26. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  27. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  28. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  29. data/lib/sidekiq/job/iterable.rb +306 -0
  30. data/lib/sidekiq/job.rb +385 -0
  31. data/lib/sidekiq/job_logger.rb +34 -7
  32. data/lib/sidekiq/job_retry.rb +164 -109
  33. data/lib/sidekiq/job_util.rb +113 -0
  34. data/lib/sidekiq/launcher.rb +208 -107
  35. data/lib/sidekiq/logger.rb +80 -0
  36. data/lib/sidekiq/manager.rb +42 -46
  37. data/lib/sidekiq/metrics/query.rb +184 -0
  38. data/lib/sidekiq/metrics/shared.rb +109 -0
  39. data/lib/sidekiq/metrics/tracking.rb +150 -0
  40. data/lib/sidekiq/middleware/chain.rb +113 -56
  41. data/lib/sidekiq/middleware/current_attributes.rb +119 -0
  42. data/lib/sidekiq/middleware/i18n.rb +7 -7
  43. data/lib/sidekiq/middleware/modules.rb +23 -0
  44. data/lib/sidekiq/monitor.rb +147 -0
  45. data/lib/sidekiq/paginator.rb +41 -16
  46. data/lib/sidekiq/processor.rb +146 -127
  47. data/lib/sidekiq/profiler.rb +72 -0
  48. data/lib/sidekiq/rails.rb +46 -43
  49. data/lib/sidekiq/redis_client_adapter.rb +113 -0
  50. data/lib/sidekiq/redis_connection.rb +79 -108
  51. data/lib/sidekiq/ring_buffer.rb +31 -0
  52. data/lib/sidekiq/scheduled.rb +112 -50
  53. data/lib/sidekiq/sd_notify.rb +149 -0
  54. data/lib/sidekiq/systemd.rb +26 -0
  55. data/lib/sidekiq/testing/inline.rb +6 -5
  56. data/lib/sidekiq/testing.rb +91 -90
  57. data/lib/sidekiq/transaction_aware_client.rb +51 -0
  58. data/lib/sidekiq/version.rb +7 -1
  59. data/lib/sidekiq/web/action.rb +125 -60
  60. data/lib/sidekiq/web/application.rb +363 -259
  61. data/lib/sidekiq/web/config.rb +120 -0
  62. data/lib/sidekiq/web/csrf_protection.rb +183 -0
  63. data/lib/sidekiq/web/helpers.rb +241 -120
  64. data/lib/sidekiq/web/router.rb +62 -71
  65. data/lib/sidekiq/web.rb +69 -161
  66. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  67. data/lib/sidekiq.rb +94 -182
  68. data/sidekiq.gemspec +26 -16
  69. data/web/assets/images/apple-touch-icon.png +0 -0
  70. data/web/assets/javascripts/application.js +150 -61
  71. data/web/assets/javascripts/base-charts.js +120 -0
  72. data/web/assets/javascripts/chart.min.js +13 -0
  73. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  74. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  75. data/web/assets/javascripts/dashboard-charts.js +194 -0
  76. data/web/assets/javascripts/dashboard.js +41 -293
  77. data/web/assets/javascripts/metrics.js +280 -0
  78. data/web/assets/stylesheets/style.css +766 -0
  79. data/web/locales/ar.yml +72 -65
  80. data/web/locales/cs.yml +63 -62
  81. data/web/locales/da.yml +61 -53
  82. data/web/locales/de.yml +66 -53
  83. data/web/locales/el.yml +44 -24
  84. data/web/locales/en.yml +94 -66
  85. data/web/locales/es.yml +92 -54
  86. data/web/locales/fa.yml +66 -65
  87. data/web/locales/fr.yml +83 -62
  88. data/web/locales/gd.yml +99 -0
  89. data/web/locales/he.yml +66 -64
  90. data/web/locales/hi.yml +60 -59
  91. data/web/locales/it.yml +93 -54
  92. data/web/locales/ja.yml +75 -64
  93. data/web/locales/ko.yml +53 -52
  94. data/web/locales/lt.yml +84 -0
  95. data/web/locales/nb.yml +62 -61
  96. data/web/locales/nl.yml +53 -52
  97. data/web/locales/pl.yml +46 -45
  98. data/web/locales/{pt-br.yml → pt-BR.yml} +84 -56
  99. data/web/locales/pt.yml +52 -51
  100. data/web/locales/ru.yml +69 -63
  101. data/web/locales/sv.yml +54 -53
  102. data/web/locales/ta.yml +61 -60
  103. data/web/locales/tr.yml +101 -0
  104. data/web/locales/uk.yml +86 -61
  105. data/web/locales/ur.yml +65 -64
  106. data/web/locales/vi.yml +84 -0
  107. data/web/locales/zh-CN.yml +106 -0
  108. data/web/locales/{zh-tw.yml → zh-TW.yml} +43 -9
  109. data/web/views/_footer.erb +31 -19
  110. data/web/views/_job_info.erb +94 -75
  111. data/web/views/_metrics_period_select.erb +15 -0
  112. data/web/views/_nav.erb +14 -21
  113. data/web/views/_paging.erb +23 -19
  114. data/web/views/_poll_link.erb +3 -6
  115. data/web/views/_summary.erb +23 -23
  116. data/web/views/busy.erb +139 -87
  117. data/web/views/dashboard.erb +82 -53
  118. data/web/views/dead.erb +31 -27
  119. data/web/views/filtering.erb +6 -0
  120. data/web/views/layout.erb +15 -29
  121. data/web/views/metrics.erb +84 -0
  122. data/web/views/metrics_for_job.erb +58 -0
  123. data/web/views/morgue.erb +60 -70
  124. data/web/views/profiles.erb +43 -0
  125. data/web/views/queue.erb +50 -39
  126. data/web/views/queues.erb +45 -29
  127. data/web/views/retries.erb +65 -75
  128. data/web/views/retry.erb +32 -27
  129. data/web/views/scheduled.erb +58 -52
  130. data/web/views/scheduled_job_info.erb +1 -1
  131. metadata +96 -76
  132. data/.circleci/config.yml +0 -61
  133. data/.github/contributing.md +0 -32
  134. data/.github/issue_template.md +0 -11
  135. data/.gitignore +0 -15
  136. data/.travis.yml +0 -11
  137. data/3.0-Upgrade.md +0 -70
  138. data/4.0-Upgrade.md +0 -53
  139. data/5.0-Upgrade.md +0 -56
  140. data/COMM-LICENSE +0 -97
  141. data/Ent-Changes.md +0 -238
  142. data/Gemfile +0 -23
  143. data/LICENSE +0 -9
  144. data/Pro-2.0-Upgrade.md +0 -138
  145. data/Pro-3.0-Upgrade.md +0 -44
  146. data/Pro-4.0-Upgrade.md +0 -35
  147. data/Pro-Changes.md +0 -759
  148. data/Rakefile +0 -9
  149. data/bin/sidekiqctl +0 -20
  150. data/code_of_conduct.md +0 -50
  151. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  152. data/lib/sidekiq/core_ext.rb +0 -1
  153. data/lib/sidekiq/ctl.rb +0 -221
  154. data/lib/sidekiq/delay.rb +0 -42
  155. data/lib/sidekiq/exception_handler.rb +0 -29
  156. data/lib/sidekiq/extensions/action_mailer.rb +0 -57
  157. data/lib/sidekiq/extensions/active_record.rb +0 -40
  158. data/lib/sidekiq/extensions/class_methods.rb +0 -40
  159. data/lib/sidekiq/extensions/generic_proxy.rb +0 -31
  160. data/lib/sidekiq/logging.rb +0 -122
  161. data/lib/sidekiq/middleware/server/active_record.rb +0 -23
  162. data/lib/sidekiq/util.rb +0 -66
  163. data/lib/sidekiq/worker.rb +0 -220
  164. data/web/assets/stylesheets/application-rtl.css +0 -246
  165. data/web/assets/stylesheets/application.css +0 -1144
  166. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  167. data/web/assets/stylesheets/bootstrap.css +0 -5
  168. data/web/locales/zh-cn.yml +0 -68
  169. data/web/views/_status.erb +0 -4
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redis_client"
4
+ require "redis_client/decorator"
5
+
6
+ module Sidekiq
7
+ class RedisClientAdapter
8
+ BaseError = RedisClient::Error
9
+ CommandError = RedisClient::CommandError
10
+
11
+ # You can add/remove items or clear the whole thing if you don't want deprecation warnings.
12
+ DEPRECATED_COMMANDS = %i[rpoplpush zrangebyscore zrevrange zrevrangebyscore getset hmset setex setnx].to_set
13
+
14
+ module CompatMethods
15
+ def info
16
+ @client.call("INFO") { |i| i.lines(chomp: true).map { |l| l.split(":", 2) }.select { |l| l.size == 2 }.to_h }
17
+ end
18
+
19
+ def evalsha(sha, keys, argv)
20
+ @client.call("EVALSHA", sha, keys.size, *keys, *argv)
21
+ end
22
+
23
+ # this is the set of Redis commands used by Sidekiq. Not guaranteed
24
+ # to be comprehensive, we use this as a performance enhancement to
25
+ # avoid calling method_missing on most commands
26
+ USED_COMMANDS = %w[bitfield bitfield_ro del exists expire flushdb
27
+ get hdel hget hgetall hincrby hlen hmget hset hsetnx incr incrby
28
+ lindex llen lmove lpop lpush lrange lrem mget mset ping pttl
29
+ publish rpop rpush sadd scard script set sismember smembers
30
+ srem ttl type unlink zadd zcard zincrby zrange zrem
31
+ zremrangebyrank zremrangebyscore]
32
+
33
+ USED_COMMANDS.each do |name|
34
+ define_method(name) do |*args, **kwargs|
35
+ @client.call(name, *args, **kwargs)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # this allows us to use methods like `conn.hmset(...)` instead of having to use
42
+ # redis-client's native `conn.call("hmset", ...)`
43
+ def method_missing(*args, &block)
44
+ warn("[sidekiq#5788] Redis has deprecated the `#{args.first}`command, called at #{caller(1..1)}") if DEPRECATED_COMMANDS.include?(args.first)
45
+ @client.call(*args, *block)
46
+ end
47
+ ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
48
+
49
+ def respond_to_missing?(name, include_private = false)
50
+ super # Appease the linter. We can't tell what is a valid command.
51
+ end
52
+ end
53
+
54
+ CompatClient = RedisClient::Decorator.create(CompatMethods)
55
+
56
+ class CompatClient
57
+ def config
58
+ @client.config
59
+ end
60
+ end
61
+
62
+ def initialize(options)
63
+ opts = client_opts(options)
64
+ @config = if opts.key?(:sentinels)
65
+ RedisClient.sentinel(**opts)
66
+ elsif opts.key?(:nodes)
67
+ # Sidekiq does not support Redis clustering but Sidekiq Enterprise's
68
+ # rate limiters are cluster-safe so we can scale to millions
69
+ # of rate limiters using a Redis cluster. This requires the
70
+ # `redis-cluster-client` gem.
71
+ # Sidekiq::Limiter.redis = { nodes: [...] }
72
+ RedisClient.cluster(**opts)
73
+ else
74
+ RedisClient.config(**opts)
75
+ end
76
+ end
77
+
78
+ def new_client
79
+ CompatClient.new(@config.new_client)
80
+ end
81
+
82
+ private
83
+
84
+ def client_opts(options)
85
+ opts = options.dup
86
+
87
+ if opts[:namespace]
88
+ raise ArgumentError, "Your Redis configuration uses the namespace '#{opts[:namespace]}' but this feature is no longer supported in Sidekiq 7+. See https://github.com/sidekiq/sidekiq/blob/main/docs/7.0-Upgrade.md#redis-namespace."
89
+ end
90
+
91
+ opts.delete(:size)
92
+ opts.delete(:pool_timeout)
93
+
94
+ if opts[:network_timeout]
95
+ opts[:timeout] = opts[:network_timeout]
96
+ opts.delete(:network_timeout)
97
+ end
98
+
99
+ opts[:name] = opts.delete(:master_name) if opts.key?(:master_name)
100
+ opts[:role] = opts[:role].to_sym if opts.key?(:role)
101
+ opts[:driver] = opts[:driver].to_sym if opts.key?(:driver)
102
+
103
+ # Issue #3303, redis-rb will silently retry an operation.
104
+ # This can lead to duplicate jobs if Sidekiq::Client's LPUSH
105
+ # is performed twice but I believe this is much, much rarer
106
+ # than the reconnect silently fixing a problem; we keep it
107
+ # on by default.
108
+ opts[:reconnect_attempts] ||= 1
109
+
110
+ opts
111
+ end
112
+ end
113
+ end
@@ -1,111 +1,92 @@
1
1
  # frozen_string_literal: true
2
- require 'connection_pool'
3
- require 'redis'
4
- require 'uri'
2
+
3
+ require "connection_pool"
4
+ require "uri"
5
+ require "sidekiq/redis_client_adapter"
5
6
 
6
7
  module Sidekiq
7
- class RedisConnection
8
+ module RedisConnection
8
9
  class << self
9
-
10
- def create(options={})
11
- options.keys.each do |key|
12
- options[key.to_sym] = options.delete(key)
13
- end
14
-
15
- options[:id] = "Sidekiq-#{Sidekiq.server? ? "server" : "client"}-PID-#{$$}" if !options.has_key?(:id)
16
- options[:url] ||= determine_redis_provider
17
-
18
- size = if options[:size]
19
- options[:size]
20
- elsif Sidekiq.server?
21
- Sidekiq.options[:concurrency] + 5
22
- elsif ENV['RAILS_MAX_THREADS']
23
- Integer(ENV['RAILS_MAX_THREADS'])
24
- else
25
- 5
26
- end
27
-
28
- verify_sizing(size, Sidekiq.options[:concurrency]) if Sidekiq.server?
29
-
30
- pool_timeout = options[:pool_timeout] || 1
31
- log_info(options)
32
-
33
- ConnectionPool.new(:timeout => pool_timeout, :size => size) do
34
- build_client(options)
10
+ def create(options = {})
11
+ symbolized_options = deep_symbolize_keys(options)
12
+ symbolized_options[:url] ||= determine_redis_provider
13
+ symbolized_options[:password] = wrap(symbolized_options[:password]) if symbolized_options.key?(:password)
14
+ symbolized_options[:sentinel_password] = wrap(symbolized_options[:sentinel_password]) if symbolized_options.key?(:sentinel_password)
15
+
16
+ logger = symbolized_options.delete(:logger)
17
+ logger&.info { "Sidekiq #{Sidekiq::VERSION} connecting to Redis with options #{scrub(symbolized_options)}" }
18
+
19
+ raise "Sidekiq 7+ does not support Redis protocol 2" if symbolized_options[:protocol] == 2
20
+
21
+ safe = !!symbolized_options.delete(:cluster_safe)
22
+ raise ":nodes not allowed, Sidekiq is not safe to run on Redis Cluster" if !safe && symbolized_options.key?(:nodes)
23
+
24
+ size = symbolized_options.delete(:size) || 5
25
+ pool_timeout = symbolized_options.delete(:pool_timeout) || 1
26
+ pool_name = symbolized_options.delete(:pool_name)
27
+
28
+ # Default timeout in redis-client is 1 second, which can be too aggressive
29
+ # if the Sidekiq process is CPU-bound. With 10-15 threads and a thread quantum of 100ms,
30
+ # it can be easy to get the occasional ReadTimeoutError. You can still provide
31
+ # a smaller timeout explicitly:
32
+ # config.redis = { url: "...", timeout: 1 }
33
+ symbolized_options[:timeout] ||= 3
34
+
35
+ redis_config = Sidekiq::RedisClientAdapter.new(symbolized_options)
36
+ ConnectionPool.new(timeout: pool_timeout, size: size, name: pool_name) do
37
+ redis_config.new_client
35
38
  end
36
39
  end
37
40
 
38
41
  private
39
42
 
40
- # Sidekiq needs a lot of concurrent Redis connections.
41
- #
42
- # We need a connection for each Processor.
43
- # We need a connection for Pro's real-time change listener
44
- # We need a connection to various features to call Redis every few seconds:
45
- # - the process heartbeat.
46
- # - enterprise's leader election
47
- # - enterprise's cron support
48
- def verify_sizing(size, concurrency)
49
- raise ArgumentError, "Your Redis connection pool is too small for Sidekiq to work. Your pool has #{size} connections but must have at least #{concurrency + 2}" if size <= concurrency
50
- end
51
-
52
- def build_client(options)
53
- namespace = options[:namespace]
54
-
55
- client = Redis.new client_opts(options)
56
- if namespace
57
- begin
58
- require 'redis/namespace'
59
- Redis::Namespace.new(namespace, :redis => client)
60
- rescue LoadError
61
- Sidekiq.logger.error("Your Redis configuration uses the namespace '#{namespace}' but the redis-namespace gem is not included in the Gemfile." \
62
- "Add the gem to your Gemfile to continue using a namespace. Otherwise, remove the namespace parameter.")
63
- exit(-127)
64
- end
43
+ # Wrap hard-coded passwords in a Proc to avoid logging the value
44
+ def wrap(pwd)
45
+ if pwd.is_a?(String)
46
+ ->(username) { pwd }
65
47
  else
66
- client
48
+ pwd
67
49
  end
68
50
  end
69
51
 
70
- def client_opts(options)
71
- opts = options.dup
72
- if opts[:namespace]
73
- opts.delete(:namespace)
74
- end
75
-
76
- if opts[:network_timeout]
77
- opts[:timeout] = opts[:network_timeout]
78
- opts.delete(:network_timeout)
52
+ def deep_symbolize_keys(object)
53
+ case object
54
+ when Hash
55
+ object.each_with_object({}) do |(key, value), result|
56
+ result[key.to_sym] = deep_symbolize_keys(value)
57
+ end
58
+ when Array
59
+ object.map { |e| deep_symbolize_keys(e) }
60
+ else
61
+ object
79
62
  end
80
-
81
- opts[:driver] ||= Redis::Connection.drivers.last || 'ruby'
82
-
83
- # Issue #3303, redis-rb will silently retry an operation.
84
- # This can lead to duplicate jobs if Sidekiq::Client's LPUSH
85
- # is performed twice but I believe this is much, much rarer
86
- # than the reconnect silently fixing a problem; we keep it
87
- # on by default.
88
- opts[:reconnect_attempts] ||= 1
89
-
90
- opts
91
63
  end
92
64
 
93
- def log_info(options)
94
- # Don't log Redis AUTH password
65
+ def scrub(options)
95
66
  redacted = "REDACTED"
96
- scrubbed_options = options.dup
67
+
68
+ # Deep clone so we can muck with these options all we want and exclude
69
+ # params from dump-and-load that may contain objects that Marshal is
70
+ # unable to safely dump.
71
+ keys = options.keys - [:logger, :ssl_params, :password, :sentinel_password]
72
+ scrubbed_options = Marshal.load(Marshal.dump(options.slice(*keys)))
97
73
  if scrubbed_options[:url] && (uri = URI.parse(scrubbed_options[:url])) && uri.password
98
74
  uri.password = redacted
99
75
  scrubbed_options[:url] = uri.to_s
100
76
  end
101
- if scrubbed_options[:password]
102
- scrubbed_options[:password] = redacted
103
- end
104
- if Sidekiq.server?
105
- Sidekiq.logger.info("Booting Sidekiq #{Sidekiq::VERSION} with redis options #{scrubbed_options}")
106
- else
107
- Sidekiq.logger.debug("#{Sidekiq::NAME} client with redis options #{scrubbed_options}")
77
+ scrubbed_options[:password] = redacted if options.key?(:password)
78
+ scrubbed_options[:sentinel_password] = redacted if options.key?(:sentinel_password)
79
+ scrubbed_options[:sentinels]&.each do |sentinel|
80
+ if sentinel.is_a?(String)
81
+ if (uri = URI(sentinel)) && uri.password
82
+ uri.password = redacted
83
+ sentinel.replace(uri.to_s)
84
+ end
85
+ elsif sentinel[:password]
86
+ sentinel[:password] = redacted
87
+ end
108
88
  end
89
+ scrubbed_options
109
90
  end
110
91
 
111
92
  def determine_redis_provider
@@ -115,30 +96,20 @@ module Sidekiq
115
96
  # REDIS_PROVIDER=MY_REDIS_URL
116
97
  # and Sidekiq will find your custom URL variable with no custom
117
98
  # initialization code at all.
118
- p = ENV['REDIS_PROVIDER']
119
- if p && p =~ /\:/
120
- Sidekiq.logger.error <<-EOM
121
-
122
- #################################################################################
123
-
124
- REDIS_PROVIDER should be set to the **name** of the variable which contains the Redis URL, not a URL itself.
125
- Platforms like Heroku sell addons that publish a *_URL variable. You tell Sidekiq with REDIS_PROVIDER, e.g.:
126
-
127
- REDIS_PROVIDER=REDISTOGO_URL
128
- REDISTOGO_URL=redis://somehost.example.com:6379/4
129
-
130
- Use REDIS_URL if you wish to point Sidekiq to a URL directly.
131
-
132
- This configuration error will crash starting in Sidekiq 5.3.
133
-
134
- #################################################################################
135
- EOM
99
+ #
100
+ p = ENV["REDIS_PROVIDER"]
101
+ if p && p =~ /:/
102
+ raise <<~EOM
103
+ REDIS_PROVIDER should be set to the name of the variable which contains the Redis URL, not a URL itself.
104
+ Platforms like Heroku will sell addons that publish a *_URL variable. You need to tell Sidekiq with REDIS_PROVIDER, e.g.:
105
+
106
+ REDISTOGO_URL=redis://somehost.example.com:6379/4
107
+ REDIS_PROVIDER=REDISTOGO_URL
108
+ EOM
136
109
  end
137
- ENV[
138
- ENV['REDIS_PROVIDER'] || 'REDIS_URL'
139
- ]
140
- end
141
110
 
111
+ ENV[p.to_s] || ENV["REDIS_URL"]
112
+ end
142
113
  end
143
114
  end
144
115
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Sidekiq
6
+ class RingBuffer
7
+ include Enumerable
8
+ extend Forwardable
9
+ def_delegators :@buf, :[], :each, :size
10
+
11
+ def initialize(size, default = 0)
12
+ @size = size
13
+ @buf = Array.new(size, default)
14
+ @index = 0
15
+ end
16
+
17
+ def <<(element)
18
+ @buf[@index % @size] = element
19
+ @index += 1
20
+ element
21
+ end
22
+
23
+ def buffer
24
+ @buf
25
+ end
26
+
27
+ def reset(default = 0)
28
+ @buf.fill(default)
29
+ end
30
+ end
31
+ end
@@ -1,35 +1,66 @@
1
1
  # frozen_string_literal: true
2
- require 'sidekiq'
3
- require 'sidekiq/util'
4
- require 'sidekiq/api'
2
+
3
+ require "sidekiq"
4
+ require "sidekiq/component"
5
5
 
6
6
  module Sidekiq
7
7
  module Scheduled
8
- SETS = %w(retry schedule)
8
+ SETS = %w[retry schedule]
9
9
 
10
10
  class Enq
11
- def enqueue_jobs(now=Time.now.to_f.to_s, sorted_sets=SETS)
11
+ include Sidekiq::Component
12
+
13
+ LUA_ZPOPBYSCORE = <<~LUA
14
+ local key, now = KEYS[1], ARGV[1]
15
+ local jobs = redis.call("zrange", key, "-inf", now, "byscore", "limit", 0, 1)
16
+ if jobs[1] then
17
+ redis.call("zrem", key, jobs[1])
18
+ return jobs[1]
19
+ end
20
+ LUA
21
+
22
+ def initialize(container)
23
+ @config = container
24
+ @client = Sidekiq::Client.new(config: container)
25
+ @done = false
26
+ @lua_zpopbyscore_sha = nil
27
+ end
28
+
29
+ def enqueue_jobs(sorted_sets = SETS)
12
30
  # A job's "score" in Redis is the time at which it should be processed.
13
31
  # Just check Redis for the set of jobs with a timestamp before now.
14
- Sidekiq.redis do |conn|
32
+ redis do |conn|
15
33
  sorted_sets.each do |sorted_set|
16
- # Get the next item in the queue if it's score (time to execute) is <= now.
34
+ # Get next item in the queue with score (time to execute) <= now.
17
35
  # We need to go through the list one at a time to reduce the risk of something
18
36
  # going wrong between the time jobs are popped from the scheduled queue and when
19
37
  # they are pushed onto a work queue and losing the jobs.
20
- while job = conn.zrangebyscore(sorted_set, '-inf', now, :limit => [0, 1]).first do
21
-
22
- # Pop item off the queue and add it to the work queue. If the job can't be popped from
23
- # the queue, it's because another process already popped it so we can move on to the
24
- # next one.
25
- if conn.zrem(sorted_set, job)
26
- Sidekiq::Client.push(Sidekiq.load_json(job))
27
- Sidekiq::Logging.logger.debug { "enqueued #{sorted_set}: #{job}" }
28
- end
38
+ while !@done && (job = zpopbyscore(conn, keys: [sorted_set], argv: [Time.now.to_f.to_s]))
39
+ @client.push(Sidekiq.load_json(job))
40
+ logger.debug { "enqueued #{sorted_set}: #{job}" }
29
41
  end
30
42
  end
31
43
  end
32
44
  end
45
+
46
+ def terminate
47
+ @done = true
48
+ end
49
+
50
+ private
51
+
52
+ def zpopbyscore(conn, keys: nil, argv: nil)
53
+ if @lua_zpopbyscore_sha.nil?
54
+ @lua_zpopbyscore_sha = conn.script(:load, LUA_ZPOPBYSCORE)
55
+ end
56
+
57
+ conn.call("EVALSHA", @lua_zpopbyscore_sha, keys.size, *keys, *argv)
58
+ rescue RedisClient::CommandError => e
59
+ raise unless e.message.start_with?("NOSCRIPT")
60
+
61
+ @lua_zpopbyscore_sha = nil
62
+ retry
63
+ end
33
64
  end
34
65
 
35
66
  ##
@@ -38,49 +69,47 @@ module Sidekiq
38
69
  # just pops the job back onto its original queue so the
39
70
  # workers can pick it up like any other job.
40
71
  class Poller
41
- include Util
72
+ include Sidekiq::Component
42
73
 
43
74
  INITIAL_WAIT = 10
44
75
 
45
- def initialize
46
- @enq = (Sidekiq.options[:scheduled_enq] || Sidekiq::Scheduled::Enq).new
76
+ def initialize(config)
77
+ @config = config
78
+ @enq = (config[:scheduled_enq] || Sidekiq::Scheduled::Enq).new(config)
47
79
  @sleeper = ConnectionPool::TimedStack.new
48
80
  @done = false
49
81
  @thread = nil
82
+ @count_calls = 0
50
83
  end
51
84
 
52
85
  # Shut down this instance, will pause until the thread is dead.
53
86
  def terminate
54
87
  @done = true
55
- if @thread
56
- t = @thread
57
- @thread = nil
58
- @sleeper << 0
59
- t.value
60
- end
88
+ @enq.terminate
89
+
90
+ @sleeper << 0
91
+ @thread&.value
61
92
  end
62
93
 
63
94
  def start
64
- @thread ||= safe_thread("scheduler") do
95
+ @thread ||= safe_thread("scheduler") {
65
96
  initial_wait
66
97
 
67
- while !@done
98
+ until @done
68
99
  enqueue
69
100
  wait
70
101
  end
71
- Sidekiq.logger.info("Scheduler exiting...")
72
- end
102
+ logger.info("Scheduler exiting...")
103
+ }
73
104
  end
74
105
 
75
106
  def enqueue
76
- begin
77
- @enq.enqueue_jobs
78
- rescue => ex
79
- # Most likely a problem with redis networking.
80
- # Punt and try again at the next interval
81
- logger.error ex.message
82
- handle_exception(ex)
83
- end
107
+ @enq.enqueue_jobs
108
+ rescue => ex
109
+ # Most likely a problem with redis networking.
110
+ # Punt and try again at the next interval
111
+ logger.error ex.message
112
+ handle_exception(ex)
84
113
  end
85
114
 
86
115
  private
@@ -115,15 +144,18 @@ module Sidekiq
115
144
  # In the example above, each process should schedule every 10 seconds on average. We special
116
145
  # case smaller clusters to add 50% so they would sleep somewhere between 5 and 15 seconds.
117
146
  # As we run more processes, the scheduling interval average will approach an even spread
118
- # between 0 and poll interval so we don't need this artifical boost.
147
+ # between 0 and poll interval so we don't need this artificial boost.
119
148
  #
120
- if process_count < 10
149
+ count = process_count
150
+ interval = poll_interval_average(count)
151
+
152
+ if count < 10
121
153
  # For small clusters, calculate a random interval that is ±50% the desired average.
122
- poll_interval_average * rand + poll_interval_average.to_f / 2
154
+ interval * rand + interval.to_f / 2
123
155
  else
124
156
  # With 10+ processes, we should have enough randomness to get decent polling
125
157
  # across the entire timespan
126
- poll_interval_average * rand
158
+ interval * rand
127
159
  end
128
160
  end
129
161
 
@@ -140,35 +172,65 @@ module Sidekiq
140
172
  # the same time: the thundering herd problem.
141
173
  #
142
174
  # We only do this if poll_interval_average is unset (the default).
143
- def poll_interval_average
144
- Sidekiq.options[:poll_interval_average] ||= scaled_poll_interval
175
+ def poll_interval_average(count)
176
+ @config[:poll_interval_average] || scaled_poll_interval(count)
145
177
  end
146
178
 
147
179
  # Calculates an average poll interval based on the number of known Sidekiq processes.
148
180
  # This minimizes a single point of failure by dispersing check-ins but without taxing
149
181
  # Redis if you run many Sidekiq processes.
150
- def scaled_poll_interval
151
- process_count * Sidekiq.options[:average_scheduled_poll_interval]
182
+ def scaled_poll_interval(process_count)
183
+ process_count * @config[:average_scheduled_poll_interval]
152
184
  end
153
185
 
154
186
  def process_count
155
- pcount = Sidekiq::ProcessSet.new.size
187
+ pcount = Sidekiq.redis { |conn| conn.scard("processes") }
156
188
  pcount = 1 if pcount == 0
157
189
  pcount
158
190
  end
159
191
 
192
+ # A copy of Sidekiq::ProcessSet#cleanup because server
193
+ # should never depend on sidekiq/api.
194
+ def cleanup
195
+ # dont run cleanup more than once per minute
196
+ return 0 unless redis { |conn| conn.set("process_cleanup", "1", "NX", "EX", "60") }
197
+
198
+ count = 0
199
+ redis do |conn|
200
+ procs = conn.sscan("processes").to_a
201
+ heartbeats = conn.pipelined { |pipeline|
202
+ procs.each do |key|
203
+ pipeline.hget(key, "info")
204
+ end
205
+ }
206
+
207
+ # the hash named key has an expiry of 60 seconds.
208
+ # if it's not found, that means the process has not reported
209
+ # in to Redis and probably died.
210
+ to_prune = procs.select.with_index { |proc, i|
211
+ heartbeats[i].nil?
212
+ }
213
+ count = conn.srem("processes", to_prune) unless to_prune.empty?
214
+ end
215
+ count
216
+ end
217
+
160
218
  def initial_wait
161
- # Have all processes sleep between 5-15 seconds. 10 seconds
162
- # to give time for the heartbeat to register (if the poll interval is going to be calculated by the number
219
+ # Have all processes sleep between 5-15 seconds. 10 seconds to give time for
220
+ # the heartbeat to register (if the poll interval is going to be calculated by the number
163
221
  # of workers), and 5 random seconds to ensure they don't all hit Redis at the same time.
164
222
  total = 0
165
- total += INITIAL_WAIT unless Sidekiq.options[:poll_interval_average]
223
+ total += INITIAL_WAIT unless @config[:poll_interval_average]
166
224
  total += (5 * rand)
167
225
 
168
226
  @sleeper.pop(total)
169
227
  rescue Timeout::Error
228
+ ensure
229
+ # periodically clean out the `processes` set in Redis which can collect
230
+ # references to dead processes over time. The process count affects how
231
+ # often we scan for scheduled jobs.
232
+ cleanup
170
233
  end
171
-
172
234
  end
173
235
  end
174
236
  end