karafka 2.3.1 → 2.3.3

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 (77) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.rspec +2 -0
  4. data/CHANGELOG.md +20 -0
  5. data/Gemfile.lock +6 -6
  6. data/README.md +2 -2
  7. data/bin/integrations +2 -1
  8. data/bin/rspecs +6 -2
  9. data/config/locales/errors.yml +33 -8
  10. data/config/locales/pro_errors.yml +6 -0
  11. data/docker-compose.yml +1 -1
  12. data/lib/karafka/app.rb +14 -0
  13. data/lib/karafka/cli/base.rb +19 -0
  14. data/lib/karafka/cli/server.rb +62 -76
  15. data/lib/karafka/cli/swarm.rb +30 -0
  16. data/lib/karafka/connection/client.rb +7 -0
  17. data/lib/karafka/constraints.rb +3 -3
  18. data/lib/karafka/contracts/config.rb +41 -0
  19. data/lib/karafka/errors.rb +12 -0
  20. data/lib/karafka/helpers/config_importer.rb +30 -0
  21. data/lib/karafka/instrumentation/logger_listener.rb +31 -0
  22. data/lib/karafka/instrumentation/notifications.rb +9 -0
  23. data/lib/karafka/instrumentation/vendors/datadog/logger_listener.rb +2 -0
  24. data/lib/karafka/instrumentation/vendors/datadog/metrics_listener.rb +34 -4
  25. data/lib/karafka/instrumentation/vendors/kubernetes/base_listener.rb +72 -0
  26. data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +11 -40
  27. data/lib/karafka/instrumentation/vendors/kubernetes/swarm_liveness_listener.rb +54 -0
  28. data/lib/karafka/pro/active_job/job_options_contract.rb +1 -1
  29. data/lib/karafka/pro/base_consumer.rb +16 -0
  30. data/lib/karafka/pro/connection/manager.rb +6 -1
  31. data/lib/karafka/pro/processing/coordinator.rb +13 -3
  32. data/lib/karafka/pro/processing/coordinators/errors_tracker.rb +74 -0
  33. data/lib/karafka/pro/processing/coordinators/filters_applier.rb +107 -0
  34. data/lib/karafka/pro/processing/coordinators/virtual_offset_manager.rb +180 -0
  35. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom.rb +5 -7
  36. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom_vp.rb +5 -7
  37. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom.rb +8 -10
  38. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom_vp.rb +8 -16
  39. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom.rb +5 -7
  40. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom_vp.rb +5 -7
  41. data/lib/karafka/pro/processing/strategies/aj/dlq_mom.rb +8 -10
  42. data/lib/karafka/pro/processing/strategies/aj/dlq_mom_vp.rb +7 -9
  43. data/lib/karafka/pro/processing/strategies/dlq/default.rb +36 -10
  44. data/lib/karafka/pro/processing/strategies/dlq/ftr.rb +3 -7
  45. data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj.rb +4 -8
  46. data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj_mom.rb +6 -9
  47. data/lib/karafka/pro/processing/strategies/dlq/ftr_mom.rb +5 -15
  48. data/lib/karafka/pro/processing/strategies/dlq/lrj.rb +4 -8
  49. data/lib/karafka/pro/processing/strategies/dlq/lrj_mom.rb +6 -9
  50. data/lib/karafka/pro/processing/strategies/dlq/mom.rb +10 -20
  51. data/lib/karafka/pro/processing/strategies/vp/default.rb +7 -0
  52. data/lib/karafka/pro/routing/features/dead_letter_queue/contracts/topic.rb +6 -0
  53. data/lib/karafka/pro/routing/features/dead_letter_queue/topic.rb +39 -0
  54. data/lib/karafka/pro/routing/features/swarm/config.rb +31 -0
  55. data/lib/karafka/pro/routing/features/swarm/contracts/topic.rb +67 -0
  56. data/lib/karafka/pro/routing/features/swarm/topic.rb +54 -0
  57. data/lib/karafka/pro/routing/features/swarm.rb +25 -0
  58. data/lib/karafka/pro/swarm/liveness_listener.rb +171 -0
  59. data/lib/karafka/process.rb +27 -1
  60. data/lib/karafka/routing/features/dead_letter_queue/config.rb +2 -0
  61. data/lib/karafka/routing/subscription_group.rb +44 -9
  62. data/lib/karafka/server.rb +11 -13
  63. data/lib/karafka/setup/config.rb +41 -2
  64. data/lib/karafka/status.rb +4 -2
  65. data/lib/karafka/swarm/liveness_listener.rb +55 -0
  66. data/lib/karafka/swarm/manager.rb +229 -0
  67. data/lib/karafka/swarm/node.rb +179 -0
  68. data/lib/karafka/swarm/pidfd.rb +147 -0
  69. data/lib/karafka/swarm/supervisor.rb +187 -0
  70. data/lib/karafka/swarm.rb +27 -0
  71. data/lib/karafka/version.rb +1 -1
  72. data/lib/karafka.rb +1 -1
  73. data.tar.gz.sig +0 -0
  74. metadata +21 -4
  75. metadata.gz.sig +0 -0
  76. data/lib/karafka/pro/processing/filters_applier.rb +0 -105
  77. data/lib/karafka/pro/processing/virtual_offset_manager.rb +0 -177
