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.
Files changed (127) 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 +59 -0
  5. data/Gemfile +6 -3
  6. data/Gemfile.lock +29 -27
  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 +211 -90
  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/constraints.rb +3 -3
  33. data/lib/karafka/contracts/config.rb +14 -1
  34. data/lib/karafka/contracts/topic.rb +1 -1
  35. data/lib/karafka/deserializers/headers.rb +15 -0
  36. data/lib/karafka/deserializers/key.rb +15 -0
  37. data/lib/karafka/deserializers/payload.rb +16 -0
  38. data/lib/karafka/embedded.rb +2 -0
  39. data/lib/karafka/helpers/async.rb +5 -2
  40. data/lib/karafka/helpers/colorize.rb +6 -0
  41. data/lib/karafka/instrumentation/callbacks/oauthbearer_token_refresh.rb +29 -0
  42. data/lib/karafka/instrumentation/logger_listener.rb +23 -3
  43. data/lib/karafka/instrumentation/notifications.rb +10 -0
  44. data/lib/karafka/instrumentation/vendors/appsignal/client.rb +16 -2
  45. data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +20 -0
  46. data/lib/karafka/messages/batch_metadata.rb +1 -1
  47. data/lib/karafka/messages/builders/batch_metadata.rb +1 -1
  48. data/lib/karafka/messages/builders/message.rb +10 -6
  49. data/lib/karafka/messages/message.rb +2 -1
  50. data/lib/karafka/messages/metadata.rb +20 -4
  51. data/lib/karafka/messages/parser.rb +1 -1
  52. data/lib/karafka/pro/base_consumer.rb +12 -23
  53. data/lib/karafka/pro/encryption/cipher.rb +7 -3
  54. data/lib/karafka/pro/encryption/contracts/config.rb +1 -0
  55. data/lib/karafka/pro/encryption/errors.rb +4 -1
  56. data/lib/karafka/pro/encryption/messages/middleware.rb +13 -11
  57. data/lib/karafka/pro/encryption/messages/parser.rb +22 -20
  58. data/lib/karafka/pro/encryption/setup/config.rb +5 -0
  59. data/lib/karafka/pro/iterator/expander.rb +2 -1
  60. data/lib/karafka/pro/iterator/tpl_builder.rb +38 -0
  61. data/lib/karafka/pro/iterator.rb +28 -2
  62. data/lib/karafka/pro/loader.rb +3 -0
  63. data/lib/karafka/pro/processing/coordinator.rb +15 -2
  64. data/lib/karafka/pro/processing/expansions_selector.rb +2 -0
  65. data/lib/karafka/pro/processing/jobs_queue.rb +122 -5
  66. data/lib/karafka/pro/processing/periodic_job/consumer.rb +67 -0
  67. data/lib/karafka/pro/processing/piping/consumer.rb +126 -0
  68. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom.rb +1 -1
  69. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom_vp.rb +1 -1
  70. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom.rb +1 -1
  71. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom_vp.rb +1 -1
  72. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom.rb +1 -1
  73. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom_vp.rb +1 -1
  74. data/lib/karafka/pro/processing/strategies/aj/dlq_mom.rb +1 -1
  75. data/lib/karafka/pro/processing/strategies/aj/dlq_mom_vp.rb +1 -1
  76. data/lib/karafka/pro/processing/strategies/aj/lrj_mom_vp.rb +2 -0
  77. data/lib/karafka/pro/processing/strategies/default.rb +5 -1
  78. data/lib/karafka/pro/processing/strategies/dlq/default.rb +21 -5
  79. data/lib/karafka/pro/processing/strategies/lrj/default.rb +2 -0
  80. data/lib/karafka/pro/processing/strategies/lrj/mom.rb +2 -0
  81. data/lib/karafka/pro/processing/subscription_groups_coordinator.rb +52 -0
  82. data/lib/karafka/pro/routing/features/direct_assignments/config.rb +27 -0
  83. data/lib/karafka/pro/routing/features/direct_assignments/contracts/consumer_group.rb +53 -0
  84. data/lib/karafka/pro/routing/features/direct_assignments/contracts/topic.rb +108 -0
  85. data/lib/karafka/pro/routing/features/direct_assignments/subscription_group.rb +77 -0
  86. data/lib/karafka/pro/routing/features/direct_assignments/topic.rb +69 -0
  87. data/lib/karafka/pro/routing/features/direct_assignments.rb +25 -0
  88. data/lib/karafka/pro/routing/features/patterns/builder.rb +1 -1
  89. data/lib/karafka/pro/routing/features/swarm/contracts/routing.rb +76 -0
  90. data/lib/karafka/pro/routing/features/swarm/contracts/topic.rb +16 -5
  91. data/lib/karafka/pro/routing/features/swarm/topic.rb +25 -2
  92. data/lib/karafka/pro/routing/features/swarm.rb +11 -0
  93. data/lib/karafka/pro/swarm/liveness_listener.rb +20 -0
  94. data/lib/karafka/processing/coordinator.rb +17 -8
  95. data/lib/karafka/processing/coordinators_buffer.rb +5 -2
  96. data/lib/karafka/processing/executor.rb +6 -2
  97. data/lib/karafka/processing/executors_buffer.rb +5 -2
  98. data/lib/karafka/processing/jobs_queue.rb +9 -4
  99. data/lib/karafka/processing/strategies/aj_dlq_mom.rb +1 -1
  100. data/lib/karafka/processing/strategies/default.rb +7 -1
  101. data/lib/karafka/processing/strategies/dlq.rb +17 -2
  102. data/lib/karafka/processing/workers_batch.rb +4 -1
  103. data/lib/karafka/routing/builder.rb +6 -2
  104. data/lib/karafka/routing/consumer_group.rb +2 -1
  105. data/lib/karafka/routing/features/dead_letter_queue/config.rb +5 -0
  106. data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +8 -0
  107. data/lib/karafka/routing/features/dead_letter_queue/topic.rb +10 -2
  108. data/lib/karafka/routing/features/deserializers/config.rb +18 -0
  109. data/lib/karafka/routing/features/deserializers/contracts/topic.rb +31 -0
  110. data/lib/karafka/routing/features/deserializers/topic.rb +51 -0
  111. data/lib/karafka/routing/features/deserializers.rb +11 -0
  112. data/lib/karafka/routing/proxy.rb +9 -14
  113. data/lib/karafka/routing/router.rb +11 -2
  114. data/lib/karafka/routing/subscription_group.rb +9 -1
  115. data/lib/karafka/routing/topic.rb +0 -1
  116. data/lib/karafka/runner.rb +1 -1
  117. data/lib/karafka/setup/config.rb +50 -9
  118. data/lib/karafka/status.rb +7 -8
  119. data/lib/karafka/swarm/supervisor.rb +16 -2
  120. data/lib/karafka/templates/karafka.rb.erb +28 -1
  121. data/lib/karafka/version.rb +1 -1
  122. data.tar.gz.sig +0 -0
  123. metadata +38 -12
  124. metadata.gz.sig +0 -0
  125. data/lib/karafka/routing/consumer_mapper.rb +0 -23
  126. data/lib/karafka/serialization/json/deserializer.rb +0 -19
  127. 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