sidekiq 6.0.3 → 6.1.0

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

Potentially problematic release.


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

Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +13 -24
  3. data/6.0-Upgrade.md +1 -1
  4. data/Changes.md +61 -0
  5. data/Ent-Changes.md +15 -2
  6. data/Gemfile +2 -2
  7. data/Gemfile.lock +121 -109
  8. data/Pro-Changes.md +15 -1
  9. data/README.md +2 -5
  10. data/bin/sidekiq +26 -2
  11. data/lib/generators/sidekiq/worker_generator.rb +1 -1
  12. data/lib/sidekiq.rb +13 -7
  13. data/lib/sidekiq/api.rb +9 -5
  14. data/lib/sidekiq/cli.rb +24 -5
  15. data/lib/sidekiq/client.rb +23 -12
  16. data/lib/sidekiq/extensions/active_record.rb +3 -2
  17. data/lib/sidekiq/extensions/class_methods.rb +5 -4
  18. data/lib/sidekiq/fetch.rb +20 -18
  19. data/lib/sidekiq/job_logger.rb +1 -1
  20. data/lib/sidekiq/job_retry.rb +2 -2
  21. data/lib/sidekiq/launcher.rb +34 -7
  22. data/lib/sidekiq/logger.rb +9 -9
  23. data/lib/sidekiq/manager.rb +3 -3
  24. data/lib/sidekiq/monitor.rb +2 -2
  25. data/lib/sidekiq/processor.rb +5 -5
  26. data/lib/sidekiq/rails.rb +16 -18
  27. data/lib/sidekiq/redis_connection.rb +21 -13
  28. data/lib/sidekiq/sd_notify.rb +149 -0
  29. data/lib/sidekiq/systemd.rb +24 -0
  30. data/lib/sidekiq/testing.rb +1 -1
  31. data/lib/sidekiq/version.rb +1 -1
  32. data/lib/sidekiq/web.rb +16 -8
  33. data/lib/sidekiq/web/application.rb +15 -9
  34. data/lib/sidekiq/web/csrf_protection.rb +153 -0
  35. data/lib/sidekiq/web/helpers.rb +4 -7
  36. data/lib/sidekiq/web/router.rb +2 -4
  37. data/lib/sidekiq/worker.rb +4 -7
  38. data/sidekiq.gemspec +2 -3
  39. data/web/assets/javascripts/application.js +24 -21
  40. data/web/assets/stylesheets/application-dark.css +132 -124
  41. data/web/assets/stylesheets/application.css +5 -0
  42. data/web/locales/en.yml +2 -0
  43. data/web/locales/fr.yml +2 -2
  44. data/web/locales/ja.yml +2 -0
  45. data/web/locales/lt.yml +83 -0
  46. data/web/locales/pl.yml +4 -4
  47. data/web/locales/vi.yml +83 -0
  48. data/web/views/layout.erb +1 -1
  49. data/web/views/queues.erb +8 -0
  50. metadata +14 -23
@@ -39,7 +39,7 @@ module Sidekiq
39
39
  # attribute to expose the underlying thing.
40
40
  h = {
41
41
  class: job_hash["wrapped"] || job_hash["class"],
42
- jid: job_hash["jid"],
42
+ jid: job_hash["jid"]
43
43
  }
44
44
  h[:bid] = job_hash["bid"] if job_hash["bid"]
45
45
  h[:tags] = job_hash["tags"] if job_hash["tags"]
@@ -189,13 +189,13 @@ module Sidekiq
189
189
  handle_exception(e, {context: "Error calling retries_exhausted", job: msg})
190
190
  end
191
191
 
192
+ send_to_morgue(msg) unless msg["dead"] == false
193
+
192
194
  Sidekiq.death_handlers.each do |handler|
193
195
  handler.call(msg, exception)
194
196
  rescue => e
195
197
  handle_exception(e, {context: "Error calling death handler", job: msg})
