sidekiq 4.2.10 → 6.1.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sidekiq might be problematic. Click here for more details.

Files changed (106) hide show
  1. checksums.yaml +5 -5
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +20 -0
  3. data/.github/workflows/ci.yml +41 -0
  4. data/.gitignore +2 -1
  5. data/.standard.yml +20 -0
  6. data/5.0-Upgrade.md +56 -0
  7. data/6.0-Upgrade.md +72 -0
  8. data/COMM-LICENSE +12 -10
  9. data/Changes.md +354 -1
  10. data/Ent-2.0-Upgrade.md +37 -0
  11. data/Ent-Changes.md +111 -3
  12. data/Gemfile +16 -21
  13. data/Gemfile.lock +192 -0
  14. data/LICENSE +1 -1
  15. data/Pro-4.0-Upgrade.md +35 -0
  16. data/Pro-5.0-Upgrade.md +25 -0
  17. data/Pro-Changes.md +181 -4
  18. data/README.md +19 -33
  19. data/Rakefile +6 -8
  20. data/bin/sidekiq +26 -2
  21. data/bin/sidekiqload +37 -34
  22. data/bin/sidekiqmon +8 -0
  23. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +1 -1
  24. data/lib/generators/sidekiq/templates/worker_test.rb.erb +1 -1
  25. data/lib/generators/sidekiq/worker_generator.rb +21 -13
  26. data/lib/sidekiq.rb +86 -61
  27. data/lib/sidekiq/api.rb +320 -209
  28. data/lib/sidekiq/cli.rb +207 -217
  29. data/lib/sidekiq/client.rb +78 -51
  30. data/lib/sidekiq/delay.rb +41 -0
  31. data/lib/sidekiq/exception_handler.rb +12 -16
  32. data/lib/sidekiq/extensions/action_mailer.rb +13 -22
  33. data/lib/sidekiq/extensions/active_record.rb +13 -10
  34. data/lib/sidekiq/extensions/class_methods.rb +14 -11
  35. data/lib/sidekiq/extensions/generic_proxy.rb +10 -4
  36. data/lib/sidekiq/fetch.rb +29 -30
  37. data/lib/sidekiq/job_logger.rb +63 -0
  38. data/lib/sidekiq/job_retry.rb +262 -0
  39. data/lib/sidekiq/launcher.rb +102 -69
  40. data/lib/sidekiq/logger.rb +165 -0
  41. data/lib/sidekiq/manager.rb +16 -19
  42. data/lib/sidekiq/middleware/chain.rb +15 -5
  43. data/lib/sidekiq/middleware/i18n.rb +5 -7
  44. data/lib/sidekiq/monitor.rb +133 -0
  45. data/lib/sidekiq/paginator.rb +18 -14
  46. data/lib/sidekiq/processor.rb +161 -82
  47. data/lib/sidekiq/rails.rb +27 -100
  48. data/lib/sidekiq/redis_connection.rb +60 -20
  49. data/lib/sidekiq/scheduled.rb +61 -35
  50. data/lib/sidekiq/sd_notify.rb +149 -0
  51. data/lib/sidekiq/systemd.rb +24 -0
  52. data/lib/sidekiq/testing.rb +48 -28
  53. data/lib/sidekiq/testing/inline.rb +2 -1
  54. data/lib/sidekiq/util.rb +20 -16
  55. data/lib/sidekiq/version.rb +2 -1
  56. data/lib/sidekiq/web.rb +57 -57
  57. data/lib/sidekiq/web/action.rb +14 -14
  58. data/lib/sidekiq/web/application.rb +103 -84
  59. data/lib/sidekiq/web/csrf_protection.rb +158 -0
  60. data/lib/sidekiq/web/helpers.rb +126 -71
  61. data/lib/sidekiq/web/router.rb +18 -17
  62. data/lib/sidekiq/worker.rb +164 -41
  63. data/sidekiq.gemspec +15 -27
  64. data/web/assets/javascripts/application.js +25 -27
  65. data/web/assets/javascripts/dashboard.js +33 -37
  66. data/web/assets/stylesheets/application-dark.css +143 -0
  67. data/web/assets/stylesheets/application-rtl.css +246 -0
  68. data/web/assets/stylesheets/application.css +385 -10
  69. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  70. data/web/assets/stylesheets/bootstrap.css +2 -2
  71. data/web/locales/ar.yml +81 -0
  72. data/web/locales/de.yml +14 -2
  73. data/web/locales/en.yml +4 -0
  74. data/web/locales/es.yml +4 -3
  75. data/web/locales/fa.yml +1 -0
  76. data/web/locales/fr.yml +2 -2
  77. data/web/locales/he.yml +79 -0
  78. data/web/locales/ja.yml +9 -4
  79. data/web/locales/lt.yml +83 -0
  80. data/web/locales/pl.yml +4 -4
  81. data/web/locales/ru.yml +4 -0
  82. data/web/locales/ur.yml +80 -0
  83. data/web/locales/vi.yml +83 -0
  84. data/web/views/_footer.erb +5 -2
  85. data/web/views/_job_info.erb +2 -1
  86. data/web/views/_nav.erb +4 -18
  87. data/web/views/_paging.erb +1 -1
  88. data/web/views/busy.erb +15 -8
  89. data/web/views/dashboard.erb +1 -1
  90. data/web/views/dead.erb +2 -2
  91. data/web/views/layout.erb +12 -2
  92. data/web/views/morgue.erb +9 -6
  93. data/web/views/queue.erb +18 -8
  94. data/web/views/queues.erb +11 -1
  95. data/web/views/retries.erb +14 -7
  96. data/web/views/retry.erb +2 -2
  97. data/web/views/scheduled.erb +7 -4
  98. metadata +41 -188
  99. data/.github/issue_template.md +0 -9
  100. data/.travis.yml +0 -18
  101. data/bin/sidekiqctl +0 -99
  102. data/lib/sidekiq/core_ext.rb +0 -119
  103. data/lib/sidekiq/logging.rb +0 -106
  104. data/lib/sidekiq/middleware/server/active_record.rb +0 -13
  105. data/lib/sidekiq/middleware/server/logging.rb +0 -31
  106. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
