mini-clean-lib 0.0.1

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 (151) hide show
  1. checksums.yaml +7 -0
  2. data/mini-clean-lib.gemspec +12 -0
  3. data/sidekiq-8.1.6/Changes.md +2515 -0
  4. data/sidekiq-8.1.6/LICENSE.txt +9 -0
  5. data/sidekiq-8.1.6/README.md +124 -0
  6. data/sidekiq-8.1.6/bin/kiq +17 -0
  7. data/sidekiq-8.1.6/bin/lint-herb +13 -0
  8. data/sidekiq-8.1.6/bin/multi_queue_bench +271 -0
  9. data/sidekiq-8.1.6/bin/sidekiq +37 -0
  10. data/sidekiq-8.1.6/bin/sidekiqload +256 -0
  11. data/sidekiq-8.1.6/bin/sidekiqmon +11 -0
  12. data/sidekiq-8.1.6/bin/webload +69 -0
  13. data/sidekiq-8.1.6/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
  14. data/sidekiq-8.1.6/lib/generators/sidekiq/job_generator.rb +71 -0
  15. data/sidekiq-8.1.6/lib/generators/sidekiq/templates/job.rb.erb +9 -0
  16. data/sidekiq-8.1.6/lib/generators/sidekiq/templates/job_spec.rb.erb +6 -0
  17. data/sidekiq-8.1.6/lib/generators/sidekiq/templates/job_test.rb.erb +8 -0
  18. data/sidekiq-8.1.6/lib/sidekiq/api.rb +1466 -0
  19. data/sidekiq-8.1.6/lib/sidekiq/capsule.rb +135 -0
  20. data/sidekiq-8.1.6/lib/sidekiq/cli.rb +433 -0
  21. data/sidekiq-8.1.6/lib/sidekiq/client.rb +306 -0
  22. data/sidekiq-8.1.6/lib/sidekiq/component.rb +132 -0
  23. data/sidekiq-8.1.6/lib/sidekiq/config.rb +316 -0
  24. data/sidekiq-8.1.6/lib/sidekiq/deploy.rb +64 -0
  25. data/sidekiq-8.1.6/lib/sidekiq/embedded.rb +64 -0
  26. data/sidekiq-8.1.6/lib/sidekiq/fetch.rb +89 -0
  27. data/sidekiq-8.1.6/lib/sidekiq/iterable_job.rb +56 -0
  28. data/sidekiq-8.1.6/lib/sidekiq/job/interrupt_handler.rb +24 -0
  29. data/sidekiq-8.1.6/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  30. data/sidekiq-8.1.6/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  31. data/sidekiq-8.1.6/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  32. data/sidekiq-8.1.6/lib/sidekiq/job/iterable.rb +322 -0
  33. data/sidekiq-8.1.6/lib/sidekiq/job.rb +400 -0
  34. data/sidekiq-8.1.6/lib/sidekiq/job_logger.rb +54 -0
  35. data/sidekiq-8.1.6/lib/sidekiq/job_retry.rb +332 -0
  36. data/sidekiq-8.1.6/lib/sidekiq/job_util.rb +113 -0
  37. data/sidekiq-8.1.6/lib/sidekiq/launcher.rb +283 -0
  38. data/sidekiq-8.1.6/lib/sidekiq/loader.rb +57 -0
  39. data/sidekiq-8.1.6/lib/sidekiq/logger.rb +87 -0
  40. data/sidekiq-8.1.6/lib/sidekiq/manager.rb +134 -0
  41. data/sidekiq-8.1.6/lib/sidekiq/metrics/query.rb +184 -0
  42. data/sidekiq-8.1.6/lib/sidekiq/metrics/shared.rb +109 -0
  43. data/sidekiq-8.1.6/lib/sidekiq/metrics/tracking.rb +153 -0
  44. data/sidekiq-8.1.6/lib/sidekiq/middleware/chain.rb +207 -0
  45. data/sidekiq-8.1.6/lib/sidekiq/middleware/current_attributes.rb +120 -0
  46. data/sidekiq-8.1.6/lib/sidekiq/middleware/i18n.rb +44 -0
  47. data/sidekiq-8.1.6/lib/sidekiq/middleware/modules.rb +23 -0
  48. data/sidekiq-8.1.6/lib/sidekiq/monitor.rb +143 -0
  49. data/sidekiq-8.1.6/lib/sidekiq/paginator.rb +74 -0
  50. data/sidekiq-8.1.6/lib/sidekiq/processor.rb +298 -0
  51. data/sidekiq-8.1.6/lib/sidekiq/profiler.rb +73 -0
  52. data/sidekiq-8.1.6/lib/sidekiq/rails.rb +63 -0
  53. data/sidekiq-8.1.6/lib/sidekiq/redis_client_adapter.rb +117 -0
  54. data/sidekiq-8.1.6/lib/sidekiq/redis_connection.rb +115 -0
  55. data/sidekiq-8.1.6/lib/sidekiq/ring_buffer.rb +32 -0
  56. data/sidekiq-8.1.6/lib/sidekiq/scheduled.rb +235 -0
  57. data/sidekiq-8.1.6/lib/sidekiq/sd_notify.rb +149 -0
  58. data/sidekiq-8.1.6/lib/sidekiq/systemd.rb +26 -0
  59. data/sidekiq-8.1.6/lib/sidekiq/test_api.rb +331 -0
  60. data/sidekiq-8.1.6/lib/sidekiq/testing/inline.rb +2 -0
  61. data/sidekiq-8.1.6/lib/sidekiq/testing.rb +3 -0
  62. data/sidekiq-8.1.6/lib/sidekiq/transaction_aware_client.rb +59 -0
  63. data/sidekiq-8.1.6/lib/sidekiq/tui/controls.rb +53 -0
  64. data/sidekiq-8.1.6/lib/sidekiq/tui/filtering.rb +53 -0
  65. data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/base_tab.rb +204 -0
  66. data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/busy.rb +118 -0
  67. data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/dead.rb +19 -0
  68. data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/home.rb +144 -0
  69. data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/metrics.rb +131 -0
  70. data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/queues.rb +95 -0
  71. data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/retries.rb +19 -0
  72. data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/scheduled.rb +19 -0
  73. data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/set_tab.rb +96 -0
  74. data/sidekiq-8.1.6/lib/sidekiq/tui/tabs.rb +15 -0
  75. data/sidekiq-8.1.6/lib/sidekiq/tui.rb +382 -0
  76. data/sidekiq-8.1.6/lib/sidekiq/version.rb +10 -0
  77. data/sidekiq-8.1.6/lib/sidekiq/web/action.rb +178 -0
  78. data/sidekiq-8.1.6/lib/sidekiq/web/application.rb +480 -0
  79. data/sidekiq-8.1.6/lib/sidekiq/web/config.rb +117 -0
  80. data/sidekiq-8.1.6/lib/sidekiq/web/helpers.rb +474 -0
  81. data/sidekiq-8.1.6/lib/sidekiq/web/router.rb +91 -0
  82. data/sidekiq-8.1.6/lib/sidekiq/web.rb +140 -0
  83. data/sidekiq-8.1.6/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  84. data/sidekiq-8.1.6/lib/sidekiq.rb +161 -0
  85. data/sidekiq-8.1.6/sidekiq.gemspec +31 -0
  86. data/sidekiq-8.1.6/web/assets/images/apple-touch-icon.png +0 -0
  87. data/sidekiq-8.1.6/web/assets/images/favicon.ico +0 -0
  88. data/sidekiq-8.1.6/web/assets/images/logo.png +0 -0
  89. data/sidekiq-8.1.6/web/assets/images/status.png +0 -0
  90. data/sidekiq-8.1.6/web/assets/javascripts/application.js +204 -0
  91. data/sidekiq-8.1.6/web/assets/javascripts/base-charts.js +120 -0
  92. data/sidekiq-8.1.6/web/assets/javascripts/chart.min.js +13 -0
  93. data/sidekiq-8.1.6/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  94. data/sidekiq-8.1.6/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  95. data/sidekiq-8.1.6/web/assets/javascripts/dashboard-charts.js +194 -0
  96. data/sidekiq-8.1.6/web/assets/javascripts/dashboard.js +63 -0
  97. data/sidekiq-8.1.6/web/assets/javascripts/metrics.js +280 -0
  98. data/sidekiq-8.1.6/web/assets/stylesheets/style.css +776 -0
  99. data/sidekiq-8.1.6/web/locales/ar.yml +89 -0
  100. data/sidekiq-8.1.6/web/locales/cs.yml +80 -0
  101. data/sidekiq-8.1.6/web/locales/da.yml +77 -0
  102. data/sidekiq-8.1.6/web/locales/de.yml +83 -0
  103. data/sidekiq-8.1.6/web/locales/el.yml +89 -0
  104. data/sidekiq-8.1.6/web/locales/en.yml +110 -0
  105. data/sidekiq-8.1.6/web/locales/es.yml +109 -0
  106. data/sidekiq-8.1.6/web/locales/fa.yml +82 -0
  107. data/sidekiq-8.1.6/web/locales/fr.yml +100 -0
  108. data/sidekiq-8.1.6/web/locales/gd.yml +110 -0
  109. data/sidekiq-8.1.6/web/locales/he.yml +82 -0
  110. data/sidekiq-8.1.6/web/locales/hi.yml +77 -0
  111. data/sidekiq-8.1.6/web/locales/it.yml +109 -0
  112. data/sidekiq-8.1.6/web/locales/ja.yml +92 -0
  113. data/sidekiq-8.1.6/web/locales/ko.yml +70 -0
  114. data/sidekiq-8.1.6/web/locales/lt.yml +85 -0
  115. data/sidekiq-8.1.6/web/locales/nb.yml +79 -0
  116. data/sidekiq-8.1.6/web/locales/nl.yml +70 -0
  117. data/sidekiq-8.1.6/web/locales/pl.yml +61 -0
  118. data/sidekiq-8.1.6/web/locales/pt-BR.yml +97 -0
  119. data/sidekiq-8.1.6/web/locales/pt.yml +69 -0
  120. data/sidekiq-8.1.6/web/locales/ru.yml +85 -0
  121. data/sidekiq-8.1.6/web/locales/sv.yml +70 -0
  122. data/sidekiq-8.1.6/web/locales/ta.yml +77 -0
  123. data/sidekiq-8.1.6/web/locales/tr.yml +102 -0
  124. data/sidekiq-8.1.6/web/locales/uk.yml +102 -0
  125. data/sidekiq-8.1.6/web/locales/ur.yml +82 -0
  126. data/sidekiq-8.1.6/web/locales/vi.yml +85 -0
  127. data/sidekiq-8.1.6/web/locales/zh-CN.yml +107 -0
  128. data/sidekiq-8.1.6/web/locales/zh-TW.yml +103 -0
  129. data/sidekiq-8.1.6/web/views/_footer.html.erb +32 -0
  130. data/sidekiq-8.1.6/web/views/_job_info.html.erb +115 -0
  131. data/sidekiq-8.1.6/web/views/_metrics_period_select.html.erb +15 -0
  132. data/sidekiq-8.1.6/web/views/_nav.html.erb +45 -0
  133. data/sidekiq-8.1.6/web/views/_paging.html.erb +26 -0
  134. data/sidekiq-8.1.6/web/views/_poll_link.html.erb +4 -0
  135. data/sidekiq-8.1.6/web/views/_summary.html.erb +40 -0
  136. data/sidekiq-8.1.6/web/views/busy.html.erb +155 -0
  137. data/sidekiq-8.1.6/web/views/dashboard.html.erb +104 -0
  138. data/sidekiq-8.1.6/web/views/dead.html.erb +38 -0
  139. data/sidekiq-8.1.6/web/views/filtering.html.erb +6 -0
  140. data/sidekiq-8.1.6/web/views/layout.html.erb +26 -0
  141. data/sidekiq-8.1.6/web/views/metrics.html.erb +85 -0
  142. data/sidekiq-8.1.6/web/views/metrics_for_job.html.erb +58 -0
  143. data/sidekiq-8.1.6/web/views/morgue.html.erb +69 -0
  144. data/sidekiq-8.1.6/web/views/profiles.html.erb +43 -0
  145. data/sidekiq-8.1.6/web/views/queue.html.erb +57 -0
  146. data/sidekiq-8.1.6/web/views/queues.html.erb +46 -0
  147. data/sidekiq-8.1.6/web/views/retries.html.erb +77 -0
  148. data/sidekiq-8.1.6/web/views/retry.html.erb +39 -0
  149. data/sidekiq-8.1.6/web/views/scheduled.html.erb +64 -0
  150. data/sidekiq-8.1.6/web/views/scheduled_job_info.html.erb +8 -0
  151. metadata +190 -0