196
198
  end
197
-
198
- send_to_morgue(msg) unless msg["dead"] == false
199
199
  end
200
200
 
201
201
  def send_to_morgue(msg)
@@ -16,12 +16,13 @@ module Sidekiq
16
16
  proc { Sidekiq::VERSION },
17
17
  proc { |me, data| data["tag"] },
18
18
  proc { |me, data| "[#{Processor::WORKER_STATE.size} of #{data["concurrency"]} busy]" },
19
- proc { |me, data| "stopping" if me.stopping? },
19
+ proc { |me, data| "stopping" if me.stopping? }
20
20
  ]
21
21
 
22
22
  attr_accessor :manager, :poller, :fetcher
23
23
 
24
24
  def initialize(options)
25
+ options[:fetch] ||= BasicFetch.new(options)
25
26
  @manager = Sidekiq::Manager.new(options)
26
27
  @poller = Sidekiq::Scheduled::Poller.new
27
28
  @done = false
@@ -56,7 +57,7 @@ module Sidekiq
56
57
 
57
58
  # Requeue everything in case there was a worker who grabbed work while stopped
58
59
  # This call is a no-op in Sidekiq but necessary for Sidekiq Pro.
59
- strategy = (@options[:fetch] || Sidekiq::BasicFetch)
60
+ strategy = @options[:fetch]
60
61
  strategy.bulk_requeue([], @options)
61
62
 
62
63
  clear_heartbeat
@@ -83,7 +84,7 @@ module Sidekiq
83
84
  Sidekiq.redis do |conn|
84
85
  conn.pipelined do
85
86
  conn.srem("processes", identity)
86
- conn.del("#{identity}:workers")
87
+ conn.unlink("#{identity}:workers")
87
88
  end
88
89
  end
89
90
  rescue
@@ -96,6 +97,32 @@ module Sidekiq
96
97
 
97
98
  end
98
99
 
100
+ def self.flush_stats
101
+ fails = Processor::FAILURE.reset
102
+ procd = Processor::PROCESSED.reset
103
+ return if fails + procd == 0
104
+
105
+ nowdate = Time.now.utc.strftime("%Y-%m-%d")
106
+ begin
107
+ Sidekiq.redis do |conn|
108
+ conn.pipelined do
109
+ conn.incrby("stat:processed", procd)
110
+ conn.incrby("stat:processed:#{nowdate}", procd)
111
+ conn.expire("stat:processed:#{nowdate}", STATS_TTL)
112
+
113
+ conn.incrby("stat:failed", fails)
114
+ conn.incrby("stat:failed:#{nowdate}", fails)
115
+ conn.expire("stat:failed:#{nowdate}", STATS_TTL)
116
+ end
117
+ end
118
+ rescue => ex
119
+ # we're exiting the process, things might be shut down so don't
120
+ # try to handle the exception
121
+ Sidekiq.logger.warn("Unable to flush stats: #{ex}")
122
+ end
123
+ end
124
+ at_exit(&method(:flush_stats))
125
+
99
126
  def ❤
100
127
  key = identity
101
128
  fails = procd = 0
@@ -118,7 +145,7 @@ module Sidekiq
118
145
  conn.incrby("stat:failed:#{nowdate}", fails)
119
146
  conn.expire("stat:failed:#{nowdate}", STATS_TTL)
120
147
 
121
- conn.del(workers_key)
148
+ conn.unlink(workers_key)
122
149
  curstate.each_pair do |tid, hash|
123
150
  conn.hset(workers_key, tid, Sidekiq.dump_json(hash))
124
151
  end