@@ -1,101 +1,9 @@
1
1
  # frozen_string_literal: true
2
- module Sidekiq
3
- def self.hook_rails!
4
- return if defined?(@delay_removed)
5
-
6
- ActiveSupport.on_load(:active_record) do
7
- include Sidekiq::Extensions::ActiveRecord
8
- end
9
-
10
- ActiveSupport.on_load(:action_mailer) do
11
- extend Sidekiq::Extensions::ActionMailer
12
- end
13
-
14
- Module.__send__(:include, Sidekiq::Extensions::Klass)
15
- end
16
-
17
- # Removes the generic aliases which MAY clash with names of already
18
- # created methods by other applications. The methods `sidekiq_delay`,
19
- # `sidekiq_delay_for` and `sidekiq_delay_until` can be used instead.
20
- def self.remove_delay!
21
- @delay_removed = true
22
2
 
23
- [Extensions::ActiveRecord,
24
- Extensions::ActionMailer,
25
- Extensions::Klass].each do |mod|
26
- mod.module_eval do
27
- remove_method :delay if respond_to?(:delay)
28
- remove_method :delay_for if respond_to?(:delay_for)
29
- remove_method :delay_until if respond_to?(:delay_until)
30
- end
31
- end
32
- end
3
+ require "sidekiq/worker"
33
4
 
5
+ module Sidekiq
34
6
  class Rails < ::Rails::Engine
