karafka 2.5.0 → 2.5.1.beta1
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
- data/.github/workflows/{ci.yml → ci_linux_ubuntu_x86_64_gnu.yml} +54 -30
- data/.github/workflows/ci_macos_arm64.yml +148 -0
- data/.github/workflows/push.yml +2 -2
- data/.github/workflows/trigger-wiki-refresh.yml +30 -0
- data/.github/workflows/verify-action-pins.yml +1 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +28 -1
- data/Gemfile +2 -1
- data/Gemfile.lock +55 -26
- data/README.md +2 -2
- data/bin/integrations +3 -1
- data/bin/verify_kafka_warnings +2 -1
- data/config/locales/errors.yml +153 -152
- data/config/locales/pro_errors.yml +135 -134
- data/karafka.gemspec +3 -3
- data/lib/active_job/queue_adapters/karafka_adapter.rb +30 -1
- data/lib/karafka/active_job/dispatcher.rb +19 -9
- data/lib/karafka/admin/acl.rb +7 -8
- data/lib/karafka/admin/configs/config.rb +2 -2
- data/lib/karafka/admin/configs/resource.rb +2 -2
- data/lib/karafka/admin/configs.rb +3 -7
- data/lib/karafka/admin/consumer_groups.rb +351 -0
- data/lib/karafka/admin/topics.rb +206 -0
- data/lib/karafka/admin.rb +42 -451
- data/lib/karafka/base_consumer.rb +22 -0
- data/lib/karafka/{pro/contracts/server_cli_options.rb → cli/contracts/server.rb} +4 -12
- data/lib/karafka/cli/info.rb +1 -1
- data/lib/karafka/cli/install.rb +0 -2
- data/lib/karafka/connection/client.rb +8 -0
- data/lib/karafka/connection/listener.rb +5 -1
- data/lib/karafka/connection/status.rb +12 -9
- data/lib/karafka/errors.rb +0 -8
- data/lib/karafka/instrumentation/assignments_tracker.rb +16 -0
- data/lib/karafka/instrumentation/logger_listener.rb +109 -50
- data/lib/karafka/pro/active_job/dispatcher.rb +5 -0
- data/lib/karafka/pro/cleaner/messages/messages.rb +18 -8
- data/lib/karafka/pro/cli/contracts/server.rb +106 -0
- data/lib/karafka/pro/encryption/contracts/config.rb +1 -1
- data/lib/karafka/pro/loader.rb +1 -1
- data/lib/karafka/pro/recurring_tasks/contracts/config.rb +1 -1
- data/lib/karafka/pro/routing/features/adaptive_iterator/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/adaptive_iterator/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/dead_letter_queue/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/dead_letter_queue/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/delaying/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/delaying/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/direct_assignments/contracts/consumer_group.rb +1 -1
- data/lib/karafka/pro/routing/features/direct_assignments/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/direct_assignments/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/expiring/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/expiring/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/filtering/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/filtering/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/inline_insights/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/inline_insights/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/long_running_job/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/long_running_job/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/multiplexing/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/multiplexing.rb +1 -1
- data/lib/karafka/pro/routing/features/offset_metadata/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/offset_metadata/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/parallel_segments/contracts/consumer_group.rb +1 -1
- data/lib/karafka/pro/routing/features/patterns/contracts/consumer_group.rb +1 -1
- data/lib/karafka/pro/routing/features/patterns/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/patterns/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/pausing/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/periodic_job/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/periodic_job/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/recurring_tasks/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/recurring_tasks/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/scheduled_messages/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/scheduled_messages/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/swarm/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/swarm/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/throttling/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/throttling/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/virtual_partitions/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/virtual_partitions/topic.rb +9 -0
- data/lib/karafka/pro/scheduled_messages/contracts/config.rb +1 -1
- data/lib/karafka/pro/scheduled_messages/daily_buffer.rb +9 -3
- data/lib/karafka/pro/swarm/liveness_listener.rb +17 -2
- data/lib/karafka/processing/executor.rb +1 -1
- data/lib/karafka/routing/builder.rb +0 -3
- data/lib/karafka/routing/consumer_group.rb +1 -4
- data/lib/karafka/routing/contracts/consumer_group.rb +84 -0
- data/lib/karafka/routing/contracts/routing.rb +61 -0
- data/lib/karafka/routing/contracts/topic.rb +83 -0
- data/lib/karafka/routing/features/active_job/contracts/topic.rb +1 -1
- data/lib/karafka/routing/features/active_job/topic.rb +9 -0
- data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +1 -1
- data/lib/karafka/routing/features/dead_letter_queue/topic.rb +9 -0
- data/lib/karafka/routing/features/declaratives/contracts/topic.rb +1 -1
- data/lib/karafka/routing/features/declaratives/topic.rb +9 -0
- data/lib/karafka/routing/features/deserializers/contracts/topic.rb +1 -1
- data/lib/karafka/routing/features/deserializers/topic.rb +9 -0
- data/lib/karafka/routing/features/eofed/contracts/topic.rb +1 -1
- data/lib/karafka/routing/features/eofed/topic.rb +9 -0
- data/lib/karafka/routing/features/inline_insights/contracts/topic.rb +1 -1
- data/lib/karafka/routing/features/inline_insights/topic.rb +9 -0
- data/lib/karafka/routing/features/manual_offset_management/contracts/topic.rb +1 -1
- data/lib/karafka/routing/features/manual_offset_management/topic.rb +9 -0
- data/lib/karafka/routing/subscription_group.rb +1 -10
- data/lib/karafka/routing/topic.rb +9 -1
- data/lib/karafka/server.rb +2 -7
- data/lib/karafka/setup/attributes_map.rb +36 -0
- data/lib/karafka/setup/config.rb +6 -7
- data/lib/karafka/setup/contracts/config.rb +217 -0
- data/lib/karafka/setup/defaults_injector.rb +3 -1
- data/lib/karafka/swarm/node.rb +66 -6
- data/lib/karafka/swarm.rb +2 -2
- data/lib/karafka/templates/karafka.rb.erb +2 -7
- data/lib/karafka/version.rb +1 -1
- data/lib/karafka.rb +17 -18
- metadata +18 -15
- data/lib/karafka/contracts/config.rb +0 -210
- data/lib/karafka/contracts/consumer_group.rb +0 -81
- data/lib/karafka/contracts/routing.rb +0 -59
- data/lib/karafka/contracts/server_cli_options.rb +0 -92
- data/lib/karafka/contracts/topic.rb +0 -81
- data/lib/karafka/swarm/pidfd.rb +0 -147
@@ -1,210 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Karafka
|
4
|
-
module Contracts
|
5
|
-
# Contract with validation rules for Karafka configuration details.
|
6
|
-
#
|
7
|
-
# @note There are many more configuration options inside of the
|
8
|
-
# `Karafka::Setup::Config` model, but we don't validate them here as they are
|
9
|
-
# validated per each route (topic + consumer_group) because they can be overwritten,
|
10
|
-
# so we validate all of that once all the routes are defined and ready.
|
11
|
-
class Config < Base
|
12
|
-
configure do |config|
|
13
|
-
config.error_messages = YAML.safe_load(
|
14
|
-
File.read(
|
15
|
-
File.join(Karafka.gem_root, 'config', 'locales', 'errors.yml')
|
16
|
-
)
|
17
|
-
).fetch('en').fetch('validations').fetch('config')
|
18
|
-
end
|
19
|
-
|
20
|
-
# License validity happens in the licenser. Here we do only the simple consistency checks
|
21
|
-
nested(:license) do
|
22
|
-
required(:token) { |val| [true, false].include?(val) || val.is_a?(String) }
|
23
|
-
required(:entity) { |val| val.is_a?(String) }
|
24
|
-
end
|
25
|
-
|
26
|
-
required(:client_id) { |val| val.is_a?(String) && Contracts::TOPIC_REGEXP.match?(val) }
|
27
|
-
required(:concurrency) { |val| val.is_a?(Integer) && val.positive? }
|
28
|
-
required(:consumer_persistence) { |val| [true, false].include?(val) }
|
29
|
-
required(:pause_timeout) { |val| val.is_a?(Integer) && val.positive? }
|
30
|
-
required(:pause_max_timeout) { |val| val.is_a?(Integer) && val.positive? }
|
31
|
-
required(:pause_with_exponential_backoff) { |val| [true, false].include?(val) }
|
32
|
-
required(:strict_topics_namespacing) { |val| [true, false].include?(val) }
|
33
|
-
required(:shutdown_timeout) { |val| val.is_a?(Integer) && val.positive? }
|
34
|
-
required(:max_wait_time) { |val| val.is_a?(Integer) && val.positive? }
|
35
|
-
required(:group_id) { |val| val.is_a?(String) && Contracts::TOPIC_REGEXP.match?(val) }
|
36
|
-
required(:kafka) { |val| val.is_a?(Hash) && !val.empty? }
|
37
|
-
required(:strict_declarative_topics) { |val| [true, false].include?(val) }
|
38
|
-
required(:worker_thread_priority) { |val| (-3..3).to_a.include?(val) }
|
39
|
-
|
40
|
-
nested(:swarm) do
|
41
|
-
required(:nodes) { |val| val.is_a?(Integer) && val.positive? }
|
42
|
-
required(:node) { |val| val == false || val.is_a?(Karafka::Swarm::Node) }
|
43
|
-
end
|
44
|
-
|
45
|
-
nested(:oauth) do
|
46
|
-
required(:token_provider_listener) do |val|
|
47
|
-
val == false || val.respond_to?(:on_oauthbearer_token_refresh)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
nested(:admin) do
|
52
|
-
# Can be empty because inherits values from the root kafka
|
53
|
-
required(:kafka) { |val| val.is_a?(Hash) }
|
54
|
-
required(:group_id) { |val| val.is_a?(String) && Contracts::TOPIC_REGEXP.match?(val) }
|
55
|
-
required(:max_wait_time) { |val| val.is_a?(Integer) && val.positive? }
|
56
|
-
required(:retry_backoff) { |val| val.is_a?(Integer) && val >= 100 }
|
57
|
-
required(:max_retries_duration) { |val| val.is_a?(Integer) && val >= 1_000 }
|
58
|
-
end
|
59
|
-
|
60
|
-
# We validate internals just to be sure, that they are present and working
|
61
|
-
nested(:internal) do
|
62
|
-
required(:status) { |val| !val.nil? }
|
63
|
-
required(:process) { |val| !val.nil? }
|
64
|
-
# In theory this could be less than a second, however this would impact the maximum time
|
65
|
-
# of a single consumer queue poll, hence we prevent it
|
66
|
-
required(:tick_interval) { |val| val.is_a?(Integer) && val >= 1_000 }
|
67
|
-
required(:supervision_sleep) { |val| val.is_a?(Numeric) && val.positive? }
|
68
|
-
required(:forceful_exit_code) { |val| val.is_a?(Integer) && val >= 0 }
|
69
|
-
|
70
|
-
nested(:swarm) do
|
71
|
-
required(:manager) { |val| !val.nil? }
|
72
|
-
required(:orphaned_exit_code) { |val| val.is_a?(Integer) && val >= 0 }
|
73
|
-
required(:pidfd_open_syscall) { |val| val.is_a?(Integer) && val >= 0 }
|
74
|
-
required(:pidfd_signal_syscall) { |val| val.is_a?(Integer) && val >= 0 }
|
75
|
-
required(:supervision_interval) { |val| val.is_a?(Integer) && val >= 1_000 }
|
76
|
-
required(:liveness_interval) { |val| val.is_a?(Integer) && val >= 1_000 }
|
77
|
-
required(:liveness_listener) { |val| !val.nil? }
|
78
|
-
required(:node_report_timeout) { |val| val.is_a?(Integer) && val >= 1_000 }
|
79
|
-
required(:node_restart_timeout) { |val| val.is_a?(Integer) && val >= 1_000 }
|
80
|
-
end
|
81
|
-
|
82
|
-
nested(:connection) do
|
83
|
-
required(:manager) { |val| !val.nil? }
|
84
|
-
required(:conductor) { |val| !val.nil? }
|
85
|
-
required(:reset_backoff) { |val| val.is_a?(Integer) && val >= 1_000 }
|
86
|
-
required(:listener_thread_priority) { |val| (-3..3).to_a.include?(val) }
|
87
|
-
|
88
|
-
nested(:proxy) do
|
89
|
-
nested(:commit) do
|
90
|
-
required(:max_attempts) { |val| val.is_a?(Integer) && val.positive? }
|
91
|
-
required(:wait_time) { |val| val.is_a?(Integer) && val.positive? }
|
92
|
-
end
|
93
|
-
|
94
|
-
# All of them have the same requirements
|
95
|
-
%i[
|
96
|
-
query_watermark_offsets
|
97
|
-
offsets_for_times
|
98
|
-
committed
|
99
|
-
metadata
|
100
|
-
].each do |scope|
|
101
|
-
nested(scope) do
|
102
|
-
required(:timeout) { |val| val.is_a?(Integer) && val.positive? }
|
103
|
-
required(:max_attempts) { |val| val.is_a?(Integer) && val.positive? }
|
104
|
-
required(:wait_time) { |val| val.is_a?(Integer) && val.positive? }
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
nested(:routing) do
|
111
|
-
required(:builder) { |val| !val.nil? }
|
112
|
-
required(:subscription_groups_builder) { |val| !val.nil? }
|
113
|
-
end
|
114
|
-
|
115
|
-
nested(:processing) do
|
116
|
-
required(:jobs_builder) { |val| !val.nil? }
|
117
|
-
required(:jobs_queue_class) { |val| !val.nil? }
|
118
|
-
required(:scheduler_class) { |val| !val.nil? }
|
119
|
-
required(:coordinator_class) { |val| !val.nil? }
|
120
|
-
required(:errors_tracker_class) { |val| val.nil? || val.is_a?(Class) }
|
121
|
-
required(:partitioner_class) { |val| !val.nil? }
|
122
|
-
required(:strategy_selector) { |val| !val.nil? }
|
123
|
-
required(:expansions_selector) { |val| !val.nil? }
|
124
|
-
required(:executor_class) { |val| !val.nil? }
|
125
|
-
required(:worker_job_call_wrapper) { |val| val == false || val.respond_to?(:wrap) }
|
126
|
-
end
|
127
|
-
|
128
|
-
nested(:active_job) do
|
129
|
-
required(:dispatcher) { |val| !val.nil? }
|
130
|
-
required(:job_options_contract) { |val| !val.nil? }
|
131
|
-
required(:consumer_class) { |val| !val.nil? }
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
# Ensure all root kafka keys are symbols
|
136
|
-
virtual do |data, errors|
|
137
|
-
next unless errors.empty?
|
138
|
-
|
139
|
-
detected_errors = []
|
140
|
-
|
141
|
-
data.fetch(:kafka).each_key do |key|
|
142
|
-
next if key.is_a?(Symbol)
|
143
|
-
|
144
|
-
detected_errors << [[:kafka, key], :key_must_be_a_symbol]
|
145
|
-
end
|
146
|
-
|
147
|
-
detected_errors
|
148
|
-
end
|
149
|
-
|
150
|
-
# Ensure all admin kafka keys are symbols
|
151
|
-
virtual do |data, errors|
|
152
|
-
next unless errors.empty?
|
153
|
-
|
154
|
-
detected_errors = []
|
155
|
-
|
156
|
-
data.fetch(:admin).fetch(:kafka).each_key do |key|
|
157
|
-
next if key.is_a?(Symbol)
|
158
|
-
|
159
|
-
detected_errors << [[:admin, :kafka, key], :key_must_be_a_symbol]
|
160
|
-
end
|
161
|
-
|
162
|
-
detected_errors
|
163
|
-
end
|
164
|
-
|
165
|
-
virtual do |data, errors|
|
166
|
-
next unless errors.empty?
|
167
|
-
|
168
|
-
pause_timeout = data.fetch(:pause_timeout)
|
169
|
-
pause_max_timeout = data.fetch(:pause_max_timeout)
|
170
|
-
|
171
|
-
next if pause_timeout <= pause_max_timeout
|
172
|
-
|
173
|
-
[[%i[pause_timeout], :max_timeout_vs_pause_max_timeout]]
|
174
|
-
end
|
175
|
-
|
176
|
-
virtual do |data, errors|
|
177
|
-
next unless errors.empty?
|
178
|
-
|
179
|
-
shutdown_timeout = data.fetch(:shutdown_timeout)
|
180
|
-
max_wait_time = data.fetch(:max_wait_time)
|
181
|
-
|
182
|
-
next if max_wait_time < shutdown_timeout
|
183
|
-
|
184
|
-
[[%i[shutdown_timeout], :shutdown_timeout_vs_max_wait_time]]
|
185
|
-
end
|
186
|
-
|
187
|
-
# `internal.swarm.node_report_timeout` should not be close to `max_wait_time` otherwise
|
188
|
-
# there may be a case where node cannot report often enough because it is clogged by waiting
|
189
|
-
# on more data.
|
190
|
-
#
|
191
|
-
# We handle that at a config level to make sure that this is correctly configured.
|
192
|
-
#
|
193
|
-
# We do not validate this in the context of swarm usage (validate only if...) because it is
|
194
|
-
# often that swarm only runs on prod and we do not want to crash it surprisingly.
|
195
|
-
virtual do |data, errors|
|
196
|
-
next unless errors.empty?
|
197
|
-
|
198
|
-
max_wait_time = data.fetch(:max_wait_time)
|
199
|
-
node_report_timeout = data.fetch(:internal)[:swarm][:node_report_timeout] || false
|
200
|
-
|
201
|
-
next unless node_report_timeout
|
202
|
-
# max wait time should be at least 20% smaller than the reporting time to have enough
|
203
|
-
# time for reporting
|
204
|
-
next if max_wait_time < node_report_timeout * 0.8
|
205
|
-
|
206
|
-
[[%i[max_wait_time], :max_wait_time_vs_swarm_node_report_timeout]]
|
207
|
-
end
|
208
|
-
end
|
209
|
-
end
|
210
|
-
end
|
@@ -1,81 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Karafka
|
4
|
-
module Contracts
|
5
|
-
# Contract for single full route (consumer group + topics) validation.
|
6
|
-
class ConsumerGroup < Base
|
7
|
-
configure do |config|
|
8
|
-
config.error_messages = YAML.safe_load(
|
9
|
-
File.read(
|
10
|
-
File.join(Karafka.gem_root, 'config', 'locales', 'errors.yml')
|
11
|
-
)
|
12
|
-
).fetch('en').fetch('validations').fetch('consumer_group')
|
13
|
-
end
|
14
|
-
|
15
|
-
required(:id) { |val| val.is_a?(String) && Contracts::TOPIC_REGEXP.match?(val) }
|
16
|
-
required(:topics) { |val| val.is_a?(Array) && !val.empty? }
|
17
|
-
|
18
|
-
virtual do |data, errors|
|
19
|
-
next unless errors.empty?
|
20
|
-
|
21
|
-
names = data.fetch(:topics).map { |topic| topic_unique_key(topic) }
|
22
|
-
|
23
|
-
next if names.size == names.uniq.size
|
24
|
-
|
25
|
-
[[%i[topics], :names_not_unique]]
|
26
|
-
end
|
27
|
-
|
28
|
-
# Prevent same topics subscriptions in one CG with different consumer classes
|
29
|
-
# This should prevent users from accidentally creating multi-sg one CG setup with weird
|
30
|
-
# different consumer usage. If you need to consume same topic twice, use distinct CGs.
|
31
|
-
virtual do |data, errors|
|
32
|
-
next unless errors.empty?
|
33
|
-
|
34
|
-
topics_consumers = Hash.new { |h, k| h[k] = Set.new }
|
35
|
-
|
36
|
-
data.fetch(:topics).map do |topic|
|
37
|
-
topics_consumers[topic[:name]] << topic[:consumer]
|
38
|
-
end
|
39
|
-
|
40
|
-
next if topics_consumers.values.map(&:size).all? { |count| count == 1 }
|
41
|
-
|
42
|
-
[[%i[topics], :many_consumers_same_topic]]
|
43
|
-
end
|
44
|
-
|
45
|
-
virtual do |data, errors|
|
46
|
-
next unless errors.empty?
|
47
|
-
next unless ::Karafka::App.config.strict_topics_namespacing
|
48
|
-
|
49
|
-
names = data.fetch(:topics).map { |topic| topic[:name] }
|
50
|
-
names_hash = names.each_with_object({}) { |n, h| h[n] = true }
|
51
|
-
error_occured = false
|
52
|
-
names.each do |n|
|
53
|
-
# Skip topic names that are not namespaced
|
54
|
-
next unless n.chars.find { |c| ['.', '_'].include?(c) }
|
55
|
-
|
56
|
-
if n.chars.include?('.')
|
57
|
-
# Check underscore styled topic
|
58
|
-
underscored_topic = n.tr('.', '_')
|
59
|
-
error_occured = names_hash[underscored_topic] ? true : false
|
60
|
-
else
|
61
|
-
# Check dot styled topic
|
62
|
-
dot_topic = n.tr('_', '.')
|
63
|
-
error_occured = names_hash[dot_topic] ? true : false
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
next unless error_occured
|
68
|
-
|
69
|
-
[[%i[topics], :topics_namespaced_names_not_unique]]
|
70
|
-
end
|
71
|
-
|
72
|
-
class << self
|
73
|
-
# @param topic [Hash] topic config hash
|
74
|
-
# @return [String] topic unique key for validators
|
75
|
-
def topic_unique_key(topic)
|
76
|
-
topic[:name]
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
@@ -1,59 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Karafka
|
4
|
-
module Contracts
|
5
|
-
# Ensures that routing wide rules are obeyed
|
6
|
-
class Routing < Base
|
7
|
-
configure do |config|
|
8
|
-
config.error_messages = YAML.safe_load(
|
9
|
-
File.read(
|
10
|
-
File.join(Karafka.gem_root, 'config', 'locales', 'errors.yml')
|
11
|
-
)
|
12
|
-
).fetch('en').fetch('validations').fetch('routing')
|
13
|
-
end
|
14
|
-
|
15
|
-
# Ensures, that when declarative topics strict requirement is on, all topics have
|
16
|
-
# declarative definition (including DLQ topics)
|
17
|
-
# @note It will ignore routing pattern topics because those topics are virtual
|
18
|
-
virtual do |data, errors|
|
19
|
-
next unless errors.empty?
|
20
|
-
# Do not validate declaratives unless required and explicitly enabled
|
21
|
-
next unless Karafka::App.config.strict_declarative_topics
|
22
|
-
|
23
|
-
# Collects declarative topics. Please note, that any topic that has a `#topic` reference,
|
24
|
-
# will be declarative by default unless explicitly disabled. This however does not apply
|
25
|
-
# to the DLQ definitions
|
26
|
-
dec_topics = Set.new
|
27
|
-
# All topics including the DLQ topics names that are marked as active
|
28
|
-
topics = Set.new
|
29
|
-
|
30
|
-
data.each do |consumer_group|
|
31
|
-
consumer_group[:topics].each do |topic|
|
32
|
-
pat = topic[:patterns]
|
33
|
-
# Ignore pattern topics because they won't exist and should not be declarative
|
34
|
-
# managed
|
35
|
-
topics << topic[:name] if !pat || !pat[:active]
|
36
|
-
|
37
|
-
dlq = topic[:dead_letter_queue]
|
38
|
-
topics << dlq[:topic] if dlq[:active]
|
39
|
-
|
40
|
-
dec = topic[:declaratives]
|
41
|
-
|
42
|
-
dec_topics << topic[:name] if dec[:active]
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
missing_dec = topics - dec_topics
|
47
|
-
|
48
|
-
next if missing_dec.empty?
|
49
|
-
|
50
|
-
missing_dec.map do |topic_name|
|
51
|
-
[
|
52
|
-
[:topics, topic_name],
|
53
|
-
:without_declarative_definition
|
54
|
-
]
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
@@ -1,92 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Karafka
|
4
|
-
module Contracts
|
5
|
-
# Contract for validating correctness of the server cli command options.
|
6
|
-
class ServerCliOptions < Base
|
7
|
-
configure do |config|
|
8
|
-
config.error_messages = YAML.safe_load(
|
9
|
-
File.read(
|
10
|
-
File.join(Karafka.gem_root, 'config', 'locales', 'errors.yml')
|
11
|
-
)
|
12
|
-
).fetch('en').fetch('validations').fetch('server_cli_options')
|
13
|
-
end
|
14
|
-
|
15
|
-
%i[
|
16
|
-
include
|
17
|
-
exclude
|
18
|
-
].each do |action|
|
19
|
-
optional(:"#{action}_consumer_groups") { |cg| cg.is_a?(Array) }
|
20
|
-
optional(:"#{action}_subscription_groups") { |sg| sg.is_a?(Array) }
|
21
|
-
optional(:"#{action}_topics") { |topics| topics.is_a?(Array) }
|
22
|
-
|
23
|
-
virtual do |data, errors|
|
24
|
-
next unless errors.empty?
|
25
|
-
|
26
|
-
value = data.fetch(:"#{action}_consumer_groups")
|
27
|
-
|
28
|
-
# If there were no consumer_groups declared in the server cli, it means that we will
|
29
|
-
# run all of them and no need to validate them here at all
|
30
|
-
next if value.empty?
|
31
|
-
next if (value - Karafka::App.consumer_groups.map(&:name)).empty?
|
32
|
-
|
33
|
-
# Found unknown consumer groups
|
34
|
-
[[[:"#{action}_consumer_groups"], :consumer_groups_inclusion]]
|
35
|
-
end
|
36
|
-
|
37
|
-
virtual do |data, errors|
|
38
|
-
next unless errors.empty?
|
39
|
-
|
40
|
-
value = data.fetch(:"#{action}_subscription_groups")
|
41
|
-
|
42
|
-
# If there were no subscription_groups declared in the server cli, it means that we will
|
43
|
-
# run all of them and no need to validate them here at all
|
44
|
-
next if value.empty?
|
45
|
-
|
46
|
-
subscription_groups = Karafka::App
|
47
|
-
.consumer_groups
|
48
|
-
.map(&:subscription_groups)
|
49
|
-
.flatten
|
50
|
-
.map(&:name)
|
51
|
-
|
52
|
-
next if (value - subscription_groups).empty?
|
53
|
-
|
54
|
-
# Found unknown subscription groups
|
55
|
-
[[[:"#{action}_subscription_groups"], :subscription_groups_inclusion]]
|
56
|
-
end
|
57
|
-
|
58
|
-
virtual do |data, errors|
|
59
|
-
next unless errors.empty?
|
60
|
-
|
61
|
-
value = data.fetch(:"#{action}_topics")
|
62
|
-
|
63
|
-
# If there were no topics declared in the server cli, it means that we will
|
64
|
-
# run all of them and no need to validate them here at all
|
65
|
-
next if value.empty?
|
66
|
-
|
67
|
-
topics = Karafka::App
|
68
|
-
.consumer_groups
|
69
|
-
.map(&:subscription_groups)
|
70
|
-
.flatten
|
71
|
-
.map(&:topics)
|
72
|
-
.map { |gtopics| gtopics.map(&:name) }
|
73
|
-
.flatten
|
74
|
-
|
75
|
-
next if (value - topics).empty?
|
76
|
-
|
77
|
-
# Found unknown topics
|
78
|
-
[[[:"#{action}_topics"], :topics_inclusion]]
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
# Makes sure we have anything to subscribe to when we start the server
|
83
|
-
virtual do |_, errors|
|
84
|
-
next unless errors.empty?
|
85
|
-
|
86
|
-
next unless Karafka::App.subscription_groups.empty?
|
87
|
-
|
88
|
-
[[%i[include_topics], :topics_missing]]
|
89
|
-
end
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
@@ -1,81 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Karafka
|
4
|
-
module Contracts
|
5
|
-
# Consumer group topic validation rules.
|
6
|
-
class Topic < Base
|
7
|
-
configure do |config|
|
8
|
-
config.error_messages = YAML.safe_load(
|
9
|
-
File.read(
|
10
|
-
File.join(Karafka.gem_root, 'config', 'locales', 'errors.yml')
|
11
|
-
)
|
12
|
-
).fetch('en').fetch('validations').fetch('topic')
|
13
|
-
end
|
14
|
-
|
15
|
-
required(:deserializers) { |val| !val.nil? }
|
16
|
-
required(:id) { |val| val.is_a?(String) && Contracts::TOPIC_REGEXP.match?(val) }
|
17
|
-
required(:kafka) { |val| val.is_a?(Hash) && !val.empty? }
|
18
|
-
required(:max_messages) { |val| val.is_a?(Integer) && val >= 1 }
|
19
|
-
required(:initial_offset) { |val| %w[earliest latest].include?(val) }
|
20
|
-
required(:max_wait_time) { |val| val.is_a?(Integer) && val >= 10 }
|
21
|
-
required(:name) { |val| val.is_a?(String) && Contracts::TOPIC_REGEXP.match?(val) }
|
22
|
-
required(:active) { |val| [true, false].include?(val) }
|
23
|
-
nested(:subscription_group_details) do
|
24
|
-
required(:name) { |val| val.is_a?(String) && !val.empty? }
|
25
|
-
end
|
26
|
-
|
27
|
-
# Consumer needs to be present only if topic is active
|
28
|
-
# We allow not to define consumer for non-active because they may be only used via admin
|
29
|
-
# api or other ways and not consumed with consumer
|
30
|
-
virtual do |data, errors|
|
31
|
-
next unless errors.empty?
|
32
|
-
next if data.fetch(:consumer)
|
33
|
-
next unless data.fetch(:active)
|
34
|
-
|
35
|
-
[[%w[consumer], :missing]]
|
36
|
-
end
|
37
|
-
|
38
|
-
virtual do |data, errors|
|
39
|
-
next unless errors.empty?
|
40
|
-
|
41
|
-
value = data.fetch(:kafka)
|
42
|
-
|
43
|
-
begin
|
44
|
-
# This will trigger rdkafka validations that we catch and re-map the info and use dry
|
45
|
-
# compatible format
|
46
|
-
Rdkafka::Config.new(value).send(:native_config)
|
47
|
-
|
48
|
-
nil
|
49
|
-
rescue Rdkafka::Config::ConfigError => e
|
50
|
-
[[%w[kafka], e.message]]
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
# When users redefine kafka scope settings per topic, they often forget to define the
|
55
|
-
# basic stuff as they assume it is auto-inherited. It is not (unless inherit flag used),
|
56
|
-
# leaving them with things like bootstrap.servers undefined. This checks that bootstrap
|
57
|
-
# servers are defined so we can catch those issues before they cause more problems.
|
58
|
-
virtual do |data, errors|
|
59
|
-
next unless errors.empty?
|
60
|
-
|
61
|
-
kafka = data.fetch(:kafka)
|
62
|
-
|
63
|
-
next if kafka.key?(:'bootstrap.servers')
|
64
|
-
|
65
|
-
[[%w[kafka bootstrap.servers], :missing]]
|
66
|
-
end
|
67
|
-
|
68
|
-
virtual do |data, errors|
|
69
|
-
next unless errors.empty?
|
70
|
-
next unless ::Karafka::App.config.strict_topics_namespacing
|
71
|
-
|
72
|
-
value = data.fetch(:name)
|
73
|
-
namespacing_chars_count = value.chars.find_all { |c| ['.', '_'].include?(c) }.uniq.size
|
74
|
-
|
75
|
-
next if namespacing_chars_count <= 1
|
76
|
-
|
77
|
-
[[%w[name], :inconsistent_namespacing]]
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
data/lib/karafka/swarm/pidfd.rb
DELETED
@@ -1,147 +0,0 @@
|
|
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
|