@@ -131,7 +158,7 @@ module Sidekiq
131
158
  _, exists, _, _, msg = Sidekiq.redis { |conn|
132
159
  conn.multi {
133
160
  conn.sadd("processes", key)
134
- conn.exists(key)
161
+ conn.exists?(key)
135
162
  conn.hmset(key, "info", to_json, "busy", curstate.size, "beat", Time.now.to_f, "quiet", @done)
136
163
  conn.expire(key, 60)
137
164
  conn.rpop("#{key}-signals")
@@ -146,7 +173,7 @@ module Sidekiq
146
173
  ::Process.kill(msg, ::Process.pid)
147
174
  rescue => e
148
175
  # ignore all redis/network issues
149
- logger.error("heartbeat: #{e.message}")
176
+ logger.error("heartbeat: #{e}")
150
177
  # don't lose the counts if there was a network issue
151
178
  Processor::PROCESSED.incr(procd)
152
179
  Processor::FAILURE.incr(fails)
@@ -163,7 +190,7 @@ module Sidekiq
163
190
  "concurrency" => @options[:concurrency],
164
191
  "queues" => @options[:queues].uniq,
165
192
  "labels" => @options[:labels],
166
- "identity" => identity,
193
+ "identity" => identity
167
194
  }
168
195
  end
169
196
  end
@@ -23,7 +23,7 @@ module Sidekiq
23
23
  "info" => 1,
24
24
  "warn" => 2,
25
25
  "error" => 3,
26
- "fatal" => 4,
26
+ "fatal" => 4
27
27
  }
28
28
  LEVELS.default_proc = proc do |_, level|
29
29
  Sidekiq.logger.warn("Invalid log level: #{level.inspect}")
@@ -31,23 +31,23 @@ module Sidekiq
31
31
  end
32
32
 
33
33
  def debug?
34
- level >= 0
34
+ level <= 0
35
35
  end
36
36
 
37
37
  def info?
38
- level >= 1
38
+ level <= 1
39
39
  end
40
40
 
41
41
  def warn?
42
- level >= 2
42
+ level <= 2
43
43
  end
44
44
 
45
45
  def error?
46
- level >= 3
46
+ level <= 3
47
47
  end
48
48
 
49
49
  def fatal?
50
- level >= 4
50
+ level <= 4
51
51
  end
52
52
 
53
53
  def local_level
@@ -83,7 +83,7 @@ module Sidekiq
83
83
  # Redefined to check severity against #level, and thus the thread-local level, rather than +@level+.
84
84
  # FIXME: Remove when the minimum Ruby version supports overriding Logger#level.
85
85
  def add(severity, message = nil, progname = nil, &block)
86
- severity ||= UNKNOWN
86
+ severity ||= ::Logger::UNKNOWN
87
87
  progname ||= @progname
88
88
 
89
89
  return true if @logdev.nil? || severity < level
@@ -104,7 +104,7 @@ module Sidekiq
104
104
  class Logger < ::Logger
105
105
  include LoggingUtils
106
106
 
107
- def initialize(*args)
107
+ def initialize(*args, **kwargs)
108
108
  super
109
109
  self.formatter = Sidekiq.log_formatter
110
110
  end
@@ -152,7 +152,7 @@ module Sidekiq
152
152
  pid: ::Process.pid,
153
153
  tid: tid,
154
154
  lvl: severity,
155
- msg: message,
155
+ msg: message
156
156
  }
157
157
  c = ctx
158
158
  hash["ctx"] = c unless c.empty?
@@ -35,7 +35,7 @@ module Sidekiq
35
35
  @done = false
36
36
  @workers = Set.new
37
37
  @count.times do
38
- @workers << Processor.new(self)
38
+ @workers << Processor.new(self, options)
39
39
  end
40
40
  @plock = Mutex.new
41
41
  end
@@ -90,7 +90,7 @@ module Sidekiq
90
90
  @plock.synchronize do
91
91
  @workers.delete(processor)
92
92
  unless @done
93
- p = Processor.new(self)
93
+ p = Processor.new(self, options)
94
94
  @workers << p
95
95
  p.start
96
96
  end
@@ -123,7 +123,7 @@ module Sidekiq
123
123
  # contract says that jobs are run AT LEAST once. Process termination