35
- # We need to setup this up before any application configuration which might
36
- # change Sidekiq middleware.
37
- #
38
- # This hook happens after `Rails::Application` is inherited within
39
- # config/application.rb and before config is touched, usually within the
40
- # class block. Definitely before config/environments/*.rb and
41
- # config/initializers/*.rb.
42
- config.before_configuration do
43
- if ::Rails::VERSION::MAJOR < 5 && defined?(::ActiveRecord)
44
- Sidekiq.server_middleware do |chain|
45
- require 'sidekiq/middleware/server/active_record'
46
- chain.add Sidekiq::Middleware::Server::ActiveRecord
47
- end
48
- end
49
- end
50
-
51
- initializer 'sidekiq' do
52
- Sidekiq.hook_rails!
53
- end
54
-
55
- config.after_initialize do
56
- # This hook happens after all initializers are run, just before returning
57
- # from config/environment.rb back to sidekiq/cli.rb.
58
- # We have to add the reloader after initialize to see if cache_classes has
59
- # been turned on.
60
- #
61
- # None of this matters on the client-side, only within the Sidekiq process itself.
62
- #
63
- Sidekiq.configure_server do |_|
64
- if ::Rails::VERSION::MAJOR >= 5
65
- # The reloader also takes care of ActiveRecord but is incompatible with
66
- # the ActiveRecord middleware so make sure it's not in the chain already.
67
- if defined?(Sidekiq::Middleware::Server::ActiveRecord) && Sidekiq.server_middleware.exists?(Sidekiq::Middleware::Server::ActiveRecord)
68
- raise ArgumentError, "You are using the Sidekiq ActiveRecord middleware and the new Rails 5 reloader which are incompatible. Please remove the ActiveRecord middleware from your Sidekiq middleware configuration."
69
- elsif ::Rails.application.config.cache_classes
70
- # The reloader API has proven to be troublesome under load in production.
71
- # We won't use it at all when classes are cached, see #3154
72
- Sidekiq.logger.debug { "Autoload disabled in #{::Rails.env}, Sidekiq will not reload changed classes" }
73
- Sidekiq.options[:executor] = Sidekiq::Rails::Executor.new
74
- else
75
- Sidekiq.logger.debug { "Enabling Rails 5+ live code reloading, so hot!" }
76
- Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
77
- Psych::Visitors::ToRuby.prepend(Sidekiq::Rails::PsychAutoload)
78
- end
79
- end
80
- end
81
- end
82
-
83
- class Executor
84
- def initialize(app = ::Rails.application)
85
- @app = app
86
- end
87
-
88
- def call
89
- @app.executor.wrap do
90
- yield
91
- end
92
- end
93
-
94
- def inspect
95
- "#<Sidekiq::Rails::Executor @app=#{@app.class.name}>"
96
- end
97
- end
98
-
99
7
  class Reloader
100
8
  def initialize(app = ::Rails.application)
101
9
  @app = app
@@ -112,12 +20,31 @@ module Sidekiq
112
20
  end
113
21
  end
114
22
 
115
- module PsychAutoload
116
- def resolve_class(klass_name)
117
- klass_name && klass_name.constantize
118
- rescue NameError
119
- super
23
+ # By including the Options module, we allow AJs to directly control sidekiq features
24
+ # via the *sidekiq_options* class method and, for instance, not use AJ's retry system.
25
+ # AJ retries don't show up in the Sidekiq UI Retries tab, save any error data, can't be
26
+ # manually retried, don't automatically die, etc.
27
+ #
28
+ # class SomeJob < ActiveJob::Base
29
+ # queue_as :default
30
+ # sidekiq_options retry: 3, backtrace: 10
31
+ # def perform
32
+ # end
33
+ # end
34
+ initializer "sidekiq.active_job_integration" do
35
+ ActiveSupport.on_load(:active_job) do
36
+ include ::Sidekiq::Worker::Options unless respond_to?(:sidekiq_options)
120
37
  end
121
38
  end
122
- end if defined?(::Rails)
39
+
40
+ # This hook happens after all initializers are run, just before returning
41
+ # from config/environment.rb back to sidekiq/cli.rb.
42
+ #
43
+ # None of this matters on the client-side, only within the Sidekiq process itself.
44
+ config.after_initialize do
45
+ Sidekiq.configure_server do |_|
46
+ Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
47
+ end
48
+ end
49
+ end
123
50
  end
@@ -1,26 +1,38 @@
1
1
  # frozen_string_literal: true
2
- require 'connection_pool'
3
- require 'redis'
4
- require 'uri'
2
+
3
+ require "connection_pool"
4
+ require "redis"
5
+ require "uri"
5
6
 
6
7
  module Sidekiq
7
8
  class RedisConnection
8
9
  class << self
10
+ def create(options = {})
11
+ symbolized_options = options.transform_keys(&:to_sym)
9
12
 