@@ -7,6 +7,7 @@ module Karafka
7
7
  STATES = {
8
8
  initializing: :initialize!,
9
9
  initialized: :initialized!,
10
+ supervising: :supervise!,
10
11
  running: :run!,
11
12
  # will no longer pickup any work, but current work will be finished
12
13
  quieting: :quiet!,
@@ -49,8 +50,8 @@ module Karafka
49
50
 
50
51
  def #{transition}
51
52
  MUTEX.synchronize do
52
- # Do not allow reverse state transitions (we always go one way) or transition to the same
53
- # state as currently
53
+ # Do not allow reverse state transitions (we always go one way) or transition to the
54
+ # same state as currently
54
55
  return if @status && STATES.keys.index(:#{state}) <= STATES.keys.index(@status)
55
56
 
56
57
  @status = :#{state}
@@ -78,6 +79,7 @@ module Karafka
78
79
  def done?
79
80
  # Short-track for the most common case not to invoke all others on normal execution
80
81
  return false if running?
82
+ return false if supervising?
81
83
 
82
84
  stopping? || stopped? || quieting? || quiet? || terminated?
83
85
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Swarm
5
+ # Simple listener for swarm nodes that:
6
+ # - reports once in a while to make sure that supervisor is aware we do not hang
7
+ # - makes sure we did not become an orphan and if so, exits
8
+ class LivenessListener
9
+ include Karafka::Core::Helpers::Time
10
+ include Helpers::ConfigImporter.new(
11
+ node: %i[swarm node],
12
+ liveness_interval: %i[internal swarm liveness_interval],
13
+ orphaned_exit_code: %i[internal swarm orphaned_exit_code]
14
+ )
15
+
16
+ def initialize
17
+ @last_checked_at = 0
18
+ @mutex = Mutex.new
19
+ end
20
+
21
+ # Since there may be many statistics emitted from multiple listeners, we do not want to write
22
+ # statuses that often. Instead we do it only once in a while which should be enough
23
+ #
24
+ # While this may provide a small lag in the orphaned detection, it does not really matter
25
+ # as it will be picked up fast enough.
26
+ # @param _event [Karafka::Core::Monitoring::Event]
27
+ def on_statistics_emitted(_event)
28
+ periodically do
29
+ Kernel.exit!(orphaned_exit_code) if node.orphaned?
30
+
31
+ node.healthy
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ # Wraps the logic with a mutex
38
+ # @param block [Proc] code we want to run in mutex
39
+ def synchronize(&block)
40
+ @mutex.synchronize(&block)
41
+ end
42
+
43
+ # Runs requested code once in a while
44
+ def periodically
45
+ return if monotonic_now - @last_checked_at < liveness_interval
46
+
47
+ synchronize do
48
+ @last_checked_at = monotonic_now
49
+
50
+ yield
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Swarm
5
+ # Manager similar to the one for threads but managing processing nodes
6
+ # It starts nodes and keeps an eye on them.
7
+ #
8
+ # In any of the nodes is misbehaving (based on liveness listener) it will be restarted.
9
+ # Initially gracefully but if won't stop itself, it will be forced to.
10
+ #
11
+ # @note This is intended to run in the supervisor under mutexes (when needed)
12
+ class Manager
13
+ include Karafka::Core::Helpers::Time
14
+ include Helpers::ConfigImporter.new(
15
+ monitor: %i[monitor],
16
+ nodes_count: %i[swarm nodes],
17
+ shutdown_timeout: %i[shutdown_timeout],
18
+ node_report_timeout: %i[internal swarm node_report_timeout],
19
+ node_restart_timeout: %i[internal swarm node_restart_timeout]
20
+ )
21
+
22
+ # Status we issue when we decide to shutdown unresponsive node
23
+ # We use -1 because nodes are expected to report 0+ statuses and we can use negative numbers
24
+ # for non-node based statuses
25
+ NOT_RESPONDING_SHUTDOWN_STATUS = -1
26
+
27
+ private_constant :NOT_RESPONDING_SHUTDOWN_STATUS
28
+
29
+ # @return [Array<Node>] All nodes that manager manages
30
+ attr_reader :nodes
31
+
32
+ def initialize
33
+ @nodes = []
34
+ @statuses = Hash.new { |h, k| h[k] = {} }
35
+ end
36
+
37
+ # Starts all the expected nodes for the first time
38
+ def start
39
+ parent_pid = ::Process.pid
40
+
41
+ @nodes = Array.new(nodes_count) do |i|
42
+ start_one Node.new(i, parent_pid)
43
+ end
44
+ end
45
+
46
+ # Attempts to quiet all the nodes
47
+ def quiet
48
+ @nodes.each(&:quiet)
49
+ end
50
+
51
+ # Attempts to stop all the nodes
52
+ def stop
53
+ @nodes.each(&:stop)
54
+ end
55
+
56
+ # Terminates all the nodes
57
+ def terminate
58
+ @nodes.each(&:terminate)
59
+ end
60
+
61
+ # Collects all processes statuses
62
+ def cleanup
63
+ @nodes.each(&:cleanup)
64
+ end
65
+
66
+ # Sends given signal to all nodes
67
+ # @param signal [String] signal name
68
+ def signal(signal)
69
+ @nodes.each { |node| node.signal(signal) }
70
+ end
71
+
72
+ # @return [Boolean] true if none of the nodes is running
73
+ def stopped?
74
+ @nodes.none?(&:alive?)
75
+ end
76
+
77
+ # Checks on nodes if they are ok one after another
78
+ def control
79
+ monitor.instrument('swarm.manager.control', caller: self) do
80
+ @nodes.each do |node|
81
+ statuses = @statuses[node]
82
+
83
+ if node.alive?
84
+ next if terminate_if_hanging(statuses, node)
85
+ next if stop_if_not_healthy(statuses, node)
86
+ next if stop_if_not_responding(statuses, node)
87
+ else
88
+ next if cleanup_one(statuses, node)
89
+ next if restart_after_timeout(statuses, node)
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ # If we've issued a stop to this process and it does not want to stop in the period, kills it
98
+ #
99
+ # @param statuses [Hash] hash with statuses transitions with times
100
+ # @param [Swarm::Node] node we're checking
101
+ # @return [Boolean] should it be the last action taken on this node in this run
102
+ def terminate_if_hanging(statuses, node)
103
+ return false unless statuses.key?(:stop)
104
+ # If we already sent the termination request, we should not do it again
105
+ return true if statuses.key?(:terminate)
106
+ # Do not run any other checks on this node if it is during stopping but still has time
107
+ return true unless over?(statuses[:stop], shutdown_timeout)
108
+
109
+ monitor.instrument('swarm.manager.terminating', caller: self, node: node) do
110
+ node.terminate
111
+ statuses[:terminate] = monotonic_now
112
+ end
113
+
114
+ true
115
+ end
116
+
117
+ # Checks if there is any new liveness report from given node and if yes, issues stop if it
118
+ # reported it is not healthy.
119
+ #
120
+ # @param statuses [Hash] hash with statuses transitions with times
121
+ # @param [Swarm::Node] node we're checking
122
+ # @return [Boolean] should it be the last action taken on this node in this run
123
+ def stop_if_not_healthy(statuses, node)
124
+ status = node.status
125
+
126
+ case status
127
+ # If no new state reported, we should just move with other checks
128
+ when -1
129
+ false
130
+ when 0
131
+ # Exists and reports as healthy, so no other checks should happen on it in this go
132
+ statuses[:control] = monotonic_now
133
+ true
134
+ else
135
+ # A single invalid report will cause it to stop. We do not support intermediate failures
136
+ # that would recover. Such states should be implemented in the listener.
137
+ monitor.instrument('swarm.manager.stopping', caller: self, node: node, status: status) do
138
+ node.stop
139
+ statuses[:stop] = monotonic_now
140
+ end
141
+
142
+ true
143
+ end
144
+ end
145
+
146
+ # If node stopped responding, starts the stopping procedure.
147
+ #
148
+ # @param statuses [Hash] hash with statuses transitions with times
149
+ # @param [Swarm::Node] node we're checking
150
+ # @return [Boolean] should it be the last action taken on this node in this run
151
+ def stop_if_not_responding(statuses, node)
152
+ # Do nothing if already stopping
153
+ return true if statuses.key?(:stop)
154
+ # Do nothing if we've received status update recently enough
155
+ return true unless over?(statuses[:control], node_report_timeout)
156
+
157
+ # Start the stopping procedure if the node stopped reporting frequently enough
158
+ monitor.instrument(
159
+ 'swarm.manager.stopping',
160
+ caller: self,
161
+ node: node,
162
+ status: NOT_RESPONDING_SHUTDOWN_STATUS
163
+ ) do
164
+ node.stop
165
+ statuses[:stop] = monotonic_now
166
+ end
167
+
168
+ true
169
+ end
170
+
171
+ # Cleans up a dead process and remembers time of death for restart after a period.
172
+ #
173
+ # @param statuses [Hash] hash with statuses transitions with times
174
+ # @param [Swarm::Node] node we're checking
175
+ # @return [Boolean] should it be the last action taken on this node in this run
176
+ def cleanup_one(statuses, node)
177
+ return false if statuses.key?(:dead_since)
178
+
179
+ node.cleanup
180
+ statuses[:dead_since] = monotonic_now
181
+
182
+ true
183
+ end
184
+
185
+ # Restarts the node if there was enough of a backoff.
186
+ #
187
+ # We always wait a bit to make sure, we do not overload the system in case forks would be
188
+ # killed for some external reason.
189
+ #
190
+ # @param statuses [Hash] hash with statuses transitions with times
191
+ # @param [Swarm::Node] node we're checking
192
+ # @return [Boolean] should it be the last action taken on this node in this run
193
+ def restart_after_timeout(statuses, node)
194
+ return false unless over?(statuses[:dead_since], node_restart_timeout)
195
+
196
+ start_one(node)
197
+
198
+ true
199
+ end
200
+
201
+ # Starts a new node (or restarts dead)
202
+ #
203
+ # @param [Swarm::Node] node we're starting
204
+ def start_one(node)
205
+ instr_args = { caller: self, node: node }
206
+
207
+ statuses = @statuses[node]
208
+
209
+ statuses.clear
210
+ statuses[:control] = monotonic_now
211
+
212
+ monitor.instrument('swarm.manager.before_fork', instr_args)
213
+ node.start
214
+ monitor.instrument('swarm.manager.after_fork', instr_args)
215
+
216
+ node
217
+ end
218
+
219
+ # Are we over certain time from an event happening
220
+ #
221
+ # @param event_time [Float] when something happened
222
+ # @param delay [Float] how long should we wait
223
+ # @return [Boolean] true if we're past the delay
224
+ def over?(event_time, delay)
225
+ monotonic_now - event_time >= delay
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Swarm
5
+ # Represents a single forked process node in a swarm
6
+ # Provides simple API to control forks and check their status
7
+ #
8
+ # @note Some of this APIs are for parent process only
9
+ #
10
+ # @note Keep in mind this can be used in both forks and supervisor and has a slightly different
11
+ # role in each. In case of the supervisor it is used to get information about the child and
12
+ # make certain requests to it. In case of child, it is used to provide zombie-fencing and
13
+ # report liveness
14
+ class Node
15
+ include Helpers::ConfigImporter.new(
16
+ monitor: %i[monitor],
17
+ config: %i[itself],
18
+ kafka: %i[kafka],
19
+ swarm: %i[swarm],
20
+ process: %i[process],
21
+ liveness_listener: %i[internal swarm liveness_listener]
22
+ )
23
+
24
+ # @return [Integer] id of the node. Useful for client.group.id assignment
25
+ attr_reader :id
26
+
27
+ # @return [Integer] pid of the node
28
+ attr_reader :pid
29
+
30
+ # @param id [Integer] number of the fork. Used for uniqueness setup for group client ids and
31
+ # other stuff where we need to know a unique reference of the fork in regards to the rest
32
+ # of them.
33
+ # @param parent_pid [Integer] parent pid for zombie fencing
34
+ def initialize(id, parent_pid)
35
+ @id = id
36
+ @parent_pidfd = Pidfd.new(parent_pid)
37
+ end
38
+
39
+ # Starts a new fork and:
40
+ # - stores pid and parent reference
41
+ # - makes sure reader pipe is closed
42
+ # - sets up liveness listener
43
+ # - recreates producer and web producer
44
+ # @note Parent API
45
+ def start
46
+ @reader, @writer = IO.pipe
47
+
48
+ # :nocov:
49
+ @pid = ::Process.fork do
50
+ # Close the old producer so it is not a subject to GC
51
+ # While it was not opened in the parent, without explicit closing, there still could be
52
+ # an attempt to close it when finalized, meaning it would be kept in memory.
53
+ config.producer.close
54
+
55
+ # Supervisor producer is closed, hence we need a new one here
56
+ config.producer = ::WaterDrop::Producer.new do |p_config|
57
+ p_config.kafka = Setup::AttributesMap.producer(kafka.dup)
58
+ p_config.logger = config.logger
59
+ end
60
+
61
+ @pid = ::Process.pid
62
+ @reader.close
63
+
64
+ # Indicate we are alive right after start
65
+ healthy
66
+
67
+ swarm.node = self
68
+ monitor.subscribe(liveness_listener)
69
+ monitor.instrument('swarm.node.after_fork', caller: self)
70
+
71
+ Server.run
72
+
73
+ @writer.close
74
+ end
75
+ # :nocov:
76
+
77
+ @writer.close
78
+ @pidfd = Pidfd.new(@pid)
79
+ end
80
+
81
+ # Indicates that this node is doing well
82
+ # @note Child API
83
+ def healthy
84
+ write('0')
85
+ end
86
+
87
+ # Indicates, that this node has failed
88
+ # @param reason_code [Integer, String] numeric code we want to use to indicate that we are
89
+ # not healthy. Anything bigger than 0 will be considered not healthy. Useful it we want to
90
+ # have complex health-checking with reporting.
91
+ # @note Child API
92
+ # @note We convert this to string to normalize the API
93
+ def unhealthy(reason_code = '1')
94
+ write(reason_code.to_s)
95
+ end
96
+
97
+ # @return [Integer] This returns following status code depending on the data:
98
+ # - -1 if node did not report anything new
99
+ # - 0 if all good,
100
+ # - positive number if there was a problem (indicates error code)
101
+ #
102
+ # @note Parent API
103
+ # @note If there were few issues reported, it will pick the one with highest number
104
+ def status
105
+ result = read
106
+
107
+ return -1 if result.nil?
108
+ return -1 if result == false
109
+
110
+ result.split("\n").map(&:to_i).max
111
+ end
112
+
113
+ # @return [Boolean] true if node is alive or false if died
114
+ # @note Parent API
115
+ # @note Keep in mind that the fact that process is alive does not mean it is healthy
116
+ def alive?
117
+ @pidfd.alive?
118
+ end
119
+
120
+ # @return [Boolean] true if node is orphaned or false otherwise. Used for orphans detection.
121
+ # @note Child API
122
+ def orphaned?
123
+ !@parent_pidfd.alive?
124
+ end
125
+
126
+ # Sends sigterm to the node
127
+ # @note Parent API
128
+ def stop
129
+ signal('TERM')
130
+ end
131
+
132
+ # Sends sigtstp to the node
133
+ # @note Parent API
134
+ def quiet
135
+ signal('TSTP')
136
+ end
137
+
138
+ # Terminates node
139
+ # @note Parent API
140
+ def terminate
141
+ signal('KILL')
142
+ end
143
+
144
+ # Sends provided signal to the node
145
+ # @param signal [String]
146
+ def signal(signal)
147
+ @pidfd.signal(signal)
148
+ end
149
+
150
+ # Removes the dead process from the processes table
151
+ def cleanup
152
+ @pidfd.cleanup
153
+ end
154
+
155
+ private
156
+
157
+ # Reads in a non-blocking way provided content
158
+ # @return [String, false] Content from the pipe or false if nothing or something went wrong
159
+ # @note Parent API
160
+ def read
161
+ @reader.read_nonblock(1024)
162
+ rescue IO::WaitReadable, Errno::EPIPE, IOError
163
+ false
164
+ end
165
+
166
+ # Writes in a non-blocking way provided content into the pipe
167
+ # @param content [Integer, String] anything we want to write to the parent
168
+ # @return [Boolean] true if ok, otherwise false
169
+ # @note Child API
170
+ def write(content)
171
+ @writer.write_nonblock "#{content}\n"
172
+
173
+ true
174
+ rescue IO::WaitWritable, Errno::EPIPE, IOError
175
+ false
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Swarm
5
+ # Pidfd Linux representation wrapped with Ruby for communication within Swarm
6
+ # It is more stable than using `#pid` and `#ppid` + signals and cheaper
7
+ class Pidfd
8
+ include Helpers::ConfigImporter.new(
9
+ pidfd_open_syscall: %i[internal swarm pidfd_open_syscall],
10
+ pidfd_signal_syscall: %i[internal swarm pidfd_signal_syscall],
11
+ waitid_syscall: %i[internal swarm waitid_syscall]
12
+ )
13
+
14
+ extend FFI::Library
15
+
16
+ begin
17
+ ffi_lib FFI::Library::LIBC
18
+
19
+ # direct usage of this is only available since glibc 2.36, hence we use bindings and call
20
+ # it directly via syscalls
21
+ attach_function :fdpid_open, :syscall, %i[long int uint], :int
22
+ attach_function :fdpid_signal, :syscall, %i[long int int pointer uint], :int
23
+ attach_function :waitid, %i[int int pointer uint], :int
24
+
25
+ API_SUPPORTED = true
26
+ # LoadError is a parent to FFI::NotFoundError
27
+ rescue LoadError
28
+ API_SUPPORTED = false
29
+ ensure
30
+ private_constant :API_SUPPORTED
31
+ end
32
+
33
+ # https://github.com/torvalds/linux/blob/7e90b5c295/include/uapi/linux/wait.h#L20
34
+ P_PIDFD = 3
35
+
36
+ # Wait for child processes that have exited
37
+ WEXITED = 4
38
+
39
+ private_constant :P_PIDFD, :WEXITED
40
+
41
+ class << self
42
+ # @return [Boolean] true if syscall is supported via FFI
43
+ def supported?
44
+ # If we were not even able to load the FFI C lib, it won't be supported
45
+ return false unless API_SUPPORTED
46
+ # Won't work on macOS because it does not support pidfd
47
+ return false if RUBY_DESCRIPTION.include?('darwin')
48
+ # Won't work on Windows for the same reason as on macOS
49
+ return false if RUBY_DESCRIPTION.match?(/mswin|ming|cygwin/)
50
+
51
+ # There are some OSes like BSD that will have C lib for FFI bindings but will not support
52
+ # the needed syscalls. In such cases, we can just try and fail, which will indicate it
53
+ # won't work. The same applies to using new glibc on an old kernel.
54
+ new(::Process.pid)
55
+
56
+ true
57
+ rescue Errors::PidfdOpenFailedError
58
+ false
59
+ end
60
+ end
61
+
62
+ # @param pid [Integer] pid of the node we want to work with
63
+ def initialize(pid)
64
+ @mutex = Mutex.new
65
+
66
+ @pid = pid
67
+ @pidfd = open(pid)
68
+ @pidfd_io = IO.new(@pidfd)
69
+ end
70
+
71
+ # @return [Boolean] true if given process is alive, false if no longer
72
+ def alive?
73
+ @pidfd_select ||= [@pidfd_io]
74
+
75
+ if @mutex.owned?
76
+ return false if @cleaned
77
+
78
+ IO.select(@pidfd_select, nil, nil, 0).nil?
79
+ else
80
+ @mutex.synchronize do
81
+ return false if @cleaned
82
+
83
+ IO.select(@pidfd_select, nil, nil, 0).nil?
84
+ end
85
+ end
86
+ end
87
+
88
+ # Cleans the zombie process
89
+ # @note This should run **only** on processes that exited, otherwise will wait
90
+ def cleanup
91
+ @mutex.synchronize do
92
+ return if @cleaned
93
+
94
+ waitid(P_PIDFD, @pidfd, nil, WEXITED)
95
+
96
+ @pidfd_io.close
97
+ @pidfd_select = nil
98
+ @pidfd_io = nil
99
+ @pidfd = nil
100
+ @cleaned = true
101
+ end
102
+ end
103
+
104
+ # Sends given signal to the process using its pidfd
105
+ # @param sig_name [String] signal name
106
+ # @return [Boolean] true if signal was sent, otherwise false or error raised. `false`
107
+ # returned when we attempt to send a signal to a dead process
108
+ # @note It will not send signals to dead processes
109
+ def signal(sig_name)
110
+ @mutex.synchronize do
111
+ return false if @cleaned
112
+ # Never signal processes that are dead
113
+ return false unless alive?
114
+
115
+ result = fdpid_signal(
116
+ pidfd_signal_syscall,
117
+ @pidfd,
118
+ Signal.list.fetch(sig_name),
119
+ nil,
120
+ 0
121
+ )
122
+
123
+ return true if result.zero?
124
+
125
+ raise Errors::PidfdSignalFailedError, result
126
+ end
127
+ end
128
+
129
+ private
130
+
131
+ # Opens a pidfd for the provided pid
132
+ # @param pid [Integer]
133
+ # @return [Integer] pidfd
134
+ def open(pid)
135
+ pidfd = fdpid_open(
136
+ pidfd_open_syscall,
137
+ pid,
138
+ 0
139
+ )
140
+
141
+ return pidfd if pidfd != -1
142
+
143
+ raise Errors::PidfdOpenFailedError, pidfd
144
+ end
145
+ end
146
+ end
147
+ end