124
124
  # is delayed until we're certain the jobs are back in Redis because
125
125
  # it is worse to lose a job than to run it twice.
126
- strategy = (@options[:fetch] || Sidekiq::BasicFetch)
126
+ strategy = @options[:fetch]
127
127
  strategy.bulk_requeue(jobs, @options)
128
128
  end
129
129
 
@@ -62,7 +62,7 @@ class Sidekiq::Monitor
62
62
  columns = {
63
63
  name: [:ljust, (["name"] + queue_data.map(&:name)).map(&:length).max + COL_PAD],
64
64
  size: [:rjust, (["size"] + queue_data.map(&:size)).map(&:length).max + COL_PAD],
65
- latency: [:rjust, (["latency"] + queue_data.map(&:latency)).map(&:length).max + COL_PAD],
65
+ latency: [:rjust, (["latency"] + queue_data.map(&:latency)).map(&:length).max + COL_PAD]
66
66
  }
67
67
  columns.each { |col, (dir, width)| print col.to_s.upcase.public_send(dir, width) }
68
68
  puts
@@ -101,7 +101,7 @@ class Sidekiq::Monitor
101
101
  tags = [
102
102
  process["tag"],
103
103
  process["labels"],
104
- (process["quiet"] == "true" ? "quiet" : nil),
104
+ (process["quiet"] == "true" ? "quiet" : nil)
105
105
  ].flatten.compact