10
- def create(options={})
11
- options = options.symbolize_keys
12
-
13
- options[:url] ||= determine_redis_provider
13
+ if !symbolized_options[:url] && (u = determine_redis_provider)
14
+ symbolized_options[:url] = u
15
+ end
14
16
 
15
- size = options[:size] || (Sidekiq.server? ? (Sidekiq.options[:concurrency] + 5) : 5)
17
+ size = if symbolized_options[:size]
18
+ symbolized_options[:size]
19
+ elsif Sidekiq.server?
20
+ # Give ourselves plenty of connections. pool is lazy
21
+ # so we won't create them until we need them.
22
+ Sidekiq.options[:concurrency] + 5
23
+ elsif ENV["RAILS_MAX_THREADS"]
24
+ Integer(ENV["RAILS_MAX_THREADS"])
25
+ else
26
+ 5
27
+ end
16
28
 
17
29
  verify_sizing(size, Sidekiq.options[:concurrency]) if Sidekiq.server?
18
30
 
19
- pool_timeout = options[:pool_timeout] || 1
20
- log_info(options)
31
+ pool_timeout = symbolized_options[:pool_timeout] || 1
32
+ log_info(symbolized_options)
21
33
 
22
- ConnectionPool.new(:timeout => pool_timeout, :size => size) do
23
- build_client(options)
34
+ ConnectionPool.new(timeout: pool_timeout, size: size) do
35
+ build_client(symbolized_options)
24
36
  end
25
37
  end
26
38
 
@@ -35,7 +47,7 @@ module Sidekiq
35
47
  # - enterprise's leader election
36
48
  # - enterprise's cron support
37
49
  def verify_sizing(size, concurrency)
38
- raise ArgumentError, "Your Redis connection pool is too small for Sidekiq to work. Your pool has #{size} connections but really needs to have at least #{concurrency + 2}" if size <= concurrency
50
+ 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 + 2)
39
51
  end
40
52
 
41
53
  def build_client(options)
@@ -44,8 +56,8 @@ module Sidekiq
44
56
  client = Redis.new client_opts(options)
45
57
  if namespace
46
58
  begin
47
- require 'redis/namespace'
48
- Redis::Namespace.new(namespace, :redis => client)
59
+ require "redis/namespace"
60
+ Redis::Namespace.new(namespace, redis: client)
49
61
  rescue LoadError
50
62
  Sidekiq.logger.error("Your Redis configuration uses the namespace '#{namespace}' but the redis-namespace gem is not included in the Gemfile." \
51
63
  "Add the gem to your Gemfile to continue using a namespace. Otherwise, remove the namespace parameter.")
@@ -67,7 +79,7 @@ module Sidekiq
67
79
  opts.delete(:network_timeout)
68
80
  end
69
81
 
70
- opts[:driver] ||= 'ruby'
82
+ opts[:driver] ||= Redis::Connection.drivers.last || "ruby"
71
83
 
72
84
  # Issue #3303, redis-rb will silently retry an operation.
73
85
  # This can lead to duplicate jobs if Sidekiq::Client's LPUSH
@@ -80,9 +92,15 @@ module Sidekiq
80
92
  end
81
93
 
82
94
  def log_info(options)
83
- # Don't log Redis AUTH password
84
95
  redacted = "REDACTED"
85
- scrubbed_options = options.dup
96
+
97
+ # deep clone so we can muck with these options all we want
98
+ #
99
+ # exclude SSL params from dump-and-load because some information isn't
100
+ # safely dumpable in current Rubies
101
+ keys = options.keys
102
+ keys.delete(:ssl_params)
103
+ scrubbed_options = Marshal.load(Marshal.dump(options.slice(*keys)))
86
104
  if scrubbed_options[:url] && (uri = URI.parse(scrubbed_options[:url])) && uri.password
87
105
  uri.password = redacted
88
106
  scrubbed_options[:url] = uri.to_s
@@ -90,6 +108,9 @@ module Sidekiq
90
108
  if scrubbed_options[:password]
91
109
  scrubbed_options[:password] = redacted
92
110
  end
111
+ scrubbed_options[:sentinels]&.each do |sentinel|
112
+ sentinel[:password] = redacted if sentinel[:password]
113
+ end
93
114
  if Sidekiq.server?
