karafka 2.5.0.rc2 → 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.
Files changed (121) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/{ci.yml → ci_linux_ubuntu_x86_64_gnu.yml} +54 -30
  3. data/.github/workflows/ci_macos_arm64.yml +148 -0
  4. data/.github/workflows/push.yml +2 -2
  5. data/.github/workflows/trigger-wiki-refresh.yml +30 -0
  6. data/.github/workflows/verify-action-pins.yml +1 -1
  7. data/.ruby-version +1 -1
  8. data/CHANGELOG.md +29 -2
  9. data/Gemfile +2 -1
  10. data/Gemfile.lock +56 -27
  11. data/README.md +2 -2
  12. data/bin/integrations +3 -1
  13. data/bin/verify_kafka_warnings +2 -1
  14. data/config/locales/errors.yml +153 -152
  15. data/config/locales/pro_errors.yml +135 -134
  16. data/karafka.gemspec +3 -3
  17. data/lib/active_job/queue_adapters/karafka_adapter.rb +30 -1
  18. data/lib/karafka/active_job/dispatcher.rb +19 -9
  19. data/lib/karafka/admin/acl.rb +7 -8
  20. data/lib/karafka/admin/configs/config.rb +2 -2
  21. data/lib/karafka/admin/configs/resource.rb +2 -2
  22. data/lib/karafka/admin/configs.rb +3 -7
  23. data/lib/karafka/admin/consumer_groups.rb +351 -0
  24. data/lib/karafka/admin/topics.rb +206 -0
  25. data/lib/karafka/admin.rb +42 -451
  26. data/lib/karafka/base_consumer.rb +22 -0
  27. data/lib/karafka/{pro/contracts/server_cli_options.rb → cli/contracts/server.rb} +4 -12
  28. data/lib/karafka/cli/info.rb +1 -1
  29. data/lib/karafka/cli/install.rb +0 -2
  30. data/lib/karafka/connection/client.rb +8 -0
  31. data/lib/karafka/connection/listener.rb +5 -1
  32. data/lib/karafka/connection/status.rb +12 -9
  33. data/lib/karafka/errors.rb +0 -8
  34. data/lib/karafka/instrumentation/assignments_tracker.rb +16 -0
  35. data/lib/karafka/instrumentation/logger_listener.rb +109 -50
  36. data/lib/karafka/pro/active_job/dispatcher.rb +5 -0
  37. data/lib/karafka/pro/cleaner/messages/messages.rb +18 -8
  38. data/lib/karafka/pro/cli/contracts/server.rb +106 -0
  39. data/lib/karafka/pro/encryption/contracts/config.rb +1 -1
  40. data/lib/karafka/pro/loader.rb +1 -1
  41. data/lib/karafka/pro/recurring_tasks/contracts/config.rb +1 -1
  42. data/lib/karafka/pro/routing/features/adaptive_iterator/contracts/topic.rb +1 -1
  43. data/lib/karafka/pro/routing/features/adaptive_iterator/topic.rb +9 -0
  44. data/lib/karafka/pro/routing/features/dead_letter_queue/contracts/topic.rb +1 -1
  45. data/lib/karafka/pro/routing/features/dead_letter_queue/topic.rb +9 -0
  46. data/lib/karafka/pro/routing/features/delaying/contracts/topic.rb +1 -1
  47. data/lib/karafka/pro/routing/features/delaying/topic.rb +9 -0
  48. data/lib/karafka/pro/routing/features/direct_assignments/contracts/consumer_group.rb +1 -1
  49. data/lib/karafka/pro/routing/features/direct_assignments/contracts/topic.rb +1 -1
  50. data/lib/karafka/pro/routing/features/direct_assignments/topic.rb +9 -0
  51. data/lib/karafka/pro/routing/features/expiring/contracts/topic.rb +1 -1
  52. data/lib/karafka/pro/routing/features/expiring/topic.rb +9 -0
  53. data/lib/karafka/pro/routing/features/filtering/contracts/topic.rb +1 -1
  54. data/lib/karafka/pro/routing/features/filtering/topic.rb +9 -0
  55. data/lib/karafka/pro/routing/features/inline_insights/contracts/topic.rb +1 -1
  56. data/lib/karafka/pro/routing/features/inline_insights/topic.rb +9 -0
  57. data/lib/karafka/pro/routing/features/long_running_job/contracts/topic.rb +1 -1
  58. data/lib/karafka/pro/routing/features/long_running_job/topic.rb +9 -0
  59. data/lib/karafka/pro/routing/features/multiplexing/contracts/topic.rb +1 -1
  60. data/lib/karafka/pro/routing/features/multiplexing.rb +1 -1
  61. data/lib/karafka/pro/routing/features/offset_metadata/contracts/topic.rb +1 -1
  62. data/lib/karafka/pro/routing/features/offset_metadata/topic.rb +9 -0
  63. data/lib/karafka/pro/routing/features/parallel_segments/contracts/consumer_group.rb +1 -1
  64. data/lib/karafka/pro/routing/features/patterns/contracts/consumer_group.rb +1 -1
  65. data/lib/karafka/pro/routing/features/patterns/contracts/topic.rb +1 -1
  66. data/lib/karafka/pro/routing/features/patterns/topic.rb +9 -0
  67. data/lib/karafka/pro/routing/features/pausing/contracts/topic.rb +1 -1
  68. data/lib/karafka/pro/routing/features/periodic_job/contracts/topic.rb +1 -1
  69. data/lib/karafka/pro/routing/features/periodic_job/topic.rb +9 -0
  70. data/lib/karafka/pro/routing/features/recurring_tasks/contracts/topic.rb +1 -1
  71. data/lib/karafka/pro/routing/features/recurring_tasks/topic.rb +9 -0
  72. data/lib/karafka/pro/routing/features/scheduled_messages/contracts/topic.rb +1 -1
  73. data/lib/karafka/pro/routing/features/scheduled_messages/topic.rb +9 -0
  74. data/lib/karafka/pro/routing/features/swarm/contracts/topic.rb +1 -1
  75. data/lib/karafka/pro/routing/features/swarm/topic.rb +9 -0
  76. data/lib/karafka/pro/routing/features/throttling/contracts/topic.rb +1 -1
  77. data/lib/karafka/pro/routing/features/throttling/topic.rb +9 -0
  78. data/lib/karafka/pro/routing/features/virtual_partitions/contracts/topic.rb +1 -1
  79. data/lib/karafka/pro/routing/features/virtual_partitions/topic.rb +9 -0
  80. data/lib/karafka/pro/scheduled_messages/contracts/config.rb +1 -1
  81. data/lib/karafka/pro/scheduled_messages/daily_buffer.rb +9 -3
  82. data/lib/karafka/pro/swarm/liveness_listener.rb +17 -2
  83. data/lib/karafka/processing/executor.rb +1 -1
  84. data/lib/karafka/routing/builder.rb +0 -3
  85. data/lib/karafka/routing/consumer_group.rb +1 -4
  86. data/lib/karafka/routing/contracts/consumer_group.rb +84 -0
  87. data/lib/karafka/routing/contracts/routing.rb +61 -0
  88. data/lib/karafka/routing/contracts/topic.rb +83 -0
  89. data/lib/karafka/routing/features/active_job/contracts/topic.rb +1 -1
  90. data/lib/karafka/routing/features/active_job/topic.rb +9 -0
  91. data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +1 -1
  92. data/lib/karafka/routing/features/dead_letter_queue/topic.rb +9 -0
  93. data/lib/karafka/routing/features/declaratives/contracts/topic.rb +1 -1
  94. data/lib/karafka/routing/features/declaratives/topic.rb +9 -0
  95. data/lib/karafka/routing/features/deserializers/contracts/topic.rb +1 -1
  96. data/lib/karafka/routing/features/deserializers/topic.rb +9 -0
  97. data/lib/karafka/routing/features/eofed/contracts/topic.rb +1 -1
  98. data/lib/karafka/routing/features/eofed/topic.rb +9 -0
  99. data/lib/karafka/routing/features/inline_insights/contracts/topic.rb +1 -1
  100. data/lib/karafka/routing/features/inline_insights/topic.rb +9 -0
  101. data/lib/karafka/routing/features/manual_offset_management/contracts/topic.rb +1 -1
  102. data/lib/karafka/routing/features/manual_offset_management/topic.rb +9 -0
  103. data/lib/karafka/routing/subscription_group.rb +1 -10
  104. data/lib/karafka/routing/topic.rb +9 -1
  105. data/lib/karafka/server.rb +2 -7
  106. data/lib/karafka/setup/attributes_map.rb +36 -0
  107. data/lib/karafka/setup/config.rb +6 -7
  108. data/lib/karafka/setup/contracts/config.rb +217 -0
  109. data/lib/karafka/setup/defaults_injector.rb +3 -1
  110. data/lib/karafka/swarm/node.rb +66 -6
  111. data/lib/karafka/swarm.rb +2 -2
  112. data/lib/karafka/templates/karafka.rb.erb +2 -7
  113. data/lib/karafka/version.rb +1 -1
  114. data/lib/karafka.rb +17 -18
  115. metadata +18 -15
  116. data/lib/karafka/contracts/config.rb +0 -210
  117. data/lib/karafka/contracts/consumer_group.rb +0 -81
  118. data/lib/karafka/contracts/routing.rb +0 -59
  119. data/lib/karafka/contracts/server_cli_options.rb +0 -92
  120. data/lib/karafka/contracts/topic.rb +0 -81
  121. 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
@@ -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