ultra-fast-tool 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.
- checksums.yaml +7 -0
- data/sidekiq-8.1.6/Changes.md +2515 -0
- data/sidekiq-8.1.6/LICENSE.txt +9 -0
- data/sidekiq-8.1.6/README.md +124 -0
- data/sidekiq-8.1.6/bin/kiq +17 -0
- data/sidekiq-8.1.6/bin/lint-herb +13 -0
- data/sidekiq-8.1.6/bin/multi_queue_bench +271 -0
- data/sidekiq-8.1.6/bin/sidekiq +37 -0
- data/sidekiq-8.1.6/bin/sidekiqload +256 -0
- data/sidekiq-8.1.6/bin/sidekiqmon +11 -0
- data/sidekiq-8.1.6/bin/webload +69 -0
- data/sidekiq-8.1.6/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
- data/sidekiq-8.1.6/lib/generators/sidekiq/job_generator.rb +71 -0
- data/sidekiq-8.1.6/lib/generators/sidekiq/templates/job.rb.erb +9 -0
- data/sidekiq-8.1.6/lib/generators/sidekiq/templates/job_spec.rb.erb +6 -0
- data/sidekiq-8.1.6/lib/generators/sidekiq/templates/job_test.rb.erb +8 -0
- data/sidekiq-8.1.6/lib/sidekiq/api.rb +1466 -0
- data/sidekiq-8.1.6/lib/sidekiq/capsule.rb +135 -0
- data/sidekiq-8.1.6/lib/sidekiq/cli.rb +433 -0
- data/sidekiq-8.1.6/lib/sidekiq/client.rb +306 -0
- data/sidekiq-8.1.6/lib/sidekiq/component.rb +132 -0
- data/sidekiq-8.1.6/lib/sidekiq/config.rb +316 -0
- data/sidekiq-8.1.6/lib/sidekiq/deploy.rb +64 -0
- data/sidekiq-8.1.6/lib/sidekiq/embedded.rb +64 -0
- data/sidekiq-8.1.6/lib/sidekiq/fetch.rb +89 -0
- data/sidekiq-8.1.6/lib/sidekiq/iterable_job.rb +56 -0
- data/sidekiq-8.1.6/lib/sidekiq/job/interrupt_handler.rb +24 -0
- data/sidekiq-8.1.6/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
- data/sidekiq-8.1.6/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
- data/sidekiq-8.1.6/lib/sidekiq/job/iterable/enumerators.rb +135 -0
- data/sidekiq-8.1.6/lib/sidekiq/job/iterable.rb +322 -0
- data/sidekiq-8.1.6/lib/sidekiq/job.rb +400 -0
- data/sidekiq-8.1.6/lib/sidekiq/job_logger.rb +54 -0
- data/sidekiq-8.1.6/lib/sidekiq/job_retry.rb +332 -0
- data/sidekiq-8.1.6/lib/sidekiq/job_util.rb +113 -0
- data/sidekiq-8.1.6/lib/sidekiq/launcher.rb +283 -0
- data/sidekiq-8.1.6/lib/sidekiq/loader.rb +57 -0
- data/sidekiq-8.1.6/lib/sidekiq/logger.rb +87 -0
- data/sidekiq-8.1.6/lib/sidekiq/manager.rb +134 -0
- data/sidekiq-8.1.6/lib/sidekiq/metrics/query.rb +184 -0
- data/sidekiq-8.1.6/lib/sidekiq/metrics/shared.rb +109 -0
- data/sidekiq-8.1.6/lib/sidekiq/metrics/tracking.rb +153 -0
- data/sidekiq-8.1.6/lib/sidekiq/middleware/chain.rb +207 -0
- data/sidekiq-8.1.6/lib/sidekiq/middleware/current_attributes.rb +120 -0
- data/sidekiq-8.1.6/lib/sidekiq/middleware/i18n.rb +44 -0
- data/sidekiq-8.1.6/lib/sidekiq/middleware/modules.rb +23 -0
- data/sidekiq-8.1.6/lib/sidekiq/monitor.rb +143 -0
- data/sidekiq-8.1.6/lib/sidekiq/paginator.rb +74 -0
- data/sidekiq-8.1.6/lib/sidekiq/processor.rb +298 -0
- data/sidekiq-8.1.6/lib/sidekiq/profiler.rb +73 -0
- data/sidekiq-8.1.6/lib/sidekiq/rails.rb +63 -0
- data/sidekiq-8.1.6/lib/sidekiq/redis_client_adapter.rb +117 -0
- data/sidekiq-8.1.6/lib/sidekiq/redis_connection.rb +115 -0
- data/sidekiq-8.1.6/lib/sidekiq/ring_buffer.rb +32 -0
- data/sidekiq-8.1.6/lib/sidekiq/scheduled.rb +235 -0
- data/sidekiq-8.1.6/lib/sidekiq/sd_notify.rb +149 -0
- data/sidekiq-8.1.6/lib/sidekiq/systemd.rb +26 -0
- data/sidekiq-8.1.6/lib/sidekiq/test_api.rb +331 -0
- data/sidekiq-8.1.6/lib/sidekiq/testing/inline.rb +2 -0
- data/sidekiq-8.1.6/lib/sidekiq/testing.rb +3 -0
- data/sidekiq-8.1.6/lib/sidekiq/transaction_aware_client.rb +59 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/controls.rb +53 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/filtering.rb +53 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/base_tab.rb +204 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/busy.rb +118 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/dead.rb +19 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/home.rb +144 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/metrics.rb +131 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/queues.rb +95 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/retries.rb +19 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/scheduled.rb +19 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/set_tab.rb +96 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/tabs.rb +15 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui.rb +382 -0
- data/sidekiq-8.1.6/lib/sidekiq/version.rb +10 -0
- data/sidekiq-8.1.6/lib/sidekiq/web/action.rb +178 -0
- data/sidekiq-8.1.6/lib/sidekiq/web/application.rb +480 -0
- data/sidekiq-8.1.6/lib/sidekiq/web/config.rb +117 -0
- data/sidekiq-8.1.6/lib/sidekiq/web/helpers.rb +474 -0
- data/sidekiq-8.1.6/lib/sidekiq/web/router.rb +91 -0
- data/sidekiq-8.1.6/lib/sidekiq/web.rb +140 -0
- data/sidekiq-8.1.6/lib/sidekiq/worker_compatibility_alias.rb +13 -0
- data/sidekiq-8.1.6/lib/sidekiq.rb +161 -0
- data/sidekiq-8.1.6/sidekiq.gemspec +31 -0
- data/sidekiq-8.1.6/web/assets/images/apple-touch-icon.png +0 -0
- data/sidekiq-8.1.6/web/assets/images/favicon.ico +0 -0
- data/sidekiq-8.1.6/web/assets/images/logo.png +0 -0
- data/sidekiq-8.1.6/web/assets/images/status.png +0 -0
- data/sidekiq-8.1.6/web/assets/javascripts/application.js +204 -0
- data/sidekiq-8.1.6/web/assets/javascripts/base-charts.js +120 -0
- data/sidekiq-8.1.6/web/assets/javascripts/chart.min.js +13 -0
- data/sidekiq-8.1.6/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
- data/sidekiq-8.1.6/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
- data/sidekiq-8.1.6/web/assets/javascripts/dashboard-charts.js +194 -0
- data/sidekiq-8.1.6/web/assets/javascripts/dashboard.js +63 -0
- data/sidekiq-8.1.6/web/assets/javascripts/metrics.js +280 -0
- data/sidekiq-8.1.6/web/assets/stylesheets/style.css +776 -0
- data/sidekiq-8.1.6/web/locales/ar.yml +89 -0
- data/sidekiq-8.1.6/web/locales/cs.yml +80 -0
- data/sidekiq-8.1.6/web/locales/da.yml +77 -0
- data/sidekiq-8.1.6/web/locales/de.yml +83 -0
- data/sidekiq-8.1.6/web/locales/el.yml +89 -0
- data/sidekiq-8.1.6/web/locales/en.yml +110 -0
- data/sidekiq-8.1.6/web/locales/es.yml +109 -0
- data/sidekiq-8.1.6/web/locales/fa.yml +82 -0
- data/sidekiq-8.1.6/web/locales/fr.yml +100 -0
- data/sidekiq-8.1.6/web/locales/gd.yml +110 -0
- data/sidekiq-8.1.6/web/locales/he.yml +82 -0
- data/sidekiq-8.1.6/web/locales/hi.yml +77 -0
- data/sidekiq-8.1.6/web/locales/it.yml +109 -0
- data/sidekiq-8.1.6/web/locales/ja.yml +92 -0
- data/sidekiq-8.1.6/web/locales/ko.yml +70 -0
- data/sidekiq-8.1.6/web/locales/lt.yml +85 -0
- data/sidekiq-8.1.6/web/locales/nb.yml +79 -0
- data/sidekiq-8.1.6/web/locales/nl.yml +70 -0
- data/sidekiq-8.1.6/web/locales/pl.yml +61 -0
- data/sidekiq-8.1.6/web/locales/pt-BR.yml +97 -0
- data/sidekiq-8.1.6/web/locales/pt.yml +69 -0
- data/sidekiq-8.1.6/web/locales/ru.yml +85 -0
- data/sidekiq-8.1.6/web/locales/sv.yml +70 -0
- data/sidekiq-8.1.6/web/locales/ta.yml +77 -0
- data/sidekiq-8.1.6/web/locales/tr.yml +102 -0
- data/sidekiq-8.1.6/web/locales/uk.yml +102 -0
- data/sidekiq-8.1.6/web/locales/ur.yml +82 -0
- data/sidekiq-8.1.6/web/locales/vi.yml +85 -0
- data/sidekiq-8.1.6/web/locales/zh-CN.yml +107 -0
- data/sidekiq-8.1.6/web/locales/zh-TW.yml +103 -0
- data/sidekiq-8.1.6/web/views/_footer.html.erb +32 -0
- data/sidekiq-8.1.6/web/views/_job_info.html.erb +115 -0
- data/sidekiq-8.1.6/web/views/_metrics_period_select.html.erb +15 -0
- data/sidekiq-8.1.6/web/views/_nav.html.erb +45 -0
- data/sidekiq-8.1.6/web/views/_paging.html.erb +26 -0
- data/sidekiq-8.1.6/web/views/_poll_link.html.erb +4 -0
- data/sidekiq-8.1.6/web/views/_summary.html.erb +40 -0
- data/sidekiq-8.1.6/web/views/busy.html.erb +155 -0
- data/sidekiq-8.1.6/web/views/dashboard.html.erb +104 -0
- data/sidekiq-8.1.6/web/views/dead.html.erb +38 -0
- data/sidekiq-8.1.6/web/views/filtering.html.erb +6 -0
- data/sidekiq-8.1.6/web/views/layout.html.erb +26 -0
- data/sidekiq-8.1.6/web/views/metrics.html.erb +85 -0
- data/sidekiq-8.1.6/web/views/metrics_for_job.html.erb +58 -0
- data/sidekiq-8.1.6/web/views/morgue.html.erb +69 -0
- data/sidekiq-8.1.6/web/views/profiles.html.erb +43 -0
- data/sidekiq-8.1.6/web/views/queue.html.erb +57 -0
- data/sidekiq-8.1.6/web/views/queues.html.erb +46 -0
- data/sidekiq-8.1.6/web/views/retries.html.erb +77 -0
- data/sidekiq-8.1.6/web/views/retry.html.erb +39 -0
- data/sidekiq-8.1.6/web/views/scheduled.html.erb +64 -0
- data/sidekiq-8.1.6/web/views/scheduled_job_info.html.erb +8 -0
- data/ultra-fast-tool.gemspec +11 -0
- metadata +189 -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
|