94
115
  Sidekiq.logger.info("Booting Sidekiq #{Sidekiq::VERSION} with redis options #{scrubbed_options}")
95
116
  else
@@ -98,9 +119,28 @@ module Sidekiq
98
119
  end
99
120
 
100
121
  def determine_redis_provider
101
- ENV[ENV['REDIS_PROVIDER'] || 'REDIS_URL']
102
- end
122
+ # If you have this in your environment:
123
+ # MY_REDIS_URL=redis://hostname.example.com:1238/4
124
+ # then set:
125
+ # REDIS_PROVIDER=MY_REDIS_URL
126
+ # and Sidekiq will find your custom URL variable with no custom
127
+ # initialization code at all.
128
+ #
129
+ p = ENV["REDIS_PROVIDER"]
130
+ if p && p =~ /:/
131
+ raise <<~EOM
132
+ REDIS_PROVIDER should be set to the name of the variable which contains the Redis URL, not a URL itself.
133
+ Platforms like Heroku will sell addons that publish a *_URL variable. You need to tell Sidekiq with REDIS_PROVIDER, e.g.:
134
+
135
+ REDISTOGO_URL=redis://somehost.example.com:6379/4
136
+ REDIS_PROVIDER=REDISTOGO_URL
137
+ EOM
138
+ end
103
139
 
140
+ ENV[
141
+ p || "REDIS_URL"
142
+ ]
143
+ end
104
144
  end
105
145
  end
106
146
  end
@@ -1,30 +1,32 @@
1
1
  # frozen_string_literal: true
2
- require 'sidekiq'
3
- require 'sidekiq/util'
4
- require 'sidekiq/api'
2
+
3
+ require "sidekiq"
4
+ require "sidekiq/util"
5
+ require "sidekiq/api"
5
6
 
6
7
  module Sidekiq
7
8
  module Scheduled
8
- SETS = %w(retry schedule)
9
+ SETS = %w[retry schedule]
9
10
 
10
11
  class Enq
11
- def enqueue_jobs(now=Time.now.to_f.to_s, sorted_sets=SETS)
12
+ def enqueue_jobs(now = Time.now.to_f.to_s, sorted_sets = SETS)
12
13
  # A job's "score" in Redis is the time at which it should be processed.
13
14
  # Just check Redis for the set of jobs with a timestamp before now.
14
15
  Sidekiq.redis do |conn|
15
16
  sorted_sets.each do |sorted_set|
16
- # Get the next item in the queue if it's score (time to execute) is <= now.
17
- # We need to go through the list one at a time to reduce the risk of something
18
- # going wrong between the time jobs are popped from the scheduled queue and when
19
- # they are pushed onto a work queue and losing the jobs.
20
- while job = conn.zrangebyscore(sorted_set, '-inf'.freeze, 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}" }
17
+ # Get next items in the queue with scores (time to execute) <= now.
18
+ until (jobs = conn.zrangebyscore(sorted_set, "-inf", now, limit: [0, 100])).empty?
19
+ # We need to go through the list one at a time to reduce the risk of something
20
+ # going wrong between the time jobs are popped from the scheduled queue and when
21
+ # they are pushed onto a work queue and losing the jobs.
22
+ jobs.each do |job|
23
+ # Pop item off the queue and add it to the work queue. If the job can't be popped from
24
+ # the queue, it's because another process already popped it so we can move on to the
25
+ # next one.
26
+ if conn.zrem(sorted_set, job)
27
+ Sidekiq::Client.push(Sidekiq.load_json(job))
28
+ Sidekiq.logger.debug { "enqueued #{sorted_set}: #{job}" }
29
+ end
28
30
  end
29
31
  end
30
32
  end
@@ -61,28 +63,24 @@ module Sidekiq
61
63
  end
62
64
 
63
65
  def start
64
- @thread ||= safe_thread("scheduler") do
66
+ @thread ||= safe_thread("scheduler") {
65
67
  initial_wait
66
68
 
67
- while !@done
69
+ until @done
68
70
  enqueue
69
71
  wait
70
72
  end
71
73
  Sidekiq.logger.info("Scheduler exiting...")
72
- end
74
+ }
73
75
  end
74
76
 
75
77
  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