@@ -0,0 +1,73 @@
1
+ require "fileutils"
2
+ require "sidekiq/component"
3
+
4
+ module Sidekiq
5
+ # Allows the user to profile jobs running in production.
6
+ # See details in the Profiling wiki page.
7
+ class Profiler
8
+ EXPIRY = 86400 # 1 day
9
+ DEFAULT_OPTIONS = {
10
+ mode: :wall
11
+ }
12
+
13
+ include Sidekiq::Component
14
+
15
+ def initialize(config)
16
+ @config = config
17
+ @vernier_output_dir = ENV.fetch("VERNIER_OUTPUT_DIR") { Dir.tmpdir }
18
+ end
19
+
20
+ def call(job, &block)
21
+ return yield unless job["profile"]
22
+
23
+ token = job["profile"]
24
+ type = job["wrapped"] || job["class"]
25
+ jid = job["jid"]
26
+ started_at = Time.now
27
+
28
+ rundata = {
29
+ started_at: started_at.to_i,
30
+ token: token,
31
+ type: type,
32
+ jid: jid,
33
+ # .gz extension tells Vernier to compress the data
34
+ filename: File.join(
35
+ @vernier_output_dir,
36
+ "#{token}-#{type}-#{jid}-#{started_at.strftime("%Y%m%d-%H%M%S")}.json.gz"
37
+ )
38
+ }
39
+ profiler_options = profiler_options(job, rundata)
40
+
41
+ require "vernier"
42
+ begin
43
+ a = Time.now
44
+ rc = Vernier.profile(**profiler_options, &block)
45
+ b = Time.now
46
+
47
+ # Failed jobs will raise an exception on previous line and skip this
48
+ # block. Only successful jobs will persist profile data to Redis.
49
+ key = "#{token}-#{jid}"
50
+ data = File.read(rundata[:filename])
51
+ redis do |conn|
52
+ conn.multi do |m|
53
+ m.zadd("profiles", Time.now.to_f + EXPIRY, key)
54
+ m.hset(key, rundata.merge(elapsed: (b - a), data: data, size: data.bytesize))
55
+ m.expire(key, EXPIRY)
56
+ end
57
+ end
58
+ rc
59
+ ensure
60
+ FileUtils.rm_f(rundata[:filename])
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def profiler_options(job, rundata)
67
+ profiler_options = (job["profiler_options"] || {}).transform_keys(&:to_sym)
68
+ profiler_options[:mode] = profiler_options[:mode].to_sym if profiler_options[:mode]
69
+
70
+ DEFAULT_OPTIONS.merge(profiler_options, {out: rundata[:filename]})
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ begin
5
+ gem "railties", ">= 7.0"
6
+ require "rails"
7
+ require "sidekiq/job"
8
+ require_relative "../active_job/queue_adapters/sidekiq_adapter"
9
+
10
+ class Rails < ::Rails::Engine
11
+ class Reloader
12
+ def initialize(app = ::Rails.application)
13
+ @app = app
14
+ end
15
+
16
+ def call
17
+ params = (::Rails::VERSION::STRING >= "7.1") ? {source: "job.sidekiq"} : {}
18
+ @app.reloader.wrap(**params) do
19
+ yield
20
+ end
21
+ end
22
+
23
+ def inspect
24
+ "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
25
+ end
26
+
27
+ def to_hash
28
+ {app: @app.class.name}
29
+ end
30
+ end
31
+
32
+ initializer "sidekiq.backtrace_cleaner" do
33
+ Sidekiq.configure_server do |config|
34
+ config[:backtrace_cleaner] = ->(backtrace) { ::Rails.backtrace_cleaner.clean(backtrace) }
35
+ end
36
+ end
37
+
38
+ # This hook happens after all initializers are run, just before returning
39
+ # from config/environment.rb back to sidekiq/cli.rb.
40
+ #
41
+ # None of this matters on the client-side, only within the Sidekiq process itself.
42
+ config.after_initialize do
43
+ Sidekiq.configure_server do |config|
44
+ config[:reloader] = Sidekiq::Rails::Reloader.new
45
+
46
+ # This is the integration code necessary so that if a job uses `Rails.logger.info "Hello"`,
47
+ # it will appear in the Sidekiq console with all of the job context.
48
+ unless ::Rails.logger == config.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
49
+ if ::Rails.logger.respond_to?(:broadcast_to)
50
+ ::Rails.logger.broadcast_to(config.logger)
51
+ elsif ::ActiveSupport::Logger.respond_to?(:broadcast)
52
+ ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
53
+ else
54
+ ::Rails.logger = ::ActiveSupport::BroadcastLogger.new(::Rails.logger, config.logger)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ rescue Gem::LoadError
61
+ # Rails not available or version requirement not met
62
+ end
63
+ end
@@ -0,0 +1,117 @@
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
+ # Identify ourselves to Redis via CLIENT SETINFO so connections
111
+ # are distinguishable in CLIENT LIST / CLIENT INFO output.
112
+ opts[:driver_info] ||= "sidekiq_v#{Sidekiq::VERSION}"
113
+
114
+ opts
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "connection_pool"
4
+ require "uri"
5
+ require "sidekiq/redis_client_adapter"
6
+
7
+ module Sidekiq
8
+ module RedisConnection
9
+ class << self
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
+ 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) do
37
+ redis_config.new_client
38
+ end
39
+ end
40
+
41
+ private
42
+
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 }
47
+ else
48
+ pwd
49
+ end
50
+ end
51
+
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
62
+ end
63
+ end
64
+
65
+ def scrub(options)
66
+ redacted = "REDACTED"
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)))
73
+ if scrubbed_options[:url] && (uri = URI.parse(scrubbed_options[:url])) && uri.password
74
+ uri.password = redacted
75
+ scrubbed_options[:url] = uri.to_s
76
+ end
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
88
+ end
89
+ scrubbed_options
90
+ end
91
+
92
+ def determine_redis_provider
93
+ # If you have this in your environment:
94
+ # MY_REDIS_URL=redis://hostname.example.com:1238/4
95
+ # then set:
96
+ # REDIS_PROVIDER=MY_REDIS_URL
97
+ # and Sidekiq will find your custom URL variable with no custom
98
+ # initialization code at all.
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
109
+ end
110
+
111
+ ENV[p.to_s] || ENV["REDIS_URL"]
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Sidekiq
6
+ class RingBuffer
7
+ include Enumerable
8
+ extend Forwardable
9
+
10
+ def_delegators :@buf, :[], :each, :size
11
+
12
+ def initialize(size, default = 0)
13
+ @size = size
14
+ @buf = Array.new(size, default)
15
+ @index = 0
16
+ end
17
+
18
+ def <<(element)
19
+ @buf[@index % @size] = element
20
+ @index += 1
21
+ element
22
+ end
23
+
24
+ def buffer
25
+ @buf
26
+ end
27
+
28
+ def reset(default = 0)
29
+ @buf.fill(default)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+ require "sidekiq/component"
5
+
6
+ module Sidekiq
7
+ module Scheduled
8
+ SETS = %w[retry schedule]
9
+
10
+ class Enq
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)
30
+ # A job's "score" in Redis is the time at which it should be processed.
31
+ # Just check Redis for the set of jobs with a timestamp before now.
32
+ redis do |conn|
33
+ sorted_sets.each do |sorted_set|
34
+ # Get next item in the queue with score (time to execute) <= now.
35
+ # We need to go through the list one at a time to reduce the risk of something
36
+ # going wrong between the time jobs are popped from the scheduled queue and when
37
+ # they are pushed onto a work queue and losing the jobs.
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}" }
41
+ end
42
+ end
43
+ end
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
64
+ end
65
+
66
+ ##
67
+ # The Poller checks Redis every N seconds for jobs in the retry or scheduled
68
+ # set have passed their timestamp and should be enqueued. If so, it
69
+ # just pops the job back onto its original queue so the
70
+ # workers can pick it up like any other job.
71
+ class Poller
72
+ include Sidekiq::Component
73
+
74
+ INITIAL_WAIT = 10
75
+ attr_accessor :rnd
76
+
77
+ def initialize(config)
78
+ @config = config
79
+ @enq = (config[:scheduled_enq] || Sidekiq::Scheduled::Enq).new(config)
80
+ @sleeper = ConnectionPool::TimedStack.new
81
+ @done = false
82
+ @thread = nil
83
+ @count_calls = 0
84
+ @rnd = Random.new
85
+ end
86
+
87
+ # Shut down this instance, will pause until the thread is dead.
88
+ def terminate
89
+ @done = true
90
+ @enq.terminate
91
+
92
+ @sleeper << 0
93
+ @thread&.value
94
+ end
95
+
96
+ def start
97
+ @thread ||= safe_thread("scheduler") {
98
+ initial_wait
99
+
100
+ until @done
101
+ enqueue
102
+ wait
103
+ end
104
+ logger.info("Scheduler exiting...")
105
+ }
106
+ end
107
+
108
+ def enqueue
109
+ @enq.enqueue_jobs
110
+ rescue => ex
111
+ # Most likely a problem with redis networking.
112
+ # Punt and try again at the next interval
113
+ logger.error ex.message
114
+ handle_exception(ex)
115
+ end
116
+
117
+ private
118
+
119
+ def wait
120
+ @sleeper.pop(timeout: random_poll_interval, exception: false)
121
+ rescue => ex
122
+ # if poll_interval_average hasn't been calculated yet, we can
123
+ # raise an error trying to reach Redis.
124
+ logger.error ex.message
125
+ handle_exception(ex)
126
+ sleep 5
127
+ end
128
+
129
+ def random_poll_interval
130
+ # We want one Sidekiq process to schedule jobs every N seconds. We have M processes
131
+ # and **don't** want to coordinate.
132
+ #
133
+ # So in N*M second timespan, we want each process to schedule once. The basic loop is:
134
+ #
135
+ # * sleep a random amount within that N*M timespan
136
+ # * wake up and schedule
137
+ #
138
+ # We want to avoid one edge case: imagine a set of 2 processes, scheduling every 5 seconds,
139
+ # so N*M = 10. Each process decides to randomly sleep 8 seconds, now we've failed to meet
140
+ # that 5 second average. Thankfully each schedule cycle will sleep randomly so the next
141
+ # iteration could see each process sleep for 1 second, undercutting our average.
142
+ #
143
+ # So below 10 processes, we special case and ensure the processes sleep closer to the average.
144
+ # In the example above, each process should schedule every 10 seconds on average. We special
145
+ # case smaller clusters to add 50% so they would sleep somewhere between 5 and 15 seconds.
146
+ # As we run more processes, the scheduling interval average will approach an even spread
147
+ # between 0 and poll interval so we don't need this artificial boost.
148
+ #
149
+ count = process_count
150
+ interval = poll_interval_average(count)
151
+
152
+ if count < 10
153
+ # For small clusters, calculate a random interval that is ±50% the desired average.
154
+ interval * @rnd.rand + interval.to_f / 2
155
+ else
156
+ # With 10+ processes, we should have enough randomness to get decent polling
157
+ # across the entire timespan
158
+ interval * @rnd.rand * 2
159
+ end
160
+ end
161
+
162
+ # We do our best to tune the poll interval to the size of the active Sidekiq
163
+ # cluster. If you have 30 processes and poll every 15 seconds, that means one
164
+ # Sidekiq is checking Redis every 0.5 seconds - way too often for most people
165
+ # and really bad if the retry or scheduled sets are large.
166
+ #
167
+ # Instead try to avoid polling more than once every 15 seconds. If you have
168
+ # 30 Sidekiq processes, we'll poll every 30 * 15 or 450 seconds.
169
+ # To keep things statistically random, we'll sleep a random amount between
170
+ # 225 and 675 seconds for each poll or 450 seconds on average. Otherwise restarting
171
+ # all your Sidekiq processes at the same time will lead to them all polling at
172
+ # the same time: the thundering herd problem.
173
+ #
174
+ # We only do this if poll_interval_average is unset (the default).
175
+ def poll_interval_average(count)
176
+ @config[:poll_interval_average] || scaled_poll_interval(count)
177
+ end
178
+
179
+ # Calculates an average poll interval based on the number of known Sidekiq processes.
180
+ # This minimizes a single point of failure by dispersing check-ins but without taxing
181
+ # Redis if you run many Sidekiq processes.
182
+ def scaled_poll_interval(process_count)
183
+ process_count * @config[:average_scheduled_poll_interval]
184
+ end
185
+
186
+ def process_count
187
+ pcount = Sidekiq.redis { |conn| conn.scard("processes") }
188
+ pcount = 1 if pcount == 0
189
+ pcount
190
+ end
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
+
218
+ def initial_wait
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
221
+ # of workers), and 5 random seconds to ensure they don't all hit Redis at the same time.
222
+ total = 0
223
+ total += INITIAL_WAIT unless @config[:poll_interval_average]
224
+ total += (5 * rand)
225
+
226
+ @sleeper.pop(timeout: total, exception: false)
227
+ ensure
228
+ # periodically clean out the `processes` set in Redis which can collect
229
+ # references to dead processes over time. The process count affects how
230
+ # often we scan for scheduled jobs.
231
+ cleanup
232
+ end
233
+ end
234
+ end
235
+ end