karafka 2.5.5 → 2.5.7
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/CHANGELOG.md +20 -0
- data/LICENSE-COMM +4 -0
- data/README.md +2 -2
- data/certs/expired.txt +2 -0
- data/karafka.gemspec +23 -23
- data/lib/active_job/karafka.rb +2 -2
- data/lib/active_job/queue_adapters/karafka_adapter.rb +5 -5
- data/lib/karafka/active_job/consumer.rb +3 -3
- data/lib/karafka/active_job/current_attributes.rb +4 -4
- data/lib/karafka/active_job/job_options_contract.rb +2 -2
- data/lib/karafka/admin/acl.rb +3 -3
- data/lib/karafka/admin/configs/resource.rb +1 -1
- data/lib/karafka/admin/configs.rb +1 -1
- data/lib/karafka/admin/consumer_groups.rb +8 -8
- data/lib/karafka/admin/contracts/replication.rb +2 -2
- data/lib/karafka/admin/replication.rb +21 -21
- data/lib/karafka/admin/topics.rb +6 -6
- data/lib/karafka/admin.rb +4 -5
- data/lib/karafka/app.rb +3 -3
- data/lib/karafka/base_consumer.rb +34 -30
- data/lib/karafka/cli/base.rb +8 -8
- data/lib/karafka/cli/console.rb +1 -1
- data/lib/karafka/cli/contracts/server.rb +12 -12
- data/lib/karafka/cli/help.rb +2 -2
- data/lib/karafka/cli/info.rb +4 -4
- data/lib/karafka/cli/install.rb +11 -11
- data/lib/karafka/cli/server.rb +6 -6
- data/lib/karafka/cli/swarm.rb +1 -1
- data/lib/karafka/cli/topics/align.rb +4 -4
- data/lib/karafka/cli/topics/base.rb +5 -5
- data/lib/karafka/cli/topics/create.rb +2 -2
- data/lib/karafka/cli/topics/delete.rb +2 -2
- data/lib/karafka/cli/topics/help.rb +5 -1
- data/lib/karafka/cli/topics/plan.rb +16 -16
- data/lib/karafka/cli/topics/repartition.rb +3 -3
- data/lib/karafka/cli/topics.rb +22 -22
- data/lib/karafka/cli.rb +2 -2
- data/lib/karafka/connection/client.rb +17 -17
- data/lib/karafka/connection/listener.rb +6 -6
- data/lib/karafka/connection/mode.rb +1 -1
- data/lib/karafka/connection/proxy.rb +1 -1
- data/lib/karafka/connection/status.rb +2 -2
- data/lib/karafka/constraints.rb +3 -3
- data/lib/karafka/embedded.rb +3 -3
- data/lib/karafka/env.rb +4 -4
- data/lib/karafka/errors.rb +6 -1
- data/lib/karafka/execution_mode.rb +1 -1
- data/lib/karafka/helpers/config_importer.rb +2 -2
- data/lib/karafka/helpers/interval_runner.rb +4 -2
- data/lib/karafka/helpers/multi_delegator.rb +1 -1
- data/lib/karafka/instrumentation/assignments_tracker.rb +9 -9
- data/lib/karafka/instrumentation/callbacks/error.rb +5 -5
- data/lib/karafka/instrumentation/callbacks/oauthbearer_token_refresh.rb +4 -4
- data/lib/karafka/instrumentation/callbacks/rebalance.rb +6 -6
- data/lib/karafka/instrumentation/callbacks/statistics.rb +5 -5
- data/lib/karafka/instrumentation/logger.rb +7 -7
- data/lib/karafka/instrumentation/logger_listener.rb +76 -63
- data/lib/karafka/instrumentation/vendors/appsignal/base.rb +1 -1
- data/lib/karafka/instrumentation/vendors/appsignal/client.rb +1 -1
- data/lib/karafka/instrumentation/vendors/appsignal/errors_listener.rb +1 -1
- data/lib/karafka/instrumentation/vendors/appsignal/metrics_listener.rb +36 -36
- data/lib/karafka/instrumentation/vendors/datadog/logger_listener.rb +33 -28
- data/lib/karafka/instrumentation/vendors/datadog/metrics_listener.rb +38 -38
- data/lib/karafka/instrumentation/vendors/kubernetes/base_listener.rb +5 -5
- data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +1 -1
- data/lib/karafka/instrumentation/vendors/kubernetes/swarm_liveness_listener.rb +1 -1
- data/lib/karafka/licenser.rb +115 -8
- data/lib/karafka/messages/builders/batch_metadata.rb +4 -2
- data/lib/karafka/messages/messages.rb +1 -1
- data/lib/karafka/patches/rdkafka/bindings.rb +2 -2
- data/lib/karafka/pro/active_job/job_options_contract.rb +2 -2
- data/lib/karafka/pro/cleaner/messages/messages.rb +10 -0
- data/lib/karafka/pro/cli/contracts/server.rb +12 -12
- data/lib/karafka/pro/cli/parallel_segments/base.rb +4 -4
- data/lib/karafka/pro/cli/parallel_segments/collapse.rb +5 -5
- data/lib/karafka/pro/cli/parallel_segments/distribute.rb +3 -3
- data/lib/karafka/pro/cli/parallel_segments.rb +7 -7
- data/lib/karafka/pro/cli/topics/health.rb +162 -0
- data/lib/karafka/pro/cli/topics.rb +52 -0
- data/lib/karafka/pro/connection/manager.rb +14 -14
- data/lib/karafka/pro/encryption/contracts/config.rb +2 -2
- data/lib/karafka/pro/encryption/messages/middleware.rb +2 -2
- data/lib/karafka/pro/encryption/messages/parser.rb +2 -2
- data/lib/karafka/pro/encryption/setup/config.rb +2 -2
- data/lib/karafka/pro/iterator/tpl_builder.rb +2 -2
- data/lib/karafka/pro/iterator.rb +1 -1
- data/lib/karafka/pro/loader.rb +2 -1
- data/lib/karafka/pro/processing/adaptive_iterator/consumer.rb +1 -1
- data/lib/karafka/pro/processing/coordinators/virtual_offset_manager.rb +24 -14
- data/lib/karafka/pro/processing/filters/base.rb +1 -1
- data/lib/karafka/pro/processing/filters/delayer.rb +2 -2
- data/lib/karafka/pro/processing/filters/inline_insights_delayer.rb +1 -1
- data/lib/karafka/pro/processing/offset_metadata/consumer.rb +1 -1
- data/lib/karafka/pro/processing/parallel_segments/filters/base.rb +6 -6
- data/lib/karafka/pro/processing/partitioner.rb +3 -3
- data/lib/karafka/pro/processing/periodic_job/consumer.rb +6 -5
- data/lib/karafka/pro/processing/piping/consumer.rb +7 -7
- data/lib/karafka/pro/processing/schedulers/base.rb +5 -5
- data/lib/karafka/pro/processing/schedulers/default.rb +5 -5
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom.rb +6 -3
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom_vp.rb +6 -3
- data/lib/karafka/pro/processing/strategies/aj/ftr_lrj_mom_vp.rb +6 -3
- data/lib/karafka/pro/processing/strategies/aj/lrj_mom_vp.rb +2 -2
- data/lib/karafka/pro/processing/strategies/default.rb +22 -22
- data/lib/karafka/pro/processing/strategies/dlq/default.rb +7 -7
- data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj.rb +6 -3
- data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj_mom.rb +6 -3
- data/lib/karafka/pro/processing/strategies/ftr/default.rb +2 -2
- data/lib/karafka/pro/processing/strategies/lrj/default.rb +2 -2
- data/lib/karafka/pro/processing/strategies/lrj/ftr.rb +6 -3
- data/lib/karafka/pro/processing/strategies/lrj/ftr_mom.rb +6 -3
- data/lib/karafka/pro/processing/strategies/lrj/mom.rb +2 -2
- data/lib/karafka/pro/recurring_tasks/consumer.rb +2 -2
- data/lib/karafka/pro/recurring_tasks/contracts/config.rb +2 -2
- data/lib/karafka/pro/recurring_tasks/contracts/task.rb +2 -2
- data/lib/karafka/pro/recurring_tasks/dispatcher.rb +2 -2
- data/lib/karafka/pro/recurring_tasks/listener.rb +1 -1
- data/lib/karafka/pro/recurring_tasks/matcher.rb +2 -2
- data/lib/karafka/pro/recurring_tasks/serializer.rb +5 -5
- data/lib/karafka/pro/recurring_tasks/setup/config.rb +3 -3
- data/lib/karafka/pro/recurring_tasks/task.rb +4 -4
- data/lib/karafka/pro/recurring_tasks.rb +4 -4
- data/lib/karafka/pro/routing/features/adaptive_iterator/contracts/topic.rb +2 -2
- data/lib/karafka/pro/routing/features/dead_letter_queue/contracts/topic.rb +2 -2
- data/lib/karafka/pro/routing/features/dead_letter_queue/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/delaying/contracts/topic.rb +2 -2
- data/lib/karafka/pro/routing/features/direct_assignments/contracts/consumer_group.rb +2 -2
- data/lib/karafka/pro/routing/features/direct_assignments/contracts/topic.rb +2 -2
- data/lib/karafka/pro/routing/features/direct_assignments/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/expiring/contracts/topic.rb +2 -2
- data/lib/karafka/pro/routing/features/filtering/contracts/topic.rb +2 -2
- data/lib/karafka/pro/routing/features/inline_insights/contracts/topic.rb +2 -2
- data/lib/karafka/pro/routing/features/long_running_job/contracts/topic.rb +2 -2
- data/lib/karafka/pro/routing/features/long_running_job/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/multiplexing/contracts/topic.rb +2 -2
- data/lib/karafka/pro/routing/features/multiplexing.rb +5 -5
- data/lib/karafka/pro/routing/features/non_blocking_job/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/offset_metadata/contracts/topic.rb +2 -2
- data/lib/karafka/pro/routing/features/offset_metadata/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/offset_metadata.rb +1 -1
- data/lib/karafka/pro/routing/features/parallel_segments/consumer_group.rb +5 -5
- data/lib/karafka/pro/routing/features/parallel_segments/contracts/consumer_group.rb +2 -2
- data/lib/karafka/pro/routing/features/patterns/contracts/consumer_group.rb +2 -2
- data/lib/karafka/pro/routing/features/patterns/contracts/pattern.rb +3 -3
- data/lib/karafka/pro/routing/features/patterns/contracts/topic.rb +2 -2
- data/lib/karafka/pro/routing/features/patterns/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/pausing/contracts/topic.rb +2 -2
- data/lib/karafka/pro/routing/features/periodic_job/contracts/topic.rb +2 -2
- data/lib/karafka/pro/routing/features/periodic_job/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/recurring_tasks/builder.rb +7 -7
- data/lib/karafka/pro/routing/features/recurring_tasks/contracts/topic.rb +2 -2
- data/lib/karafka/pro/routing/features/scheduled_messages/builder.rb +13 -13
- data/lib/karafka/pro/routing/features/scheduled_messages/contracts/topic.rb +2 -2
- data/lib/karafka/pro/routing/features/swarm/contracts/routing.rb +2 -2
- data/lib/karafka/pro/routing/features/swarm/contracts/topic.rb +2 -2
- data/lib/karafka/pro/routing/features/swarm.rb +1 -1
- data/lib/karafka/pro/routing/features/throttling/contracts/topic.rb +2 -2
- data/lib/karafka/pro/routing/features/virtual_partitions/config.rb +7 -7
- data/lib/karafka/pro/routing/features/virtual_partitions/contracts/topic.rb +2 -2
- data/lib/karafka/pro/scheduled_messages/consumer.rb +4 -4
- data/lib/karafka/pro/scheduled_messages/contracts/config.rb +2 -2
- data/lib/karafka/pro/scheduled_messages/contracts/message.rb +10 -10
- data/lib/karafka/pro/scheduled_messages/daily_buffer.rb +2 -2
- data/lib/karafka/pro/scheduled_messages/deserializers/headers.rb +4 -4
- data/lib/karafka/pro/scheduled_messages/dispatcher.rb +5 -5
- data/lib/karafka/pro/scheduled_messages/proxy.rb +8 -8
- data/lib/karafka/pro/scheduled_messages/schema_validator.rb +1 -1
- data/lib/karafka/pro/scheduled_messages/setup/config.rb +2 -2
- data/lib/karafka/pro/scheduled_messages/state.rb +1 -1
- data/lib/karafka/pro/scheduled_messages/tracker.rb +2 -2
- data/lib/karafka/pro/scheduled_messages.rb +2 -2
- data/lib/karafka/pro/swarm/liveness_listener.rb +2 -2
- data/lib/karafka/process.rb +1 -1
- data/lib/karafka/processing/coordinator.rb +1 -1
- data/lib/karafka/processing/inline_insights/consumer.rb +4 -4
- data/lib/karafka/processing/inline_insights/tracker.rb +6 -6
- data/lib/karafka/processing/jobs/base.rb +6 -4
- data/lib/karafka/processing/jobs_queue.rb +10 -0
- data/lib/karafka/processing/schedulers/default.rb +4 -4
- data/lib/karafka/processing/strategies/base.rb +6 -6
- data/lib/karafka/processing/strategies/default.rb +13 -13
- data/lib/karafka/processing/strategies/dlq.rb +1 -1
- data/lib/karafka/processing/worker.rb +5 -5
- data/lib/karafka/railtie.rb +11 -11
- data/lib/karafka/routing/builder.rb +3 -3
- data/lib/karafka/routing/contracts/consumer_group.rb +6 -6
- data/lib/karafka/routing/contracts/routing.rb +2 -2
- data/lib/karafka/routing/contracts/topic.rb +4 -4
- data/lib/karafka/routing/features/active_job/contracts/topic.rb +3 -3
- data/lib/karafka/routing/features/base/expander.rb +4 -4
- data/lib/karafka/routing/features/base.rb +8 -8
- data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +2 -2
- data/lib/karafka/routing/features/declaratives/contracts/topic.rb +2 -2
- data/lib/karafka/routing/features/deserializers/contracts/topic.rb +2 -2
- data/lib/karafka/routing/features/eofed/contracts/topic.rb +3 -3
- data/lib/karafka/routing/features/inline_insights/contracts/topic.rb +2 -2
- data/lib/karafka/routing/features/inline_insights.rb +7 -7
- data/lib/karafka/routing/features/manual_offset_management/contracts/topic.rb +2 -2
- data/lib/karafka/routing/subscription_group.rb +9 -9
- data/lib/karafka/runner.rb +3 -3
- data/lib/karafka/server.rb +14 -5
- data/lib/karafka/setup/attributes_map.rb +7 -7
- data/lib/karafka/setup/config.rb +11 -11
- data/lib/karafka/setup/contracts/config.rb +2 -2
- data/lib/karafka/setup/defaults_injector.rb +11 -11
- data/lib/karafka/swarm/manager.rb +6 -6
- data/lib/karafka/swarm/node.rb +8 -37
- data/lib/karafka/swarm/producer_replacer.rb +110 -0
- data/lib/karafka/swarm/supervisor.rb +9 -6
- data/lib/karafka/swarm.rb +1 -1
- data/lib/karafka/time_trackers/pause.rb +1 -1
- data/lib/karafka/version.rb +1 -1
- data/lib/karafka.rb +36 -36
- metadata +7 -3
|
@@ -37,12 +37,12 @@ module Karafka
|
|
|
37
37
|
class Collapse < Base
|
|
38
38
|
# Runs the collapse operation
|
|
39
39
|
def call
|
|
40
|
-
puts
|
|
40
|
+
puts "Starting parallel segments collapse..."
|
|
41
41
|
|
|
42
42
|
segments_count = applicable_groups.size
|
|
43
43
|
|
|
44
44
|
if segments_count.zero?
|
|
45
|
-
puts "#{red(
|
|
45
|
+
puts "#{red("No")} consumer groups with parallel segments configuration found"
|
|
46
46
|
|
|
47
47
|
return
|
|
48
48
|
end
|
|
@@ -74,7 +74,7 @@ module Karafka
|
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
puts
|
|
77
|
-
puts "Collapse completed #{green(
|
|
77
|
+
puts "Collapse completed #{green("successfully")}!"
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
private
|
|
@@ -141,7 +141,7 @@ module Karafka
|
|
|
141
141
|
|
|
142
142
|
puts(
|
|
143
143
|
" Inconclusive offsets for #{red(topic_name)}##{red(partition_id)}: " \
|
|
144
|
-
"#{parallel_offsets.to_a.join(
|
|
144
|
+
"#{parallel_offsets.to_a.join(", ")}"
|
|
145
145
|
)
|
|
146
146
|
end
|
|
147
147
|
end
|
|
@@ -150,7 +150,7 @@ module Karafka
|
|
|
150
150
|
|
|
151
151
|
raise(
|
|
152
152
|
Karafka::Errors::CommandValidationError,
|
|
153
|
-
"Parallel segments for #{red(segment_origin)} have #{red(
|
|
153
|
+
"Parallel segments for #{red(segment_origin)} have #{red("inconclusive")} offsets"
|
|
154
154
|
)
|
|
155
155
|
end
|
|
156
156
|
|
|
@@ -44,12 +44,12 @@ module Karafka
|
|
|
44
44
|
class Distribute < Base
|
|
45
45
|
# Runs the distribution process
|
|
46
46
|
def call
|
|
47
|
-
puts
|
|
47
|
+
puts "Starting parallel segments distribution..."
|
|
48
48
|
|
|
49
49
|
segments_count = applicable_groups.size
|
|
50
50
|
|
|
51
51
|
if segments_count.zero?
|
|
52
|
-
puts "#{red(
|
|
52
|
+
puts "#{red("No")} consumer groups with parallel segments configuration found"
|
|
53
53
|
|
|
54
54
|
return
|
|
55
55
|
end
|
|
@@ -79,7 +79,7 @@ module Karafka
|
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
puts
|
|
82
|
-
puts "Distribution completed #{green(
|
|
82
|
+
puts "Distribution completed #{green("successfully")}!"
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
private
|
|
@@ -31,11 +31,11 @@ module Karafka
|
|
|
31
31
|
kafka_config: %i[kafka]
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
-
desc
|
|
34
|
+
desc "Allows for parallel segments management"
|
|
35
35
|
|
|
36
36
|
option(
|
|
37
37
|
:groups,
|
|
38
|
-
|
|
38
|
+
"Names of consumer groups on which we want to run the command. All if not provided",
|
|
39
39
|
Array,
|
|
40
40
|
%w[
|
|
41
41
|
--groups
|
|
@@ -50,7 +50,7 @@ module Karafka
|
|
|
50
50
|
# their existing distributed offsets to be reset.
|
|
51
51
|
option(
|
|
52
52
|
:force,
|
|
53
|
-
|
|
53
|
+
"Should an operation on the parallel segments consumer group be forced",
|
|
54
54
|
TrueClass,
|
|
55
55
|
%w[
|
|
56
56
|
--force
|
|
@@ -58,13 +58,13 @@ module Karafka
|
|
|
58
58
|
)
|
|
59
59
|
|
|
60
60
|
# @param action [String] action we want to take
|
|
61
|
-
def call(action =
|
|
61
|
+
def call(action = "distribute")
|
|
62
62
|
case action
|
|
63
|
-
when
|
|
63
|
+
when "distribute"
|
|
64
64
|
Distribute.new(options).call
|
|
65
|
-
when
|
|
65
|
+
when "collapse"
|
|
66
66
|
Collapse.new(options).call
|
|
67
|
-
when
|
|
67
|
+
when "reset"
|
|
68
68
|
Collapse.new(options).call
|
|
69
69
|
Distribute.new(options).call
|
|
70
70
|
else
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Karafka Pro - Source Available Commercial Software
|
|
4
|
+
# Copyright (c) 2017-present Maciej Mensfeld. All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# This software is NOT open source. It is source-available commercial software
|
|
7
|
+
# requiring a paid license for use. It is NOT covered by LGPL.
|
|
8
|
+
#
|
|
9
|
+
# PROHIBITED:
|
|
10
|
+
# - Use without a valid commercial license
|
|
11
|
+
# - Redistribution, modification, or derivative works without authorization
|
|
12
|
+
# - Use as training data for AI/ML models or inclusion in datasets
|
|
13
|
+
# - Scraping, crawling, or automated collection for any purpose
|
|
14
|
+
#
|
|
15
|
+
# PERMITTED:
|
|
16
|
+
# - Reading, referencing, and linking for personal or commercial use
|
|
17
|
+
# - Runtime retrieval by AI assistants, coding agents, and RAG systems
|
|
18
|
+
# for the purpose of providing contextual help to Karafka users
|
|
19
|
+
#
|
|
20
|
+
# License: https://karafka.io/docs/Pro-License-Comm/
|
|
21
|
+
# Contact: contact@karafka.io
|
|
22
|
+
|
|
23
|
+
module Karafka
|
|
24
|
+
module Pro
|
|
25
|
+
module Cli
|
|
26
|
+
module Topics
|
|
27
|
+
# Checks health of Kafka topics by analyzing replication and durability settings
|
|
28
|
+
# Identifies topics with:
|
|
29
|
+
# - No redundancy (RF=1)
|
|
30
|
+
# - Zero fault tolerance (RF <= min.insync.replicas)
|
|
31
|
+
# - Low durability (min.insync.replicas=1)
|
|
32
|
+
class Health < Karafka::Cli::Topics::Base
|
|
33
|
+
# Executes the health check across all topics in the cluster
|
|
34
|
+
# @return [Boolean] true if issues were found, false if all topics are healthy
|
|
35
|
+
def call
|
|
36
|
+
issues_found = false
|
|
37
|
+
|
|
38
|
+
supervised("Checking topics health") do
|
|
39
|
+
topics = existing_topics
|
|
40
|
+
puts "Found #{topics.count} topics to check..."
|
|
41
|
+
puts
|
|
42
|
+
|
|
43
|
+
topics.each do |topic|
|
|
44
|
+
# Skip internal Kafka topics
|
|
45
|
+
next if topic[:topic_name].start_with?("__")
|
|
46
|
+
|
|
47
|
+
issue = analyze_topic(topic)
|
|
48
|
+
|
|
49
|
+
if issue
|
|
50
|
+
display_issue(issue)
|
|
51
|
+
issues_found = true
|
|
52
|
+
else
|
|
53
|
+
puts "#{green("✓")} #{topic[:topic_name]}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
puts
|
|
59
|
+
display_summary(issues_found)
|
|
60
|
+
puts
|
|
61
|
+
|
|
62
|
+
issues_found
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Analyzes a single topic for replication and durability issues
|
|
68
|
+
# @param topic [Hash] topic metadata from cluster info
|
|
69
|
+
# @return [Hash, nil] issue hash if problems found, nil if healthy
|
|
70
|
+
def analyze_topic(topic)
|
|
71
|
+
topic_name = topic[:topic_name]
|
|
72
|
+
rf = topic[:partitions].first&.fetch(:replica_count) || 0
|
|
73
|
+
min_isr = fetch_min_insync_replicas(topic_name)
|
|
74
|
+
|
|
75
|
+
check_replication_issue(topic_name, rf, min_isr)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Fetches min.insync.replicas configuration for a topic
|
|
79
|
+
# @param topic_name [String] name of the topic
|
|
80
|
+
# @return [Integer] min.insync.replicas value
|
|
81
|
+
def fetch_min_insync_replicas(topic_name)
|
|
82
|
+
configs = Admin::Configs.describe(
|
|
83
|
+
Admin::Configs::Resource.new(type: :topic, name: topic_name)
|
|
84
|
+
).first.configs
|
|
85
|
+
|
|
86
|
+
configs.find { |c| c.name == "min.insync.replicas" }.value.to_i
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Checks for replication and durability issues
|
|
90
|
+
# @param topic_name [String] name of the topic
|
|
91
|
+
# @param rf [Integer] replication factor
|
|
92
|
+
# @param min_isr [Integer] min.insync.replicas setting
|
|
93
|
+
# @return [Hash, nil] issue hash if problems found, nil if healthy
|
|
94
|
+
def check_replication_issue(topic_name, rf, min_isr)
|
|
95
|
+
return build_issue(topic_name, rf, min_isr, :critical, "RF=#{rf} (no redundancy)") if rf == 1
|
|
96
|
+
|
|
97
|
+
if rf <= min_isr
|
|
98
|
+
return build_issue(
|
|
99
|
+
topic_name,
|
|
100
|
+
rf,
|
|
101
|
+
min_isr,
|
|
102
|
+
:critical,
|
|
103
|
+
"RF=#{rf}, min.insync=#{min_isr} (zero fault tolerance)"
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
if min_isr == 1
|
|
108
|
+
return build_issue(
|
|
109
|
+
topic_name,
|
|
110
|
+
rf,
|
|
111
|
+
min_isr,
|
|
112
|
+
:warning,
|
|
113
|
+
"RF=#{rf}, min.insync=#{min_isr} (low durability)"
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Builds an issue hash with consistent structure
|
|
121
|
+
# @param topic [String] topic name
|
|
122
|
+
# @param rf [Integer] replication factor
|
|
123
|
+
# @param min_isr [Integer] min.insync.replicas
|
|
124
|
+
# @param severity [Symbol] :critical or :warning
|
|
125
|
+
# @param message [String] human-readable issue description
|
|
126
|
+
# @return [Hash] issue details
|
|
127
|
+
def build_issue(topic, rf, min_isr, severity, message)
|
|
128
|
+
{
|
|
129
|
+
topic: topic,
|
|
130
|
+
rf: rf,
|
|
131
|
+
min_isr: min_isr,
|
|
132
|
+
severity: severity,
|
|
133
|
+
message: message
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Displays a single issue immediately as it's found
|
|
138
|
+
# @param issue [Hash] issue details
|
|
139
|
+
def display_issue(issue)
|
|
140
|
+
color_method = (issue[:severity] == :critical) ? :red : :yellow
|
|
141
|
+
symbol = (issue[:severity] == :critical) ? "\u2717" : "\u26A0"
|
|
142
|
+
puts "#{send(color_method, symbol)} #{issue[:topic]}: #{issue[:message]}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Displays final summary and recommendations
|
|
146
|
+
# @param issues_found [Boolean] whether any issues were found
|
|
147
|
+
def display_summary(issues_found)
|
|
148
|
+
if issues_found
|
|
149
|
+
puts
|
|
150
|
+
puts "#{grey("Recommendations")}:"
|
|
151
|
+
puts " #{grey("\u2022")} Ensure RF >= 3 for production topics"
|
|
152
|
+
puts " #{grey("\u2022")} Set min.insync.replicas to at least 2"
|
|
153
|
+
puts " #{grey("\u2022")} Maintain RF > min.insync.replicas for fault tolerance"
|
|
154
|
+
else
|
|
155
|
+
puts "#{green("\u2713")} All topics are healthy"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Karafka Pro - Source Available Commercial Software
|
|
4
|
+
# Copyright (c) 2017-present Maciej Mensfeld. All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# This software is NOT open source. It is source-available commercial software
|
|
7
|
+
# requiring a paid license for use. It is NOT covered by LGPL.
|
|
8
|
+
#
|
|
9
|
+
# PROHIBITED:
|
|
10
|
+
# - Use without a valid commercial license
|
|
11
|
+
# - Redistribution, modification, or derivative works without authorization
|
|
12
|
+
# - Use as training data for AI/ML models or inclusion in datasets
|
|
13
|
+
# - Scraping, crawling, or automated collection for any purpose
|
|
14
|
+
#
|
|
15
|
+
# PERMITTED:
|
|
16
|
+
# - Reading, referencing, and linking for personal or commercial use
|
|
17
|
+
# - Runtime retrieval by AI assistants, coding agents, and RAG systems
|
|
18
|
+
# for the purpose of providing contextual help to Karafka users
|
|
19
|
+
#
|
|
20
|
+
# License: https://karafka.io/docs/Pro-License-Comm/
|
|
21
|
+
# Contact: contact@karafka.io
|
|
22
|
+
|
|
23
|
+
module Karafka
|
|
24
|
+
module Pro
|
|
25
|
+
module Cli
|
|
26
|
+
# Pro extension for the Topics CLI command
|
|
27
|
+
# Adds health checking functionality to the base topics command
|
|
28
|
+
module Topics
|
|
29
|
+
# Extends the base topics command to add health action support
|
|
30
|
+
module Extension
|
|
31
|
+
# @param action [String] action we want to take
|
|
32
|
+
def call(action = "help")
|
|
33
|
+
if action == "health"
|
|
34
|
+
detailed_exit_code = options.fetch(:detailed_exitcode, false)
|
|
35
|
+
issues_found = Pro::Cli::Topics::Health.new.call
|
|
36
|
+
|
|
37
|
+
return unless detailed_exit_code
|
|
38
|
+
|
|
39
|
+
# Exit with code 2 if issues found, code 0 if all healthy
|
|
40
|
+
issues_found ? exit(2) : exit(0)
|
|
41
|
+
else
|
|
42
|
+
super
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Extend the OSS Topics command with Pro functionality
|
|
52
|
+
Karafka::Cli::Topics.prepend(Karafka::Pro::Cli::Topics::Extension)
|
|
@@ -42,8 +42,8 @@ module Karafka
|
|
|
42
42
|
@mutex = Mutex.new
|
|
43
43
|
@changes = Hash.new do |h, k|
|
|
44
44
|
h[k] = {
|
|
45
|
-
state:
|
|
46
|
-
join_state:
|
|
45
|
+
state: "",
|
|
46
|
+
join_state: "",
|
|
47
47
|
state_age: 0,
|
|
48
48
|
changed_at: monotonic_now
|
|
49
49
|
}
|
|
@@ -86,9 +86,9 @@ module Karafka
|
|
|
86
86
|
times = []
|
|
87
87
|
# stateage is in microseconds
|
|
88
88
|
# We monitor broker changes to make sure we do not introduce extra friction
|
|
89
|
-
times << (statistics[
|
|
90
|
-
times << statistics[
|
|
91
|
-
times << statistics[
|
|
89
|
+
times << (statistics["brokers"].values.map { |stats| stats["stateage"] }.min / 1_000)
|
|
90
|
+
times << statistics["cgrp"]["rebalance_age"]
|
|
91
|
+
times << statistics["cgrp"]["stateage"]
|
|
92
92
|
|
|
93
93
|
# Keep the previous change age for changes that were triggered by us
|
|
94
94
|
previous_changed_at = @changes[subscription_group_id][:changed_at]
|
|
@@ -96,8 +96,8 @@ module Karafka
|
|
|
96
96
|
@changes[subscription_group_id].merge!(
|
|
97
97
|
state_age: times.min,
|
|
98
98
|
changed_at: previous_changed_at,
|
|
99
|
-
join_state: statistics[
|
|
100
|
-
state: statistics[
|
|
99
|
+
join_state: statistics["cgrp"]["join_state"],
|
|
100
|
+
state: statistics["cgrp"]["state"]
|
|
101
101
|
)
|
|
102
102
|
end
|
|
103
103
|
|
|
@@ -198,11 +198,11 @@ module Karafka
|
|
|
198
198
|
# partitions assigned in sgs that can be scaled. If that is the case, we scale up.
|
|
199
199
|
def scale_up
|
|
200
200
|
multi_part_sgs_families = Karafka::App
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
201
|
+
.assignments
|
|
202
|
+
.select { |_, partitions| partitions.size > 1 }
|
|
203
|
+
.keys
|
|
204
|
+
.map { |sg| sg.subscription_group.name }
|
|
205
|
+
.uniq
|
|
206
206
|
|
|
207
207
|
# Select connections for scaling up
|
|
208
208
|
in_sg_families do |first_subscription_group, sg_listeners|
|
|
@@ -256,8 +256,8 @@ module Karafka
|
|
|
256
256
|
|
|
257
257
|
state[:state_age] >= @scale_delay &&
|
|
258
258
|
(monotonic_now - state[:changed_at]) >= @scale_delay &&
|
|
259
|
-
state[:state] ==
|
|
260
|
-
state[:join_state] ==
|
|
259
|
+
state[:state] == "up" &&
|
|
260
|
+
state[:join_state] == "steady"
|
|
261
261
|
end
|
|
262
262
|
end
|
|
263
263
|
|
|
@@ -29,8 +29,8 @@ module Karafka
|
|
|
29
29
|
class Config < Karafka::Contracts::Base
|
|
30
30
|
configure do |config|
|
|
31
31
|
config.error_messages = YAML.safe_load_file(
|
|
32
|
-
File.join(Karafka.gem_root,
|
|
33
|
-
).fetch(
|
|
32
|
+
File.join(Karafka.gem_root, "config", "locales", "pro_errors.yml")
|
|
33
|
+
).fetch("en").fetch("validations").fetch("setup").fetch("config")
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
nested(:encryption) do
|
|
@@ -41,12 +41,12 @@ module Karafka
|
|
|
41
41
|
payload = message[:payload]
|
|
42
42
|
|
|
43
43
|
message[:headers] ||= {}
|
|
44
|
-
message[:headers][
|
|
44
|
+
message[:headers]["encryption"] = version
|
|
45
45
|
message[:payload] = cipher.encrypt(payload)
|
|
46
46
|
|
|
47
47
|
return message unless fingerprinter
|
|
48
48
|
|
|
49
|
-
message[:headers][
|
|
49
|
+
message[:headers]["encryption_fingerprint"] = fingerprinter.hexdigest(payload)
|
|
50
50
|
|
|
51
51
|
message
|
|
52
52
|
end
|
|
@@ -39,8 +39,8 @@ module Karafka
|
|
|
39
39
|
# @return [Object] deserialized payload
|
|
40
40
|
def call(message)
|
|
41
41
|
headers = message.headers
|
|
42
|
-
encryption = headers[
|
|
43
|
-
fingerprint = headers[
|
|
42
|
+
encryption = headers["encryption"]
|
|
43
|
+
fingerprint = headers["encryption_fingerprint"]
|
|
44
44
|
|
|
45
45
|
return super unless active && encryption
|
|
46
46
|
|
|
@@ -35,12 +35,12 @@ module Karafka
|
|
|
35
35
|
# Supporting versions allows us to be able to rotate private and public keys in case
|
|
36
36
|
# we would need this. We can increase the version, rotate and Karafka when decrypting
|
|
37
37
|
# will figure out proper private key based on the version
|
|
38
|
-
setting(:version, default:
|
|
38
|
+
setting(:version, default: "1")
|
|
39
39
|
|
|
40
40
|
# We always support one public key for producing messages
|
|
41
41
|
# Public key needs to be always present even if we do not plan to produce messages from
|
|
42
42
|
# a Karafka process. This is because of the web-ui and potentially other cases like this
|
|
43
|
-
setting(:public_key, default:
|
|
43
|
+
setting(:public_key, default: "")
|
|
44
44
|
|
|
45
45
|
# Private keys in pem format, where the key is the version and value is the key.
|
|
46
46
|
# This allows us to support key rotation
|
|
@@ -177,8 +177,8 @@ module Karafka
|
|
|
177
177
|
|
|
178
178
|
next unless SUPPORTED_NAMED_POSITIONS.include?(named_offset)
|
|
179
179
|
|
|
180
|
-
@mapped_topics[name][partition] = -1 if named_offset ==
|
|
181
|
-
@mapped_topics[name][partition] = -2 if named_offset ==
|
|
180
|
+
@mapped_topics[name][partition] = -1 if named_offset == "latest"
|
|
181
|
+
@mapped_topics[name][partition] = -2 if named_offset == "earliest"
|
|
182
182
|
end
|
|
183
183
|
end
|
|
184
184
|
end
|
data/lib/karafka/pro/iterator.rb
CHANGED
|
@@ -54,7 +54,7 @@ module Karafka
|
|
|
54
54
|
# so we don't stop polling data even when reaching the end (end on a given moment)
|
|
55
55
|
def initialize(
|
|
56
56
|
topics,
|
|
57
|
-
settings: {
|
|
57
|
+
settings: { "auto.offset.reset": "beginning" },
|
|
58
58
|
yield_nil: false,
|
|
59
59
|
max_wait_time: 200
|
|
60
60
|
)
|
data/lib/karafka/pro/loader.rb
CHANGED
|
@@ -37,6 +37,7 @@ module Karafka
|
|
|
37
37
|
encryption/setup/config
|
|
38
38
|
encryption/contracts/config
|
|
39
39
|
encryption/messages/parser
|
|
40
|
+
cli/topics
|
|
40
41
|
].freeze
|
|
41
42
|
|
|
42
43
|
# Zeitwerk pro loader
|
|
@@ -50,7 +51,7 @@ module Karafka
|
|
|
50
51
|
def require_all
|
|
51
52
|
FORCE_LOADED.each { |file| require_relative(file) }
|
|
52
53
|
|
|
53
|
-
PRO_LOADER.push_dir(Karafka.core_root.join(
|
|
54
|
+
PRO_LOADER.push_dir(Karafka.core_root.join("pro"), namespace: Karafka::Pro)
|
|
54
55
|
PRO_LOADER.setup
|
|
55
56
|
PRO_LOADER.eager_load
|
|
56
57
|
end
|
|
@@ -36,7 +36,7 @@ module Karafka
|
|
|
36
36
|
tracker = Tracker.new(
|
|
37
37
|
adi_config.safety_margin,
|
|
38
38
|
coordinator.last_polled_at,
|
|
39
|
-
topic.subscription_group.kafka.fetch(:
|
|
39
|
+
topic.subscription_group.kafka.fetch(:"max.poll.interval.ms")
|
|
40
40
|
)
|
|
41
41
|
|
|
42
42
|
messages.each(*args) do |message|
|
|
@@ -91,17 +91,27 @@ module Karafka
|
|
|
91
91
|
@offsets_metadata[offset] = offset_metadata
|
|
92
92
|
@current_offset_metadata = offset_metadata
|
|
93
93
|
|
|
94
|
-
group =
|
|
94
|
+
group = nil
|
|
95
|
+
position = nil
|
|
96
|
+
|
|
97
|
+
@groups.each do |reg_group|
|
|
98
|
+
pos = reg_group.index(offset)
|
|
99
|
+
|
|
100
|
+
if pos
|
|
101
|
+
group = reg_group
|
|
102
|
+
position = pos
|
|
103
|
+
break
|
|
104
|
+
end
|
|
105
|
+
end
|
|
95
106
|
|
|
96
107
|
# This case can happen when someone uses MoM and wants to mark message from a previous
|
|
97
108
|
# batch as consumed. We can add it, since the real offset refresh will point to it
|
|
98
109
|
unless group
|
|
99
110
|
group = [offset]
|
|
111
|
+
position = 0
|
|
100
112
|
@groups << group
|
|
101
113
|
end
|
|
102
114
|
|
|
103
|
-
position = group.index(offset)
|
|
104
|
-
|
|
105
115
|
# Mark all previous messages from the same group also as virtually consumed
|
|
106
116
|
group[0..position].each do |markable_offset|
|
|
107
117
|
# Set previous messages metadata offset as the offset of higher one for overwrites
|
|
@@ -135,7 +145,7 @@ module Karafka
|
|
|
135
145
|
|
|
136
146
|
# @return [Array<Integer>] Offsets of messages already marked as consumed virtually
|
|
137
147
|
def marked
|
|
138
|
-
@marked.select { |_, status| status }.map
|
|
148
|
+
@marked.select { |_, status| status }.map { |offset, _| offset }.sort
|
|
139
149
|
end
|
|
140
150
|
|
|
141
151
|
# Is there a real offset we can mark as consumed
|
|
@@ -150,13 +160,13 @@ module Karafka
|
|
|
150
160
|
raise Errors::InvalidRealOffsetUsageError unless markable?
|
|
151
161
|
|
|
152
162
|
offset_metadata = case @offset_metadata_strategy
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
163
|
+
when :exact
|
|
164
|
+
@offsets_metadata.fetch(@real_offset)
|
|
165
|
+
when :current
|
|
166
|
+
@current_offset_metadata
|
|
167
|
+
else
|
|
168
|
+
raise Errors::UnsupportedCaseError, @offset_metadata_strategy
|
|
169
|
+
end
|
|
160
170
|
|
|
161
171
|
[
|
|
162
172
|
Messages::Seek.new(
|
|
@@ -171,11 +181,11 @@ module Karafka
|
|
|
171
181
|
private
|
|
172
182
|
|
|
173
183
|
# Recomputes the biggest possible real offset we can have.
|
|
174
|
-
# It picks the
|
|
184
|
+
# It picks the biggest offset that has uninterrupted stream of virtually marked as
|
|
175
185
|
# consumed because this will be the collective offset.
|
|
176
186
|
def materialize_real_offset
|
|
177
|
-
@marked.
|
|
178
|
-
break unless marked
|
|
187
|
+
@marked.keys.sort.each do |offset|
|
|
188
|
+
break unless @marked[offset]
|
|
179
189
|
|
|
180
190
|
@real_offset = offset
|
|
181
191
|
end
|
|
@@ -46,7 +46,7 @@ module Karafka
|
|
|
46
46
|
# @param messages [Array<Karafka::Messages::Message>] array with messages. Please keep
|
|
47
47
|
# in mind, this may already be partial due to execution of previous filters.
|
|
48
48
|
def apply!(messages)
|
|
49
|
-
raise NotImplementedError,
|
|
49
|
+
raise NotImplementedError, "Implement in a subclass"
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
# @return [Symbol] filter post-execution action on consumer. Either `:skip`, `:pause` or
|
|
@@ -63,14 +63,14 @@ module Karafka
|
|
|
63
63
|
|
|
64
64
|
timeout = (@delay / 1_000.0) - (::Time.now.utc - @cursor.timestamp)
|
|
65
65
|
|
|
66
|
-
timeout <= 0 ? 0 : timeout * 1_000
|
|
66
|
+
(timeout <= 0) ? 0 : timeout * 1_000
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
# @return [Symbol] action to take on post-filtering
|
|
70
70
|
def action
|
|
71
71
|
return :skip unless applied?
|
|
72
72
|
|
|
73
|
-
timeout <= 0 ? :seek : :pause
|
|
73
|
+
(timeout <= 0) ? :seek : :pause
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
76
|
end
|
|
@@ -73,7 +73,7 @@ module Karafka
|
|
|
73
73
|
|
|
74
74
|
# @return [Integer, nil] ms timeout in case of pause or nil if not delaying
|
|
75
75
|
def timeout
|
|
76
|
-
@cursor && applied? ? PAUSE_TIMEOUT : nil
|
|
76
|
+
(@cursor && applied?) ? PAUSE_TIMEOUT : nil
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
# Pause when we had to back-off or skip if delay is not needed
|
|
@@ -47,16 +47,16 @@ module Karafka
|
|
|
47
47
|
# @return [String, Numeric] segment assignment key
|
|
48
48
|
def partition(message)
|
|
49
49
|
@partitioner.call(message)
|
|
50
|
-
rescue
|
|
50
|
+
rescue => e
|
|
51
51
|
# This should not happen. If you are seeing this it means your partitioner code
|
|
52
52
|
# failed and raised an error. We highly recommend mitigating partitioner level errors
|
|
53
53
|
# on the user side because this type of collapse should be considered a last resort
|
|
54
54
|
Karafka.monitor.instrument(
|
|
55
|
-
|
|
55
|
+
"error.occurred",
|
|
56
56
|
caller: self,
|
|
57
57
|
error: e,
|
|
58
58
|
message: message,
|
|
59
|
-
type:
|
|
59
|
+
type: "parallel_segments.partitioner.error"
|
|
60
60
|
)
|
|
61
61
|
|
|
62
62
|
:failure
|
|
@@ -70,14 +70,14 @@ module Karafka
|
|
|
70
70
|
return 0 if message_segment_key == :failure
|
|
71
71
|
|
|
72
72
|
@reducer.call(message_segment_key)
|
|
73
|
-
rescue
|
|
73
|
+
rescue => e
|
|
74
74
|
# @see `#partition` method error handling doc
|
|
75
75
|
Karafka.monitor.instrument(
|
|
76
|
-
|
|
76
|
+
"error.occurred",
|
|
77
77
|
caller: self,
|
|
78
78
|
error: e,
|
|
79
79
|
message_segment_key: message_segment_key,
|
|
80
|
-
type:
|
|
80
|
+
type: "parallel_segments.reducer.error"
|
|
81
81
|
)
|
|
82
82
|
|
|
83
83
|
0
|