- ex.backtrace.each do |bt|
83
- logger.error(bt)
84
- end
85
- end
78
+ @enq.enqueue_jobs
79
+ rescue => ex
80
+ # Most likely a problem with redis networking.
81
+ # Punt and try again at the next interval
82
+ logger.error ex.message
83
+ handle_exception(ex)
86
84
  end
87
85
 
88
86
  private
@@ -95,13 +93,38 @@ module Sidekiq
95
93
  # if poll_interval_average hasn't been calculated yet, we can
96
94
  # raise an error trying to reach Redis.
97
95
  logger.error ex.message
98
- logger.error ex.backtrace.first
96
+ handle_exception(ex)
99
97
  sleep 5
100
98
  end
101
99
 
102
- # Calculates a random interval that is ±50% the desired average.
103
100
  def random_poll_interval
104
- poll_interval_average * rand + poll_interval_average.to_f / 2
101
+ # We want one Sidekiq process to schedule jobs every N seconds. We have M processes
102
+ # and **don't** want to coordinate.
103
+ #
104
+ # So in N*M second timespan, we want each process to schedule once. The basic loop is:
105
+ #
106
+ # * sleep a random amount within that N*M timespan
107
+ # * wake up and schedule
108
+ #
109
+ # We want to avoid one edge case: imagine a set of 2 processes, scheduling every 5 seconds,
110
+ # so N*M = 10. Each process decides to randomly sleep 8 seconds, now we've failed to meet
111
+ # that 5 second average. Thankfully each schedule cycle will sleep randomly so the next
112
+ # iteration could see each process sleep for 1 second, undercutting our average.
113
+ #
114
+ # So below 10 processes, we special case and ensure the processes sleep closer to the average.
115
+ # In the example above, each process should schedule every 10 seconds on average. We special
116
+ # case smaller clusters to add 50% so they would sleep somewhere between 5 and 15 seconds.
117
+ # 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.
119
+ #
120
+ if process_count < 10
121
+ # For small clusters, calculate a random interval that is ±50% the desired average.
122
+ poll_interval_average * rand + poll_interval_average.to_f / 2
123
+ else
124
+ # With 10+ processes, we should have enough randomness to get decent polling
125
+ # across the entire timespan
126
+ poll_interval_average * rand
127
+ end
105
128
  end
106
129
 
107
130
  # We do our best to tune the poll interval to the size of the active Sidekiq
@@ -125,9 +148,13 @@ module Sidekiq
125
148
  # This minimizes a single point of failure by dispersing check-ins but without taxing
126
149
  # Redis if you run many Sidekiq processes.
127
150
  def scaled_poll_interval
151
+ process_count * Sidekiq.options[:average_scheduled_poll_interval]
152
+ end
153
+
154
+ def process_count
128
155
  pcount = Sidekiq::ProcessSet.new.size
129
156
  pcount = 1 if pcount == 0
130
- pcount * Sidekiq.options[:average_scheduled_poll_interval]
157
+ pcount
131
158
  end
132
159
 
133
160
  def initial_wait
@@ -141,7 +168,6 @@ module Sidekiq
141
168
  @sleeper.pop(total)
142
169
  rescue Timeout::Error
143
170
  end
144
-
145
171
  end
146
172
  end
147
173
  end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The MIT License
