karafka 2.3.3 → 2.4.0.beta2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/ci.yml +12 -38
- data/CHANGELOG.md +59 -0
- data/Gemfile +6 -3
- data/Gemfile.lock +29 -27
- data/bin/integrations +1 -1
- data/config/locales/errors.yml +21 -2
- data/config/locales/pro_errors.yml +16 -1
- data/karafka.gemspec +4 -2
- data/lib/active_job/queue_adapters/karafka_adapter.rb +2 -0
- data/lib/karafka/admin/configs/config.rb +81 -0
- data/lib/karafka/admin/configs/resource.rb +88 -0
- data/lib/karafka/admin/configs.rb +103 -0
- data/lib/karafka/admin.rb +211 -90
- data/lib/karafka/base_consumer.rb +2 -2
- data/lib/karafka/cli/info.rb +9 -7
- data/lib/karafka/cli/server.rb +7 -7
- data/lib/karafka/cli/topics/align.rb +109 -0
- data/lib/karafka/cli/topics/base.rb +66 -0
- data/lib/karafka/cli/topics/create.rb +35 -0
- data/lib/karafka/cli/topics/delete.rb +30 -0
- data/lib/karafka/cli/topics/migrate.rb +31 -0
- data/lib/karafka/cli/topics/plan.rb +169 -0
- data/lib/karafka/cli/topics/repartition.rb +41 -0
- data/lib/karafka/cli/topics/reset.rb +18 -0
- data/lib/karafka/cli/topics.rb +13 -123
- data/lib/karafka/connection/client.rb +55 -37
- data/lib/karafka/connection/listener.rb +22 -17
- data/lib/karafka/connection/proxy.rb +93 -4
- data/lib/karafka/connection/status.rb +14 -2
- data/lib/karafka/constraints.rb +3 -3
- data/lib/karafka/contracts/config.rb +14 -1
- data/lib/karafka/contracts/topic.rb +1 -1
- data/lib/karafka/deserializers/headers.rb +15 -0
- data/lib/karafka/deserializers/key.rb +15 -0
- data/lib/karafka/deserializers/payload.rb +16 -0
- data/lib/karafka/embedded.rb +2 -0
- data/lib/karafka/helpers/async.rb +5 -2
- data/lib/karafka/helpers/colorize.rb +6 -0
- data/lib/karafka/instrumentation/callbacks/oauthbearer_token_refresh.rb +29 -0
- data/lib/karafka/instrumentation/logger_listener.rb +23 -3
- data/lib/karafka/instrumentation/notifications.rb +10 -0
- data/lib/karafka/instrumentation/vendors/appsignal/client.rb +16 -2
- data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +20 -0
- data/lib/karafka/messages/batch_metadata.rb +1 -1
- data/lib/karafka/messages/builders/batch_metadata.rb +1 -1
- data/lib/karafka/messages/builders/message.rb +10 -6
- data/lib/karafka/messages/message.rb +2 -1
- data/lib/karafka/messages/metadata.rb +20 -4
- data/lib/karafka/messages/parser.rb +1 -1
- data/lib/karafka/pro/base_consumer.rb +12 -23
- data/lib/karafka/pro/encryption/cipher.rb +7 -3
- data/lib/karafka/pro/encryption/contracts/config.rb +1 -0
- data/lib/karafka/pro/encryption/errors.rb +4 -1
- data/lib/karafka/pro/encryption/messages/middleware.rb +13 -11
- data/lib/karafka/pro/encryption/messages/parser.rb +22 -20
- data/lib/karafka/pro/encryption/setup/config.rb +5 -0
- data/lib/karafka/pro/iterator/expander.rb +2 -1
- data/lib/karafka/pro/iterator/tpl_builder.rb +38 -0
- data/lib/karafka/pro/iterator.rb +28 -2
- data/lib/karafka/pro/loader.rb +3 -0
- data/lib/karafka/pro/processing/coordinator.rb +15 -2
- data/lib/karafka/pro/processing/expansions_selector.rb +2 -0
- data/lib/karafka/pro/processing/jobs_queue.rb +122 -5
- data/lib/karafka/pro/processing/periodic_job/consumer.rb +67 -0
- data/lib/karafka/pro/processing/piping/consumer.rb +126 -0
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom_vp.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom_vp.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom_vp.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_mom.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_mom_vp.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/lrj_mom_vp.rb +2 -0
- data/lib/karafka/pro/processing/strategies/default.rb +5 -1
- data/lib/karafka/pro/processing/strategies/dlq/default.rb +21 -5
- data/lib/karafka/pro/processing/strategies/lrj/default.rb +2 -0
- data/lib/karafka/pro/processing/strategies/lrj/mom.rb +2 -0
- data/lib/karafka/pro/processing/subscription_groups_coordinator.rb +52 -0
- data/lib/karafka/pro/routing/features/direct_assignments/config.rb +27 -0
- data/lib/karafka/pro/routing/features/direct_assignments/contracts/consumer_group.rb +53 -0
- data/lib/karafka/pro/routing/features/direct_assignments/contracts/topic.rb +108 -0
- data/lib/karafka/pro/routing/features/direct_assignments/subscription_group.rb +77 -0
- data/lib/karafka/pro/routing/features/direct_assignments/topic.rb +69 -0
- data/lib/karafka/pro/routing/features/direct_assignments.rb +25 -0
- data/lib/karafka/pro/routing/features/patterns/builder.rb +1 -1
- data/lib/karafka/pro/routing/features/swarm/contracts/routing.rb +76 -0
- data/lib/karafka/pro/routing/features/swarm/contracts/topic.rb +16 -5
- data/lib/karafka/pro/routing/features/swarm/topic.rb +25 -2
- data/lib/karafka/pro/routing/features/swarm.rb +11 -0
- data/lib/karafka/pro/swarm/liveness_listener.rb +20 -0
- data/lib/karafka/processing/coordinator.rb +17 -8
- data/lib/karafka/processing/coordinators_buffer.rb +5 -2
- data/lib/karafka/processing/executor.rb +6 -2
- data/lib/karafka/processing/executors_buffer.rb +5 -2
- data/lib/karafka/processing/jobs_queue.rb +9 -4
- data/lib/karafka/processing/strategies/aj_dlq_mom.rb +1 -1
- data/lib/karafka/processing/strategies/default.rb +7 -1
- data/lib/karafka/processing/strategies/dlq.rb +17 -2
- data/lib/karafka/processing/workers_batch.rb +4 -1
- data/lib/karafka/routing/builder.rb +6 -2
- data/lib/karafka/routing/consumer_group.rb +2 -1
- data/lib/karafka/routing/features/dead_letter_queue/config.rb +5 -0
- data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +8 -0
- data/lib/karafka/routing/features/dead_letter_queue/topic.rb +10 -2
- data/lib/karafka/routing/features/deserializers/config.rb +18 -0
- data/lib/karafka/routing/features/deserializers/contracts/topic.rb +31 -0
- data/lib/karafka/routing/features/deserializers/topic.rb +51 -0
- data/lib/karafka/routing/features/deserializers.rb +11 -0
- data/lib/karafka/routing/proxy.rb +9 -14
- data/lib/karafka/routing/router.rb +11 -2
- data/lib/karafka/routing/subscription_group.rb +9 -1
- data/lib/karafka/routing/topic.rb +0 -1
- data/lib/karafka/runner.rb +1 -1
- data/lib/karafka/setup/config.rb +50 -9
- data/lib/karafka/status.rb +7 -8
- data/lib/karafka/swarm/supervisor.rb +16 -2
- data/lib/karafka/templates/karafka.rb.erb +28 -1
- data/lib/karafka/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +38 -12
- metadata.gz.sig +0 -0
- data/lib/karafka/routing/consumer_mapper.rb +0 -23
- data/lib/karafka/serialization/json/deserializer.rb +0 -19
- data/lib/karafka/time_trackers/partition_usage.rb +0 -56
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
class Cli
|
|
5
|
+
class Topics < Cli::Base
|
|
6
|
+
# Aligns configuration of all the declarative topics that exist based on the declarative
|
|
7
|
+
# topics definitions.
|
|
8
|
+
#
|
|
9
|
+
# Takes into consideration already existing settings, so will only align what is needed.
|
|
10
|
+
#
|
|
11
|
+
# Keep in mind, this is NOT transactional. Kafka topic changes are not transactional so
|
|
12
|
+
# it is highly recommended to test it before running in prod.
|
|
13
|
+
#
|
|
14
|
+
# @note This command does NOT repartition and does NOT create new topics. It only aligns
|
|
15
|
+
# configuration of existing topics.
|
|
16
|
+
class Align < Base
|
|
17
|
+
# @return [Boolean] true if there were any changes applied, otherwise false
|
|
18
|
+
def call
|
|
19
|
+
if candidate_topics.empty?
|
|
20
|
+
puts "#{yellow('Skipping')} because no declarative topics exist."
|
|
21
|
+
|
|
22
|
+
return false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
resources_to_migrate = build_resources_to_migrate
|
|
26
|
+
|
|
27
|
+
if resources_to_migrate.empty?
|
|
28
|
+
puts "#{yellow('Skipping')} because there are no configurations to align."
|
|
29
|
+
|
|
30
|
+
return false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
names = resources_to_migrate.map(&:name).join(', ')
|
|
34
|
+
puts "Updating configuration of the following topics: #{names}"
|
|
35
|
+
Karafka::Admin::Configs.alter(resources_to_migrate)
|
|
36
|
+
puts "#{green('Updated')} all requested topics configuration."
|
|
37
|
+
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# Selects topics that exist and potentially may have config to align
|
|
44
|
+
#
|
|
45
|
+
# @return [Set<Karafka::Routing::Topic>]
|
|
46
|
+
def candidate_topics
|
|
47
|
+
return @candidate_topics if @candidate_topics
|
|
48
|
+
|
|
49
|
+
@candidate_topics = Set.new
|
|
50
|
+
|
|
51
|
+
# First lets only operate on topics that do exist
|
|
52
|
+
declaratives_routing_topics.each do |topic|
|
|
53
|
+
unless existing_topics_names.include?(topic.name)
|
|
54
|
+
puts "#{yellow('Skipping')} because topic #{topic.name} does not exist."
|
|
55
|
+
next
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
@candidate_topics << topic
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
@candidate_topics
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Iterates over configs of all the candidate topics and prepares alignment resources for
|
|
65
|
+
# a single request to Kafka
|
|
66
|
+
# @return [Array<Karafka::Admin::Configs::Resource>] all topics with config change requests
|
|
67
|
+
def build_resources_to_migrate
|
|
68
|
+
# We build non-fetched topics resources representations for further altering
|
|
69
|
+
resources = candidate_topics.map do |topic|
|
|
70
|
+
Admin::Configs::Resource.new(type: :topic, name: topic.name)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
resources_to_migrate = Set.new
|
|
74
|
+
|
|
75
|
+
# We fetch all the configurations for all the topics
|
|
76
|
+
Admin::Configs.describe(resources).each do |topic_with_configs|
|
|
77
|
+
t_candidate = candidate_topics.find do |candidate|
|
|
78
|
+
candidate.name == topic_with_configs.name
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
change_resource = resources.find do |resource|
|
|
82
|
+
resource.name == topic_with_configs.name
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# librdkafka returns us all the results as strings, so we need to align our config
|
|
86
|
+
# representation so we can compare those
|
|
87
|
+
desired_configs = t_candidate.declaratives.details.dup
|
|
88
|
+
desired_configs.transform_values!(&:to_s)
|
|
89
|
+
desired_configs.transform_keys!(&:to_s)
|
|
90
|
+
|
|
91
|
+
topic_with_configs.configs.each do |config|
|
|
92
|
+
next unless desired_configs.key?(config.name)
|
|
93
|
+
|
|
94
|
+
desired_config = desired_configs.fetch(config.name)
|
|
95
|
+
|
|
96
|
+
# Do not migrate if existing and desired values are the same
|
|
97
|
+
next if desired_config == config.value
|
|
98
|
+
|
|
99
|
+
change_resource.set(config.name, desired_config)
|
|
100
|
+
resources_to_migrate << change_resource
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
resources_to_migrate.to_a
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
class Cli
|
|
5
|
+
class Topics < Cli::Base
|
|
6
|
+
# Base class for all the topics related operations
|
|
7
|
+
class Base
|
|
8
|
+
include Helpers::Colorize
|
|
9
|
+
include Helpers::ConfigImporter.new(
|
|
10
|
+
kafka_config: %i[kafka]
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
# @return [Array<Karafka::Routing::Topic>] all available topics that can be managed
|
|
16
|
+
# @note If topic is defined in multiple consumer groups, first config will be used. This
|
|
17
|
+
# means, that this CLI will not work for simultaneous management of multiple clusters
|
|
18
|
+
# from a single CLI command execution flow.
|
|
19
|
+
def declaratives_routing_topics
|
|
20
|
+
return @declaratives_routing_topics if @declaratives_routing_topics
|
|
21
|
+
|
|
22
|
+
collected_topics = {}
|
|
23
|
+
default_servers = kafka_config[:'bootstrap.servers']
|
|
24
|
+
|
|
25
|
+
App.consumer_groups.each do |consumer_group|
|
|
26
|
+
consumer_group.topics.each do |topic|
|
|
27
|
+
# Skip topics that were explicitly disabled from management
|
|
28
|
+
next unless topic.declaratives.active?
|
|
29
|
+
# If bootstrap servers are different, consider this a different cluster
|
|
30
|
+
next unless default_servers == topic.kafka[:'bootstrap.servers']
|
|
31
|
+
|
|
32
|
+
collected_topics[topic.name] ||= topic
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
@declaratives_routing_topics = collected_topics.values
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Array<Hash>] existing topics details
|
|
40
|
+
def existing_topics
|
|
41
|
+
@existing_topics ||= Admin.cluster_info.topics
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Array<String>] names of already existing topics
|
|
45
|
+
def existing_topics_names
|
|
46
|
+
existing_topics.map { |topic| topic.fetch(:topic_name) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Waits with a message, that we are waiting on topics
|
|
50
|
+
# This is not doing much, just waiting as there are some cases that it takes a bit of time
|
|
51
|
+
# for Kafka to actually propagate new topics knowledge across the cluster. We give it that
|
|
52
|
+
# bit of time just in case.
|
|
53
|
+
def wait
|
|
54
|
+
print 'Waiting for the topics to synchronize in the cluster'
|
|
55
|
+
|
|
56
|
+
5.times do
|
|
57
|
+
sleep(1)
|
|
58
|
+
print '.'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
puts
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
class Cli
|
|
5
|
+
class Topics < Cli::Base
|
|
6
|
+
# Creates topics based on the routing setup and configuration
|
|
7
|
+
class Create < Base
|
|
8
|
+
# @return [Boolean] true if any topic was created, otherwise false
|
|
9
|
+
def call
|
|
10
|
+
any_created = false
|
|
11
|
+
|
|
12
|
+
declaratives_routing_topics.each do |topic|
|
|
13
|
+
name = topic.name
|
|
14
|
+
|
|
15
|
+
if existing_topics_names.include?(name)
|
|
16
|
+
puts "#{yellow('Skipping')} because topic #{name} already exists."
|
|
17
|
+
else
|
|
18
|
+
puts "Creating topic #{name}..."
|
|
19
|
+
Admin.create_topic(
|
|
20
|
+
name,
|
|
21
|
+
topic.declaratives.partitions,
|
|
22
|
+
topic.declaratives.replication_factor,
|
|
23
|
+
topic.declaratives.details
|
|
24
|
+
)
|
|
25
|
+
puts "#{green('Created')} topic #{name}."
|
|
26
|
+
any_created = true
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
any_created
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
class Cli
|
|
5
|
+
class Topics < Cli::Base
|
|
6
|
+
# Deletes routing based topics
|
|
7
|
+
class Delete < Base
|
|
8
|
+
# @return [Boolean] true if any topic was deleted, otherwise false
|
|
9
|
+
def call
|
|
10
|
+
any_deleted = false
|
|
11
|
+
|
|
12
|
+
declaratives_routing_topics.each do |topic|
|
|
13
|
+
name = topic.name
|
|
14
|
+
|
|
15
|
+
if existing_topics_names.include?(name)
|
|
16
|
+
puts "Deleting topic #{name}..."
|
|
17
|
+
Admin.delete_topic(name)
|
|
18
|
+
puts "#{green('Deleted')} topic #{name}."
|
|
19
|
+
any_deleted = true
|
|
20
|
+
else
|
|
21
|
+
puts "#{yellow('Skipping')} because topic #{name} does not exist."
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
any_deleted
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
class Cli
|
|
5
|
+
class Topics < Cli::Base
|
|
6
|
+
# Creates missing topics and aligns the partitions count
|
|
7
|
+
class Migrate < Base
|
|
8
|
+
# Runs the migration
|
|
9
|
+
# @return [Boolean] true if there were any changes applied
|
|
10
|
+
def call
|
|
11
|
+
any_changes = false
|
|
12
|
+
|
|
13
|
+
if Topics::Create.new.call
|
|
14
|
+
any_changes = true
|
|
15
|
+
wait
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
if Topics::Repartition.new.call
|
|
19
|
+
any_changes = true
|
|
20
|
+
wait
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# No need to wait after the last one
|
|
24
|
+
any_changes = true if Topics::Align.new.call
|
|
25
|
+
|
|
26
|
+
any_changes
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
class Cli
|
|
5
|
+
class Topics < Cli::Base
|
|
6
|
+
# Plans the migration process and prints what changes are going to be applied if migration
|
|
7
|
+
# would to be executed
|
|
8
|
+
class Plan < Base
|
|
9
|
+
# Figures out scope of changes that need to happen
|
|
10
|
+
# @return [Boolean] true if running migrate would change anything, false otherwise
|
|
11
|
+
def call
|
|
12
|
+
# If no changes at all, just print and stop
|
|
13
|
+
if topics_to_create.empty? && topics_to_repartition.empty? && topics_to_alter.empty?
|
|
14
|
+
puts "Karafka will #{yellow('not')} perform any actions. No changes needed."
|
|
15
|
+
|
|
16
|
+
return false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
unless topics_to_create.empty?
|
|
20
|
+
puts 'Following topics will be created:'
|
|
21
|
+
puts
|
|
22
|
+
|
|
23
|
+
topics_to_create.each do |topic|
|
|
24
|
+
puts " #{green('+')} #{topic.name}:"
|
|
25
|
+
puts " #{green('+')} partitions: \"#{topic.declaratives.partitions}\""
|
|
26
|
+
|
|
27
|
+
topic.declaratives.details.each do |name, value|
|
|
28
|
+
puts " #{green('+')} #{name}: \"#{value}\""
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
puts
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
unless topics_to_repartition.empty?
|
|
36
|
+
puts 'Following topics will be repartitioned:'
|
|
37
|
+
puts
|
|
38
|
+
|
|
39
|
+
topics_to_repartition.each do |topic, partitions|
|
|
40
|
+
from = partitions
|
|
41
|
+
to = topic.declaratives.partitions
|
|
42
|
+
|
|
43
|
+
puts " #{yellow('~')} #{topic.name}:"
|
|
44
|
+
puts " #{yellow('~')} partitions: \"#{red(from)}\" #{grey('=>')} \"#{green(to)}\""
|
|
45
|
+
puts
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
unless topics_to_alter.empty?
|
|
50
|
+
puts 'Following topics will have configuration changes:'
|
|
51
|
+
puts
|
|
52
|
+
|
|
53
|
+
topics_to_alter.each do |topic, configs|
|
|
54
|
+
puts " #{yellow('~')} #{topic.name}:"
|
|
55
|
+
|
|
56
|
+
configs.each do |name, changes|
|
|
57
|
+
from = changes.fetch(:from)
|
|
58
|
+
to = changes.fetch(:to)
|
|
59
|
+
action = changes.fetch(:action)
|
|
60
|
+
type = action == :change ? yellow('~') : green('+')
|
|
61
|
+
puts " #{type} #{name}: \"#{red(from)}\" #{grey('=>')} \"#{green(to)}\""
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
puts
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# @return [Array<Karafka::Routing::Topic>] topics that will be created
|
|
74
|
+
def topics_to_create
|
|
75
|
+
return @topics_to_create if @topics_to_create
|
|
76
|
+
|
|
77
|
+
@topics_to_create = declaratives_routing_topics.reject do |topic|
|
|
78
|
+
existing_topics_names.include?(topic.name)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
@topics_to_create
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @return [Array<Array<Karafka::Routing::Topic, Integer>>] array with topics that will
|
|
85
|
+
# be repartitioned and current number of partitions
|
|
86
|
+
def topics_to_repartition
|
|
87
|
+
return @topics_to_repartition if @topics_to_repartition
|
|
88
|
+
|
|
89
|
+
@topics_to_repartition = []
|
|
90
|
+
|
|
91
|
+
declaratives_routing_topics.each do |declarative_topic|
|
|
92
|
+
existing_topic = existing_topics.find do |topic|
|
|
93
|
+
topic.fetch(:topic_name) == declarative_topic.name
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
next unless existing_topic
|
|
97
|
+
|
|
98
|
+
existing_partitions = existing_topic.fetch(:partition_count)
|
|
99
|
+
|
|
100
|
+
next if declarative_topic.declaratives.partitions == existing_partitions
|
|
101
|
+
|
|
102
|
+
@topics_to_repartition << [declarative_topic, existing_partitions]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
@topics_to_repartition
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @return [Hash] Hash where keys are topics to alter and values are configs that will
|
|
109
|
+
# be altered.
|
|
110
|
+
def topics_to_alter
|
|
111
|
+
return @topics_to_alter if @topics_to_alter
|
|
112
|
+
|
|
113
|
+
topics_to_check = []
|
|
114
|
+
|
|
115
|
+
declaratives_routing_topics.each do |declarative_topic|
|
|
116
|
+
next if declarative_topic.declaratives.details.empty?
|
|
117
|
+
next unless existing_topics_names.include?(declarative_topic.name)
|
|
118
|
+
|
|
119
|
+
topics_to_check << [
|
|
120
|
+
declarative_topic,
|
|
121
|
+
Admin::Configs::Resource.new(type: :topic, name: declarative_topic.name)
|
|
122
|
+
]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
@topics_to_alter = {}
|
|
126
|
+
|
|
127
|
+
return @topics_to_alter if topics_to_check.empty?
|
|
128
|
+
|
|
129
|
+
Admin::Configs.describe(topics_to_check.map(&:last)).each.with_index do |topic_c, index|
|
|
130
|
+
declarative = topics_to_check[index].first
|
|
131
|
+
declarative_config = declarative.declaratives.details.dup
|
|
132
|
+
declarative_config.transform_keys!(&:to_s)
|
|
133
|
+
declarative_config.transform_values!(&:to_s)
|
|
134
|
+
|
|
135
|
+
# We only apply additive/in-place changes so we start from our config
|
|
136
|
+
declarative_config.each do |declarative_name, declarative_value|
|
|
137
|
+
@topics_to_alter[declarative] ||= {}
|
|
138
|
+
|
|
139
|
+
@topics_to_alter[declarative][declarative_name] ||= {
|
|
140
|
+
from: '',
|
|
141
|
+
to: declarative_value,
|
|
142
|
+
action: :add
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
scoped = @topics_to_alter[declarative][declarative_name]
|
|
146
|
+
|
|
147
|
+
topic_c.configs.each do |config|
|
|
148
|
+
next unless declarative_name == config.name
|
|
149
|
+
|
|
150
|
+
scoped[:action] = :change
|
|
151
|
+
scoped[:from] = config.value
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Remove change definitions that would migrate to the same value as present
|
|
155
|
+
@topics_to_alter[declarative].delete_if do |_name, details|
|
|
156
|
+
details[:from] == details[:to]
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Remove topics without any changes
|
|
161
|
+
@topics_to_alter.delete_if { |_name, configs| configs.empty? }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
@topics_to_alter
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
class Cli
|
|
5
|
+
class Topics < Cli::Base
|
|
6
|
+
# Increases number of partitions on topics that have less partitions than defined
|
|
7
|
+
# Will **not** create topics if missing.
|
|
8
|
+
class Repartition < Base
|
|
9
|
+
# @return [Boolean] true if anything was repartitioned, otherwise false
|
|
10
|
+
def call
|
|
11
|
+
any_repartitioned = false
|
|
12
|
+
|
|
13
|
+
existing_partitions = existing_topics.map do |topic|
|
|
14
|
+
[topic.fetch(:topic_name), topic.fetch(:partition_count)]
|
|
15
|
+
end.to_h
|
|
16
|
+
|
|
17
|
+
declaratives_routing_topics.each do |topic|
|
|
18
|
+
name = topic.name
|
|
19
|
+
|
|
20
|
+
desired_count = topic.config.partitions
|
|
21
|
+
existing_count = existing_partitions.fetch(name, false)
|
|
22
|
+
|
|
23
|
+
if existing_count && existing_count < desired_count
|
|
24
|
+
puts "Increasing number of partitions to #{desired_count} on topic #{name}..."
|
|
25
|
+
Admin.create_partitions(name, desired_count)
|
|
26
|
+
change = desired_count - existing_count
|
|
27
|
+
puts "#{green('Created')} #{change} additional partitions on topic #{name}."
|
|
28
|
+
any_repartitioned = true
|
|
29
|
+
elsif existing_count
|
|
30
|
+
puts "#{yellow('Skipping')} because topic #{name} has #{existing_count} partitions."
|
|
31
|
+
else
|
|
32
|
+
puts "#{yellow('Skipping')} because topic #{name} does not exist."
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
any_repartitioned
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
class Cli
|
|
5
|
+
class Topics < Cli::Base
|
|
6
|
+
# Deletes routing based topics and re-creates them
|
|
7
|
+
class Reset < Base
|
|
8
|
+
# @return [true] since it is a reset, always changes so `true` always
|
|
9
|
+
def call
|
|
10
|
+
Delete.new.call && wait
|
|
11
|
+
Create.new.call
|
|
12
|
+
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/karafka/cli/topics.rb
CHANGED
|
@@ -5,142 +5,32 @@ module Karafka
|
|
|
5
5
|
# CLI actions related to Kafka cluster topics management
|
|
6
6
|
class Topics < Base
|
|
7
7
|
include Helpers::Colorize
|
|
8
|
+
include Helpers::ConfigImporter.new(
|
|
9
|
+
kafka_config: %i[kafka]
|
|
10
|
+
)
|
|
8
11
|
|
|
9
|
-
desc 'Allows for the topics management
|
|
12
|
+
desc 'Allows for the topics management'
|
|
10
13
|
# @param action [String] action we want to take
|
|
11
14
|
def call(action = 'missing')
|
|
12
15
|
case action
|
|
13
16
|
when 'create'
|
|
14
|
-
|
|
17
|
+
Topics::Create.new.call
|
|
15
18
|
when 'delete'
|
|
16
|
-
|
|
19
|
+
Topics::Delete.new.call
|
|
17
20
|
when 'reset'
|
|
18
|
-
|
|
21
|
+
Topics::Reset.new.call
|
|
19
22
|
when 'repartition'
|
|
20
|
-
|
|
23
|
+
Topics::Repartition.new.call
|
|
21
24
|
when 'migrate'
|
|
22
|
-
|
|
25
|
+
Topics::Migrate.new.call
|
|
26
|
+
when 'align'
|
|
27
|
+
Topics::Align.new.call
|
|
28
|
+
when 'plan'
|
|
29
|
+
Topics::Plan.new.call
|
|
23
30
|
else
|
|
24
31
|
raise ::ArgumentError, "Invalid topics action: #{action}"
|
|
25
32
|
end
|
|
26
33
|
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
# Creates topics based on the routing setup and configuration
|
|
31
|
-
def create
|
|
32
|
-
declaratives_routing_topics.each do |topic|
|
|
33
|
-
name = topic.name
|
|
34
|
-
|
|
35
|
-
if existing_topics_names.include?(name)
|
|
36
|
-
puts "#{yellow('Skipping')} because topic #{name} already exists."
|
|
37
|
-
else
|
|
38
|
-
puts "Creating topic #{name}..."
|
|
39
|
-
Admin.create_topic(
|
|
40
|
-
name,
|
|
41
|
-
topic.declaratives.partitions,
|
|
42
|
-
topic.declaratives.replication_factor,
|
|
43
|
-
topic.declaratives.details
|
|
44
|
-
)
|
|
45
|
-
puts "#{green('Created')} topic #{name}."
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Deletes routing based topics
|
|
51
|
-
def delete
|
|
52
|
-
declaratives_routing_topics.each do |topic|
|
|
53
|
-
name = topic.name
|
|
54
|
-
|
|
55
|
-
if existing_topics_names.include?(name)
|
|
56
|
-
puts "Deleting topic #{name}..."
|
|
57
|
-
Admin.delete_topic(name)
|
|
58
|
-
puts "#{green('Deleted')} topic #{name}."
|
|
59
|
-
else
|
|
60
|
-
puts "#{yellow('Skipping')} because topic #{name} does not exist."
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Deletes routing based topics and re-creates them
|
|
66
|
-
def reset
|
|
67
|
-
delete
|
|
68
|
-
|
|
69
|
-
# We need to invalidate the metadata cache, otherwise we will think, that the topic
|
|
70
|
-
# already exists
|
|
71
|
-
@existing_topics = nil
|
|
72
|
-
|
|
73
|
-
create
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Creates missing topics and aligns the partitions count
|
|
77
|
-
def migrate
|
|
78
|
-
create
|
|
79
|
-
|
|
80
|
-
@existing_topics = nil
|
|
81
|
-
|
|
82
|
-
repartition
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Increases number of partitions on topics that have less partitions than defined
|
|
86
|
-
# Will **not** create topics if missing.
|
|
87
|
-
def repartition
|
|
88
|
-
existing_partitions = existing_topics.map do |topic|
|
|
89
|
-
[topic.fetch(:topic_name), topic.fetch(:partition_count)]
|
|
90
|
-
end.to_h
|
|
91
|
-
|
|
92
|
-
declaratives_routing_topics.each do |topic|
|
|
93
|
-
name = topic.name
|
|
94
|
-
|
|
95
|
-
desired_count = topic.config.partitions
|
|
96
|
-
existing_count = existing_partitions.fetch(name, false)
|
|
97
|
-
|
|
98
|
-
if existing_count && existing_count < desired_count
|
|
99
|
-
puts "Increasing number of partitions to #{desired_count} on topic #{name}..."
|
|
100
|
-
Admin.create_partitions(name, desired_count)
|
|
101
|
-
change = desired_count - existing_count
|
|
102
|
-
puts "#{green('Created')} #{change} additional partitions on topic #{name}."
|
|
103
|
-
elsif existing_count
|
|
104
|
-
puts "#{yellow('Skipping')} because topic #{name} has #{existing_count} partitions."
|
|
105
|
-
else
|
|
106
|
-
puts "#{yellow('Skipping')} because topic #{name} does not exist."
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# @return [Array<Karafka::Routing::Topic>] all available topics that can be managed
|
|
112
|
-
# @note If topic is defined in multiple consumer groups, first config will be used. This
|
|
113
|
-
# means, that this CLI will not work for simultaneous management of multiple clusters from
|
|
114
|
-
# a single CLI command execution flow.
|
|
115
|
-
def declaratives_routing_topics
|
|
116
|
-
return @declaratives_routing_topics if @declaratives_routing_topics
|
|
117
|
-
|
|
118
|
-
collected_topics = {}
|
|
119
|
-
default_servers = Karafka::App.config.kafka[:'bootstrap.servers']
|
|
120
|
-
|
|
121
|
-
App.consumer_groups.each do |consumer_group|
|
|
122
|
-
consumer_group.topics.each do |topic|
|
|
123
|
-
# Skip topics that were explicitly disabled from management
|
|
124
|
-
next unless topic.declaratives.active?
|
|
125
|
-
# If bootstrap servers are different, consider this a different cluster
|
|
126
|
-
next unless default_servers == topic.kafka[:'bootstrap.servers']
|
|
127
|
-
|
|
128
|
-
collected_topics[topic.name] ||= topic
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
@declaratives_routing_topics = collected_topics.values
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# @return [Array<Hash>] existing topics details
|
|
136
|
-
def existing_topics
|
|
137
|
-
@existing_topics ||= Admin.cluster_info.topics
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# @return [Array<String>] names of already existing topics
|
|
141
|
-
def existing_topics_names
|
|
142
|
-
existing_topics.map { |topic| topic.fetch(:topic_name) }
|
|
143
|
-
end
|
|
144
34
|
end
|
|
145
35
|
end
|
|
146
36
|
end
|