106
106
  tags.any? ? "[#{tags.join("] [")}]" : nil
107
107
  end
@@ -28,15 +28,15 @@ module Sidekiq
28
28
  attr_reader :thread
29
29
  attr_reader :job
30
30
 
31
- def initialize(mgr)
31
+ def initialize(mgr, options)
32
32
  @mgr = mgr
33
33
  @down = false
34
34
  @done = false
35
35
  @job = nil
36
36
  @thread = nil
37
- @strategy = (mgr.options[:fetch] || Sidekiq::BasicFetch).new(mgr.options)
38
- @reloader = Sidekiq.options[:reloader]
39
- @job_logger = (mgr.options[:job_logger] || Sidekiq::JobLogger).new
37
+ @strategy = options[:fetch]
38
+ @reloader = options[:reloader] || proc { |&block| block.call }
39
+ @job_logger = (options[:job_logger] || Sidekiq::JobLogger).new
40
40
  @retrier = Sidekiq::JobRetry.new
41
41
  end
42
42
 
@@ -181,7 +181,7 @@ module Sidekiq
181
181
  # the retry subsystem (e.g. network partition). We won't acknowledge the job
182
182
  # so it can be rescued when using Sidekiq Pro.
183
183
  handle_exception(ex, {context: "Internal exception!", job: job_hash, jobstr: jobstr})
184
- raise e
184
+ raise ex
185
185
  ensure
186
186
  if ack
187
187
  # We don't want a shutdown signal to interrupt job acknowledgment.
@@ -4,6 +4,22 @@ require "sidekiq/worker"
4
4
 
5
5
  module Sidekiq
6
6
  class Rails < ::Rails::Engine
7
+ class Reloader
8
+ def initialize(app = ::Rails.application)
9
+ @app = app
10
+ end
11
+
12
+ def call
13
+ @app.reloader.wrap do
14
+ yield
15
+ end
16
+ end
17
+
18
+ def inspect
19
+ "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
20
+ end
21
+ end
22
+
7
23
  # By including the Options module, we allow AJs to directly control sidekiq features
8
24
  # via the *sidekiq_options* class method and, for instance, not use AJ's retry system.
9
25
  # AJ retries don't show up in the Sidekiq UI Retries tab, save any error data, can't be
@@ -23,8 +39,6 @@ module Sidekiq
23
39
 
24
40
  # This hook happens after all initializers are run, just before returning
25
41
  # from config/environment.rb back to sidekiq/cli.rb.
26
- # We have to add the reloader after initialize to see if cache_classes has
27
- # been turned on.
28
42
  #
29
43
  # None of this matters on the client-side, only within the Sidekiq process itself.
30
44
  config.after_initialize do
@@ -32,21 +46,5 @@ module Sidekiq
32
46
  Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
33
47
  end
34
48
  end
35
-
36
- class Reloader
37
- def initialize(app = ::Rails.application)
38
- @app = app
39
- end
40
-
41
- def call
42
- @app.reloader.wrap do
43
- yield
44
- end
45
- end
46
-
47
- def inspect
48
- "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
49
- end
50
- end
51
49
  end
52
50
  end
@@ -8,15 +8,14 @@ module Sidekiq
8
8
  class RedisConnection
9
9
  class << self
10
10
  def create(options = {})
11
- options.keys.each do |key|
12
- options[key.to_sym] = options.delete(key)
13
- end
11
+ symbolized_options = options.transform_keys(&:to_sym)
14
12
 
15
- options[:id] = "Sidekiq-#{Sidekiq.server? ? "server" : "client"}-PID-#{::Process.pid}" unless options.key?(:id)
16
- options[:url] ||= determine_redis_provider
13
+ if !symbolized_options[:url] && (u = determine_redis_provider)
14
+ symbolized_options[:url] = u
15
+ end
17
16
 
18
- size = if options[:size]
19
- options[:size]
17
+ size = if symbolized_options[:size]
18
+ symbolized_options[:size]
20
19
  elsif Sidekiq.server?
21
20
  # Give ourselves plenty of connections. pool is lazy
22
21
  # so we won't create them until we need them.
@@ -29,11 +28,11 @@ module Sidekiq
29
28
 
30
29
  verify_sizing(size, Sidekiq.options[:concurrency]) if Sidekiq.server?
31
30
 
32
- pool_timeout = options[:pool_timeout] || 1
33
- log_info(options)
31
+ pool_timeout = symbolized_options[:pool_timeout] || 1
32
+ log_info(symbolized_options)
34
33
 
35
34
  ConnectionPool.new(timeout: pool_timeout, size: size) do
36
- build_client(options)
35
+ build_client(symbolized_options)
37
36
  end
38
37
  end
39
38
 
@@ -93,9 +92,15 @@ module Sidekiq
93
92
  end
94
93
 
95
94
  def log_info(options)
96
- # Don't log Redis AUTH password
97
95
  redacted = "REDACTED"
98
- scrubbed_options = options.dup
96
+
97
+ # deep clone so we can muck with these options all we want
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)))
99
104
  if scrubbed_options[:url] && (uri = URI.parse(scrubbed_options[:url])) && uri.password
100
105
  uri.password = redacted
101
106
  scrubbed_options[:url] = uri.to_s
@@ -103,6 +108,9 @@ module Sidekiq
103
108
  if scrubbed_options[:password]
104
109
  scrubbed_options[:password] = redacted
105
110
  end
111
+ scrubbed_options[:sentinels]&.each do |sentinel|
112
+ sentinel[:password] = redacted if sentinel[:password]
113
+ end
106
114
  if Sidekiq.server?
107
115
  Sidekiq.logger.info("Booting Sidekiq #{Sidekiq::VERSION} with redis options #{scrubbed_options}")
108
116
  else
@@ -119,7 +127,7 @@ module Sidekiq
119
127
  # initialization code at all.
120
128
  #
121
129
  p = ENV["REDIS_PROVIDER"]
122
- if p && p =~ /\:/
130
+ if p && p =~ /:/
123
131
  raise <<~EOM
124
132
  REDIS_PROVIDER should be set to the name of the variable which contains the Redis URL, not a URL itself.
125
133
  Platforms like Heroku will sell addons that publish a *_URL variable. You need to tell Sidekiq with REDIS_PROVIDER, e.g.:
@@ -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