4
+ #
5
+ # Copyright (c) 2017, 2018, 2019, 2020 Agis Anastasopoulos
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of
8
+ # this software and associated documentation files (the "Software"), to deal in
9
+ # the Software without restriction, including without limitation the rights to
10
+ # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
11
+ # the Software, and to permit persons to whom the Software is furnished to do so,
12
+ # subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included in all
15
+ # copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
19
+ # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
20
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
21
+ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
22
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ # This is a copy of https://github.com/agis/ruby-sdnotify as of commit a7d52ee
25
+ # The only changes made was "rehoming" it within the Sidekiq module to avoid
26
+ # namespace collisions and applying standard's code formatting style.
27
+
28
+ require "socket"
29
+
30
+ # SdNotify is a pure-Ruby implementation of sd_notify(3). It can be used to
31
+ # notify systemd about state changes. Methods of this package are no-op on
32
+ # non-systemd systems (eg. Darwin).
33
+ #
34
+ # The API maps closely to the original implementation of sd_notify(3),
35
+ # therefore be sure to check the official man pages prior to using SdNotify.
36
+ #
37
+ # @see https://www.freedesktop.org/software/systemd/man/sd_notify.html
38
+ module Sidekiq
39
+ module SdNotify
40
+ # Exception raised when there's an error writing to the notification socket
41
+ class NotifyError < RuntimeError; end
42
+
43
+ READY = "READY=1"
44
+ RELOADING = "RELOADING=1"
45
+ STOPPING = "STOPPING=1"
46
+ STATUS = "STATUS="
47
+ ERRNO = "ERRNO="
48
+ MAINPID = "MAINPID="
49
+ WATCHDOG = "WATCHDOG=1"
50
+ FDSTORE = "FDSTORE=1"
51
+
52
+ def self.ready(unset_env = false)
53
+ notify(READY, unset_env)
54
+ end
55
+
56
+ def self.reloading(unset_env = false)
57
+ notify(RELOADING, unset_env)
58
+ end
59
+
60
+ def self.stopping(unset_env = false)
61
+ notify(STOPPING, unset_env)
62
+ end
63
+
64
+ # @param status [String] a custom status string that describes the current
65
+ # state of the service
66
+ def self.status(status, unset_env = false)
67
+ notify("#{STATUS}#{status}", unset_env)
68
+ end
69
+
70
+ # @param errno [Integer]
71
+ def self.errno(errno, unset_env = false)
72
+ notify("#{ERRNO}#{errno}", unset_env)
73
+ end
74
+
75
+ # @param pid [Integer]
76
+ def self.mainpid(pid, unset_env = false)
77
+ notify("#{MAINPID}#{pid}", unset_env)
78
+ end
79
+
80
+ def self.watchdog(unset_env = false)
81
+ notify(WATCHDOG, unset_env)
82
+ end
83
+
84
+ def self.fdstore(unset_env = false)
85
+ notify(FDSTORE, unset_env)
86
+ end
87
+
88
+ # @return [Boolean] true if the service manager expects watchdog keep-alive
89
+ # notification messages to be sent from this process.
90
+ #
91
+ # If the $WATCHDOG_USEC environment variable is set,
92
+ # and the $WATCHDOG_PID variable is unset or set to the PID of the current
93
+ # process
94
+ #
95
+ # @note Unlike sd_watchdog_enabled(3), this method does not mutate the
96
+ # environment.
97
+ def self.watchdog?
98
+ wd_usec = ENV["WATCHDOG_USEC"]
99
+ wd_pid = ENV["WATCHDOG_PID"]
100
+
101
+ return false unless wd_usec
102
+
103
+ begin
104
+ wd_usec = Integer(wd_usec)
105
+ rescue
106
+ return false
107
+ end
108
+
109
+ return false if wd_usec <= 0
110
+ return true if !wd_pid || wd_pid == $$.to_s
111
+
112
+ false
113
+ end
114
+
115
+ # Notify systemd with the provided state, via the notification socket, if
116
+ # any.
117
+ #
118
+ # Generally this method will be used indirectly through the other methods
119
+ # of the library.
120
+ #
121
+ # @param state [String]
122
+ # @param unset_env [Boolean]
123
+ #
124
+ # @return [Fixnum, nil] the number of bytes written to the notification
125
+ # socket or nil if there was no socket to report to (eg. the program wasn't
126
+ # started by systemd)
127
+ #
128
+ # @raise [NotifyError] if there was an error communicating with the systemd
129
+ # socket
130
+ #
131
+ # @see https://www.freedesktop.org/software/systemd/man/sd_notify.html
132
+ def self.notify(state, unset_env = false)
133
+ sock = ENV["NOTIFY_SOCKET"]
134
+
135
+ return nil unless sock
136
+
137
+ ENV.delete("NOTIFY_SOCKET") if unset_env
138
+
139
+ begin
140
+ Addrinfo.unix(sock, :DGRAM).connect do |s|
141
+ s.close_on_exec = true
142
+ s.write(state)
143
+ end
144
+ rescue => e
145
+ raise NotifyError, "#{e.class}: #{e.message}", e.backtrace
146
+ end
147
+ end
148
+ end
149
+ end