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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +20 -0
- data/Gemfile.lock +6 -6
- data/README.md +2 -2
- data/bin/integrations +2 -1
- data/bin/rspecs +6 -2
- data/config/locales/errors.yml +33 -8
- data/config/locales/pro_errors.yml +6 -0
- data/docker-compose.yml +1 -1
- data/lib/karafka/app.rb +14 -0
- data/lib/karafka/cli/base.rb +19 -0
- data/lib/karafka/cli/server.rb +62 -76
- data/lib/karafka/cli/swarm.rb +30 -0
- data/lib/karafka/connection/client.rb +7 -0
- data/lib/karafka/constraints.rb +3 -3
- data/lib/karafka/contracts/config.rb +41 -0
- data/lib/karafka/errors.rb +12 -0
- data/lib/karafka/helpers/config_importer.rb +30 -0
- data/lib/karafka/instrumentation/logger_listener.rb +31 -0
- data/lib/karafka/instrumentation/notifications.rb +9 -0
- data/lib/karafka/instrumentation/vendors/datadog/logger_listener.rb +2 -0
- data/lib/karafka/instrumentation/vendors/datadog/metrics_listener.rb +34 -4
- data/lib/karafka/instrumentation/vendors/kubernetes/base_listener.rb +72 -0
- data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +11 -40
- data/lib/karafka/instrumentation/vendors/kubernetes/swarm_liveness_listener.rb +54 -0
- data/lib/karafka/pro/active_job/job_options_contract.rb +1 -1
- data/lib/karafka/pro/base_consumer.rb +16 -0
- data/lib/karafka/pro/connection/manager.rb +6 -1
- data/lib/karafka/pro/processing/coordinator.rb +13 -3
- data/lib/karafka/pro/processing/coordinators/errors_tracker.rb +74 -0
- data/lib/karafka/pro/processing/coordinators/filters_applier.rb +107 -0
- data/lib/karafka/pro/processing/coordinators/virtual_offset_manager.rb +180 -0
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom.rb +5 -7
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom_vp.rb +5 -7
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom.rb +8 -10
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom_vp.rb +8 -16
- data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom.rb +5 -7
- data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom_vp.rb +5 -7
- data/lib/karafka/pro/processing/strategies/aj/dlq_mom.rb +8 -10
- data/lib/karafka/pro/processing/strategies/aj/dlq_mom_vp.rb +7 -9
- data/lib/karafka/pro/processing/strategies/dlq/default.rb +36 -10
- data/lib/karafka/pro/processing/strategies/dlq/ftr.rb +3 -7
- data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj.rb +4 -8
- data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj_mom.rb +6 -9
- data/lib/karafka/pro/processing/strategies/dlq/ftr_mom.rb +5 -15
- data/lib/karafka/pro/processing/strategies/dlq/lrj.rb +4 -8
- data/lib/karafka/pro/processing/strategies/dlq/lrj_mom.rb +6 -9
- data/lib/karafka/pro/processing/strategies/dlq/mom.rb +10 -20
- data/lib/karafka/pro/processing/strategies/vp/default.rb +7 -0
- data/lib/karafka/pro/routing/features/dead_letter_queue/contracts/topic.rb +6 -0
- data/lib/karafka/pro/routing/features/dead_letter_queue/topic.rb +39 -0
- data/lib/karafka/pro/routing/features/swarm/config.rb +31 -0
- data/lib/karafka/pro/routing/features/swarm/contracts/topic.rb +67 -0
- data/lib/karafka/pro/routing/features/swarm/topic.rb +54 -0
- data/lib/karafka/pro/routing/features/swarm.rb +25 -0
- data/lib/karafka/pro/swarm/liveness_listener.rb +171 -0
- data/lib/karafka/process.rb +27 -1
- data/lib/karafka/routing/features/dead_letter_queue/config.rb +2 -0
- data/lib/karafka/routing/subscription_group.rb +44 -9
- data/lib/karafka/server.rb +11 -13
- data/lib/karafka/setup/config.rb +41 -2
- data/lib/karafka/status.rb +4 -2
- data/lib/karafka/swarm/liveness_listener.rb +55 -0
- data/lib/karafka/swarm/manager.rb +229 -0
- data/lib/karafka/swarm/node.rb +179 -0
- data/lib/karafka/swarm/pidfd.rb +147 -0
- data/lib/karafka/swarm/supervisor.rb +187 -0
- data/lib/karafka/swarm.rb +27 -0
- data/lib/karafka/version.rb +1 -1
- data/lib/karafka.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +21 -4
- metadata.gz.sig +0 -0
- data/lib/karafka/pro/processing/filters_applier.rb +0 -105
- data/lib/karafka/pro/processing/virtual_offset_manager.rb +0 -177
data/lib/karafka/status.rb
CHANGED
|
@@ -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
|
|
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
|