karafka 2.3.3 → 2.4.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/workflows/ci.yml +12 -38
  4. data/CHANGELOG.md +57 -0
  5. data/Gemfile +6 -3
  6. data/Gemfile.lock +25 -23
  7. data/bin/integrations +1 -1
  8. data/config/locales/errors.yml +21 -2
  9. data/config/locales/pro_errors.yml +16 -1
  10. data/karafka.gemspec +4 -2
  11. data/lib/active_job/queue_adapters/karafka_adapter.rb +2 -0
  12. data/lib/karafka/admin/configs/config.rb +81 -0
  13. data/lib/karafka/admin/configs/resource.rb +88 -0
  14. data/lib/karafka/admin/configs.rb +103 -0
  15. data/lib/karafka/admin.rb +200 -89
  16. data/lib/karafka/base_consumer.rb +2 -2
  17. data/lib/karafka/cli/info.rb +9 -7
  18. data/lib/karafka/cli/server.rb +7 -7
  19. data/lib/karafka/cli/topics/align.rb +109 -0
  20. data/lib/karafka/cli/topics/base.rb +66 -0
  21. data/lib/karafka/cli/topics/create.rb +35 -0
  22. data/lib/karafka/cli/topics/delete.rb +30 -0
  23. data/lib/karafka/cli/topics/migrate.rb +31 -0
  24. data/lib/karafka/cli/topics/plan.rb +169 -0
  25. data/lib/karafka/cli/topics/repartition.rb +41 -0
  26. data/lib/karafka/cli/topics/reset.rb +18 -0
  27. data/lib/karafka/cli/topics.rb +13 -123
  28. data/lib/karafka/connection/client.rb +55 -37
  29. data/lib/karafka/connection/listener.rb +22 -17
  30. data/lib/karafka/connection/proxy.rb +93 -4
  31. data/lib/karafka/connection/status.rb +14 -2
  32. data/lib/karafka/contracts/config.rb +14 -1
  33. data/lib/karafka/contracts/topic.rb +1 -1
  34. data/lib/karafka/deserializers/headers.rb +15 -0
  35. data/lib/karafka/deserializers/key.rb +15 -0
  36. data/lib/karafka/deserializers/payload.rb +16 -0
  37. data/lib/karafka/embedded.rb +2 -0
  38. data/lib/karafka/helpers/async.rb +5 -2
  39. data/lib/karafka/helpers/colorize.rb +6 -0
  40. data/lib/karafka/instrumentation/callbacks/oauthbearer_token_refresh.rb +29 -0
  41. data/lib/karafka/instrumentation/logger_listener.rb +23 -3
  42. data/lib/karafka/instrumentation/notifications.rb +10 -0
  43. data/lib/karafka/instrumentation/vendors/appsignal/client.rb +16 -2
  44. data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +20 -0
  45. data/lib/karafka/messages/batch_metadata.rb +1 -1
  46. data/lib/karafka/messages/builders/batch_metadata.rb +1 -1
  47. data/lib/karafka/messages/builders/message.rb +10 -6
  48. data/lib/karafka/messages/message.rb +2 -1
  49. data/lib/karafka/messages/metadata.rb +20 -4
  50. data/lib/karafka/messages/parser.rb +1 -1
  51. data/lib/karafka/pro/base_consumer.rb +12 -23
  52. data/lib/karafka/pro/encryption/cipher.rb +7 -3
  53. data/lib/karafka/pro/encryption/contracts/config.rb +1 -0
  54. data/lib/karafka/pro/encryption/errors.rb +4 -1
  55. data/lib/karafka/pro/encryption/messages/middleware.rb +13 -11
  56. data/lib/karafka/pro/encryption/messages/parser.rb +22 -20
  57. data/lib/karafka/pro/encryption/setup/config.rb +5 -0
  58. data/lib/karafka/pro/iterator/expander.rb +2 -1
  59. data/lib/karafka/pro/iterator/tpl_builder.rb +38 -0
  60. data/lib/karafka/pro/iterator.rb +28 -2
  61. data/lib/karafka/pro/loader.rb +3 -0
  62. data/lib/karafka/pro/processing/coordinator.rb +15 -2
  63. data/lib/karafka/pro/processing/expansions_selector.rb +2 -0
  64. data/lib/karafka/pro/processing/jobs_queue.rb +122 -5
  65. data/lib/karafka/pro/processing/periodic_job/consumer.rb +67 -0
  66. data/lib/karafka/pro/processing/piping/consumer.rb +126 -0
  67. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom.rb +1 -1
  68. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom_vp.rb +1 -1
  69. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom.rb +1 -1
  70. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom_vp.rb +1 -1
  71. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom.rb +1 -1
  72. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom_vp.rb +1 -1
  73. data/lib/karafka/pro/processing/strategies/aj/dlq_mom.rb +1 -1
  74. data/lib/karafka/pro/processing/strategies/aj/dlq_mom_vp.rb +1 -1
  75. data/lib/karafka/pro/processing/strategies/aj/lrj_mom_vp.rb +2 -0
  76. data/lib/karafka/pro/processing/strategies/default.rb +5 -1
  77. data/lib/karafka/pro/processing/strategies/dlq/default.rb +21 -5
  78. data/lib/karafka/pro/processing/strategies/lrj/default.rb +2 -0
  79. data/lib/karafka/pro/processing/strategies/lrj/mom.rb +2 -0
  80. data/lib/karafka/pro/processing/subscription_groups_coordinator.rb +52 -0
  81. data/lib/karafka/pro/routing/features/direct_assignments/config.rb +27 -0
  82. data/lib/karafka/pro/routing/features/direct_assignments/contracts/consumer_group.rb +53 -0
  83. data/lib/karafka/pro/routing/features/direct_assignments/contracts/topic.rb +108 -0
  84. data/lib/karafka/pro/routing/features/direct_assignments/subscription_group.rb +77 -0
  85. data/lib/karafka/pro/routing/features/direct_assignments/topic.rb +69 -0
  86. data/lib/karafka/pro/routing/features/direct_assignments.rb +25 -0
  87. data/lib/karafka/pro/routing/features/patterns/builder.rb +1 -1
  88. data/lib/karafka/pro/routing/features/swarm/contracts/routing.rb +76 -0
  89. data/lib/karafka/pro/routing/features/swarm/contracts/topic.rb +16 -5
  90. data/lib/karafka/pro/routing/features/swarm/topic.rb +25 -2
  91. data/lib/karafka/pro/routing/features/swarm.rb +11 -0
  92. data/lib/karafka/pro/swarm/liveness_listener.rb +20 -0
  93. data/lib/karafka/processing/coordinator.rb +17 -8
  94. data/lib/karafka/processing/coordinators_buffer.rb +5 -2
  95. data/lib/karafka/processing/executor.rb +6 -2
  96. data/lib/karafka/processing/executors_buffer.rb +5 -2
  97. data/lib/karafka/processing/jobs_queue.rb +9 -4
  98. data/lib/karafka/processing/strategies/aj_dlq_mom.rb +1 -1
  99. data/lib/karafka/processing/strategies/default.rb +7 -1
  100. data/lib/karafka/processing/strategies/dlq.rb +17 -2
  101. data/lib/karafka/processing/workers_batch.rb +4 -1
  102. data/lib/karafka/routing/builder.rb +6 -2
  103. data/lib/karafka/routing/consumer_group.rb +2 -1
  104. data/lib/karafka/routing/features/dead_letter_queue/config.rb +5 -0
  105. data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +8 -0
  106. data/lib/karafka/routing/features/dead_letter_queue/topic.rb +10 -2
  107. data/lib/karafka/routing/features/deserializers/config.rb +18 -0
  108. data/lib/karafka/routing/features/deserializers/contracts/topic.rb +31 -0
  109. data/lib/karafka/routing/features/deserializers/topic.rb +51 -0
  110. data/lib/karafka/routing/features/deserializers.rb +11 -0
  111. data/lib/karafka/routing/proxy.rb +9 -14
  112. data/lib/karafka/routing/router.rb +11 -2
  113. data/lib/karafka/routing/subscription_group.rb +9 -1
  114. data/lib/karafka/routing/topic.rb +0 -1
  115. data/lib/karafka/runner.rb +1 -1
  116. data/lib/karafka/setup/config.rb +50 -9
  117. data/lib/karafka/status.rb +7 -8
  118. data/lib/karafka/swarm/supervisor.rb +16 -2
  119. data/lib/karafka/templates/karafka.rb.erb +28 -1
  120. data/lib/karafka/version.rb +1 -1
  121. data.tar.gz.sig +0 -0
  122. metadata +38 -12
  123. metadata.gz.sig +0 -0
  124. data/lib/karafka/routing/consumer_mapper.rb +0 -23
  125. data/lib/karafka/serialization/json/deserializer.rb +0 -19
  126. 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
@@ -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 (create, delete, reset, repartition)'
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
- create
17
+ Topics::Create.new.call
15
18
  when 'delete'
16
- delete
19
+ Topics::Delete.new.call
17
20
  when 'reset'
18
- reset
21
+ Topics::Reset.new.call
19
22
  when 'repartition'
20
- repartition
23
+ Topics::Repartition.new.call
21
24
  when 'migrate'
22
- migrate
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