karafka 1.4.13 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (170) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +3 -3
  3. data/.github/workflows/ci.yml +85 -30
  4. data/.ruby-version +1 -1
  5. data/CHANGELOG.md +268 -7
  6. data/CONTRIBUTING.md +10 -19
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +44 -87
  9. data/LICENSE +17 -0
  10. data/LICENSE-COMM +89 -0
  11. data/LICENSE-LGPL +165 -0
  12. data/README.md +44 -48
  13. data/bin/benchmarks +85 -0
  14. data/bin/create_token +22 -0
  15. data/bin/integrations +237 -0
  16. data/bin/karafka +4 -0
  17. data/bin/scenario +29 -0
  18. data/bin/stress_many +13 -0
  19. data/bin/stress_one +13 -0
  20. data/bin/wait_for_kafka +20 -0
  21. data/certs/karafka-pro.pem +11 -0
  22. data/config/errors.yml +55 -40
  23. data/docker-compose.yml +39 -3
  24. data/karafka.gemspec +11 -17
  25. data/lib/active_job/karafka.rb +21 -0
  26. data/lib/active_job/queue_adapters/karafka_adapter.rb +26 -0
  27. data/lib/karafka/active_job/consumer.rb +26 -0
  28. data/lib/karafka/active_job/dispatcher.rb +38 -0
  29. data/lib/karafka/active_job/job_extensions.rb +34 -0
  30. data/lib/karafka/active_job/job_options_contract.rb +21 -0
  31. data/lib/karafka/active_job/routing/extensions.rb +31 -0
  32. data/lib/karafka/app.rb +15 -20
  33. data/lib/karafka/base_consumer.rb +181 -31
  34. data/lib/karafka/cli/base.rb +4 -4
  35. data/lib/karafka/cli/info.rb +43 -9
  36. data/lib/karafka/cli/install.rb +19 -10
  37. data/lib/karafka/cli/server.rb +17 -42
  38. data/lib/karafka/cli.rb +4 -11
  39. data/lib/karafka/connection/client.rb +385 -90
  40. data/lib/karafka/connection/listener.rb +246 -38
  41. data/lib/karafka/connection/listeners_batch.rb +24 -0
  42. data/lib/karafka/connection/messages_buffer.rb +84 -0
  43. data/lib/karafka/connection/pauses_manager.rb +46 -0
  44. data/lib/karafka/connection/raw_messages_buffer.rb +101 -0
  45. data/lib/karafka/connection/rebalance_manager.rb +78 -0
  46. data/lib/karafka/contracts/base.rb +17 -0
  47. data/lib/karafka/contracts/config.rb +88 -11
  48. data/lib/karafka/contracts/consumer_group.rb +21 -189
  49. data/lib/karafka/contracts/consumer_group_topic.rb +34 -11
  50. data/lib/karafka/contracts/server_cli_options.rb +19 -18
  51. data/lib/karafka/contracts.rb +1 -1
  52. data/lib/karafka/env.rb +46 -0
  53. data/lib/karafka/errors.rb +21 -21
  54. data/lib/karafka/helpers/async.rb +33 -0
  55. data/lib/karafka/helpers/colorize.rb +20 -0
  56. data/lib/karafka/helpers/multi_delegator.rb +2 -2
  57. data/lib/karafka/instrumentation/callbacks/error.rb +40 -0
  58. data/lib/karafka/instrumentation/callbacks/statistics.rb +41 -0
  59. data/lib/karafka/instrumentation/logger_listener.rb +164 -0
  60. data/lib/karafka/instrumentation/monitor.rb +13 -61
  61. data/lib/karafka/instrumentation/notifications.rb +52 -0
  62. data/lib/karafka/instrumentation/proctitle_listener.rb +3 -3
  63. data/lib/karafka/instrumentation/vendors/datadog/dashboard.json +1 -0
  64. data/lib/karafka/instrumentation/vendors/datadog/listener.rb +232 -0
  65. data/lib/karafka/instrumentation.rb +21 -0
  66. data/lib/karafka/licenser.rb +75 -0
  67. data/lib/karafka/messages/batch_metadata.rb +45 -0
  68. data/lib/karafka/messages/builders/batch_metadata.rb +40 -0
  69. data/lib/karafka/messages/builders/message.rb +39 -0
  70. data/lib/karafka/messages/builders/messages.rb +32 -0
  71. data/lib/karafka/{params/params.rb → messages/message.rb} +7 -12
  72. data/lib/karafka/messages/messages.rb +64 -0
  73. data/lib/karafka/{params → messages}/metadata.rb +4 -6
  74. data/lib/karafka/messages/seek.rb +9 -0
  75. data/lib/karafka/patches/rdkafka/consumer.rb +22 -0
  76. data/lib/karafka/pro/active_job/consumer.rb +46 -0
  77. data/lib/karafka/pro/active_job/dispatcher.rb +61 -0
  78. data/lib/karafka/pro/active_job/job_options_contract.rb +32 -0
  79. data/lib/karafka/pro/base_consumer.rb +82 -0
  80. data/lib/karafka/pro/contracts/base.rb +21 -0
  81. data/lib/karafka/pro/contracts/consumer_group.rb +34 -0
  82. data/lib/karafka/pro/contracts/consumer_group_topic.rb +33 -0
  83. data/lib/karafka/pro/loader.rb +76 -0
  84. data/lib/karafka/pro/performance_tracker.rb +80 -0
  85. data/lib/karafka/pro/processing/coordinator.rb +72 -0
  86. data/lib/karafka/pro/processing/jobs/consume_non_blocking.rb +37 -0
  87. data/lib/karafka/pro/processing/jobs_builder.rb +32 -0
  88. data/lib/karafka/pro/processing/partitioner.rb +60 -0
  89. data/lib/karafka/pro/processing/scheduler.rb +56 -0
  90. data/lib/karafka/pro/routing/builder_extensions.rb +30 -0
  91. data/lib/karafka/pro/routing/topic_extensions.rb +38 -0
  92. data/lib/karafka/pro.rb +13 -0
  93. data/lib/karafka/process.rb +1 -0
  94. data/lib/karafka/processing/coordinator.rb +88 -0
  95. data/lib/karafka/processing/coordinators_buffer.rb +54 -0
  96. data/lib/karafka/processing/executor.rb +118 -0
  97. data/lib/karafka/processing/executors_buffer.rb +88 -0
  98. data/lib/karafka/processing/jobs/base.rb +51 -0
  99. data/lib/karafka/processing/jobs/consume.rb +42 -0
  100. data/lib/karafka/processing/jobs/revoked.rb +22 -0
  101. data/lib/karafka/processing/jobs/shutdown.rb +23 -0
  102. data/lib/karafka/processing/jobs_builder.rb +29 -0
  103. data/lib/karafka/processing/jobs_queue.rb +144 -0
  104. data/lib/karafka/processing/partitioner.rb +22 -0
  105. data/lib/karafka/processing/result.rb +29 -0
  106. data/lib/karafka/processing/scheduler.rb +22 -0
  107. data/lib/karafka/processing/worker.rb +88 -0
  108. data/lib/karafka/processing/workers_batch.rb +27 -0
  109. data/lib/karafka/railtie.rb +113 -0
  110. data/lib/karafka/routing/builder.rb +15 -24
  111. data/lib/karafka/routing/consumer_group.rb +11 -19
  112. data/lib/karafka/routing/consumer_mapper.rb +1 -2
  113. data/lib/karafka/routing/router.rb +1 -1
  114. data/lib/karafka/routing/subscription_group.rb +53 -0
  115. data/lib/karafka/routing/subscription_groups_builder.rb +53 -0
  116. data/lib/karafka/routing/topic.rb +61 -24
  117. data/lib/karafka/routing/topics.rb +38 -0
  118. data/lib/karafka/runner.rb +51 -0
  119. data/lib/karafka/serialization/json/deserializer.rb +6 -15
  120. data/lib/karafka/server.rb +67 -26
  121. data/lib/karafka/setup/config.rb +147 -175
  122. data/lib/karafka/status.rb +14 -5
  123. data/lib/karafka/templates/example_consumer.rb.erb +16 -0
  124. data/lib/karafka/templates/karafka.rb.erb +15 -51
  125. data/lib/karafka/time_trackers/base.rb +19 -0
  126. data/lib/karafka/time_trackers/pause.rb +92 -0
  127. data/lib/karafka/time_trackers/poll.rb +65 -0
  128. data/lib/karafka/version.rb +1 -1
  129. data/lib/karafka.rb +38 -17
  130. data.tar.gz.sig +0 -0
  131. metadata +118 -120
  132. metadata.gz.sig +0 -0
  133. data/MIT-LICENCE +0 -18
  134. data/lib/karafka/assignment_strategies/round_robin.rb +0 -13
  135. data/lib/karafka/attributes_map.rb +0 -63
  136. data/lib/karafka/backends/inline.rb +0 -16
  137. data/lib/karafka/base_responder.rb +0 -226
  138. data/lib/karafka/cli/flow.rb +0 -48
  139. data/lib/karafka/cli/missingno.rb +0 -19
  140. data/lib/karafka/code_reloader.rb +0 -67
  141. data/lib/karafka/connection/api_adapter.rb +0 -158
  142. data/lib/karafka/connection/batch_delegator.rb +0 -55
  143. data/lib/karafka/connection/builder.rb +0 -23
  144. data/lib/karafka/connection/message_delegator.rb +0 -36
  145. data/lib/karafka/consumers/batch_metadata.rb +0 -10
  146. data/lib/karafka/consumers/callbacks.rb +0 -71
  147. data/lib/karafka/consumers/includer.rb +0 -64
  148. data/lib/karafka/consumers/responders.rb +0 -24
  149. data/lib/karafka/consumers/single_params.rb +0 -15
  150. data/lib/karafka/contracts/responder_usage.rb +0 -54
  151. data/lib/karafka/fetcher.rb +0 -42
  152. data/lib/karafka/helpers/class_matcher.rb +0 -88
  153. data/lib/karafka/helpers/config_retriever.rb +0 -46
  154. data/lib/karafka/helpers/inflector.rb +0 -26
  155. data/lib/karafka/instrumentation/stdout_listener.rb +0 -140
  156. data/lib/karafka/params/batch_metadata.rb +0 -26
  157. data/lib/karafka/params/builders/batch_metadata.rb +0 -30
  158. data/lib/karafka/params/builders/params.rb +0 -38
  159. data/lib/karafka/params/builders/params_batch.rb +0 -25
  160. data/lib/karafka/params/params_batch.rb +0 -60
  161. data/lib/karafka/patches/ruby_kafka.rb +0 -47
  162. data/lib/karafka/persistence/client.rb +0 -29
  163. data/lib/karafka/persistence/consumers.rb +0 -45
  164. data/lib/karafka/persistence/topics.rb +0 -48
  165. data/lib/karafka/responders/builder.rb +0 -36
  166. data/lib/karafka/responders/topic.rb +0 -55
  167. data/lib/karafka/routing/topic_mapper.rb +0 -53
  168. data/lib/karafka/serialization/json/serializer.rb +0 -31
  169. data/lib/karafka/setup/configurators/water_drop.rb +0 -36
  170. data/lib/karafka/templates/application_responder.rb.erb +0 -11
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Processing
5
+ # Workers are used to run jobs in separate threads.
6
+ # Workers are the main processing units of the Karafka framework.
7
+ #
8
+ # Each job runs in three stages:
9
+ # - prepare - here we can run any code that we would need to run blocking before we allow
10
+ # the job to run fully async (non blocking). This will always run in a blocking
11
+ # way and can be used to make sure all the resources and external dependencies
12
+ # are satisfied before going async.
13
+ #
14
+ # - call - actual processing logic that can run sync or async
15
+ #
16
+ # - teardown - it should include any code that we want to run after we executed the user
17
+ # code. This can be used to unlock certain resources or do other things that are
18
+ # not user code but need to run after user code base is executed.
19
+ class Worker
20
+ include Helpers::Async
21
+
22
+ # @return [String] id of this worker
23
+ attr_reader :id
24
+
25
+ # @param jobs_queue [JobsQueue]
26
+ # @return [Worker]
27
+ def initialize(jobs_queue)
28
+ @id = SecureRandom.uuid
29
+ @jobs_queue = jobs_queue
30
+ end
31
+
32
+ private
33
+
34
+ # Runs processing of jobs in a loop
35
+ # Stops when queue is closed.
36
+ def call
37
+ loop { break unless process }
38
+ end
39
+
40
+ # Fetches a single job, processes it and marks as completed.
41
+ #
42
+ # @note We do not have error handling here, as no errors should propagate this far. If they
43
+ # do, it is a critical error and should bubble up.
44
+ #
45
+ # @note Upon closing the jobs queue, worker will close it's thread
46
+ def process
47
+ job = @jobs_queue.pop
48
+
49
+ if job
50
+ instrument_details = { caller: self, job: job, jobs_queue: @jobs_queue }
51
+
52
+ Karafka.monitor.instrument('worker.process', instrument_details)
53
+
54
+ Karafka.monitor.instrument('worker.processed', instrument_details) do
55
+ job.before_call
56
+
57
+ # If a job is marked as non blocking, we can run a tick in the job queue and if there
58
+ # are no other blocking factors, the job queue will be unlocked.
59
+ # If this does not run, all the things will be blocking and job queue won't allow to
60
+ # pass it until done.
61
+ @jobs_queue.tick(job.group_id) if job.non_blocking?
62
+
63
+ job.call
64
+
65
+ job.after_call
66
+
67
+ true
68
+ end
69
+ else
70
+ false
71
+ end
72
+ # We signal critical exceptions, notify and do not allow worker to fail
73
+ # rubocop:disable Lint/RescueException
74
+ rescue Exception => e
75
+ # rubocop:enable Lint/RescueException
76
+ Karafka.monitor.instrument(
77
+ 'error.occurred',
78
+ caller: self,
79
+ error: e,
80
+ type: 'worker.process.error'
81
+ )
82
+ ensure
83
+ # job can be nil when the queue is being closed
84
+ @jobs_queue.complete(job) if job
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Processing
5
+ # Abstraction layer around workers batch.
6
+ class WorkersBatch
7
+ include Enumerable
8
+
9
+ # @param jobs_queue [JobsQueue]
10
+ # @return [WorkersBatch]
11
+ def initialize(jobs_queue)
12
+ @batch = Array.new(App.config.concurrency) { Processing::Worker.new(jobs_queue) }
13
+ end
14
+
15
+ # Iterates over available workers and yields each worker
16
+ # @param block [Proc] block we want to run
17
+ def each(&block)
18
+ @batch.each(&block)
19
+ end
20
+
21
+ # @return [Integer] number of workers in the batch
22
+ def size
23
+ @batch.size
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file contains Railtie for auto-configuration
4
+
5
+ rails = false
6
+
7
+ begin
8
+ require 'rails'
9
+
10
+ rails = true
11
+ rescue LoadError
12
+ # Without defining this in any way, Zeitwerk ain't happy so we do it that way
13
+ module Karafka
14
+ class Railtie
15
+ end
16
+ end
17
+ end
18
+
19
+ if rails
20
+ # Load Karafka
21
+ require 'karafka'
22
+
23
+ # Load ActiveJob adapter
24
+ require 'active_job/karafka'
25
+
26
+ # Setup env if configured (may be configured later by .net, etc)
27
+ ENV['KARAFKA_ENV'] ||= ENV['RAILS_ENV'] if ENV.key?('RAILS_ENV')
28
+
29
+ module Karafka
30
+ # Railtie for setting up Rails integration
31
+ class Railtie < Rails::Railtie
32
+ railtie_name :karafka
33
+
34
+ initializer 'karafka.active_job_integration' do
35
+ ActiveSupport.on_load(:active_job) do
36
+ # Extend ActiveJob with some Karafka specific ActiveJob magic
37
+ extend ::Karafka::ActiveJob::JobExtensions
38
+ end
39
+ end
40
+
41
+ # This lines will make Karafka print to stdout like puma or unicorn when we run karafka
42
+ # server + will support code reloading with each fetched loop. We do it only for karafka
43
+ # based commands as Rails processes and console will have it enabled already
44
+ initializer 'karafka.configure_rails_logger' do
45
+ # Make Karafka use Rails logger
46
+ ::Karafka::App.config.logger = Rails.logger
47
+
48
+ next unless Rails.env.development?
49
+ next unless ENV.key?('KARAFKA_CLI')
50
+
51
+ Rails.logger.extend(
52
+ ActiveSupport::Logger.broadcast(
53
+ ActiveSupport::Logger.new($stdout)
54
+ )
55
+ )
56
+ end
57
+
58
+ initializer 'karafka.configure_rails_auto_load_paths' do |app|
59
+ # Consumers should autoload by default in the Rails app so they are visible
60
+ app.config.autoload_paths += %w[app/consumers]
61
+ end
62
+
63
+ initializer 'karafka.configure_rails_code_reloader' do
64
+ # There are components that won't work with older Rails version, so we check it and
65
+ # provide a failover
66
+ rails6plus = Rails.gem_version >= Gem::Version.new('6.0.0')
67
+
68
+ next unless Rails.env.development?
69
+ next unless ENV.key?('KARAFKA_CLI')
70
+ next unless rails6plus
71
+
72
+ # We can have many listeners, but it does not matter in which we will reload the code
73
+ # as long as all the consumers will be re-created as Rails reload is thread-safe
74
+ ::Karafka::App.monitor.subscribe('connection.listener.fetch_loop') do
75
+ # Reload code each time there is a change in the code
76
+ next unless Rails.application.reloaders.any?(&:updated?)
77
+
78
+ Rails.application.reloader.reload!
79
+ end
80
+ end
81
+
82
+ initializer 'karafka.require_karafka_boot_file' do |app|
83
+ rails6plus = Rails.gem_version >= Gem::Version.new('6.0.0')
84
+
85
+ # If the boot file location is set to "false", we should not raise an exception and we
86
+ # should just not load karafka stuff. Setting this explicitly to false indicates, that
87
+ # karafka is part of the supply chain but it is not a first class citizen of a given
88
+ # system (may be just a dependency of a dependency), thus railtie should not kick in to
89
+ # load the non-existing boot file
90
+ next if Karafka.boot_file.to_s == 'false'
91
+
92
+ karafka_boot_file = Rails.root.join(Karafka.boot_file.to_s).to_s
93
+
94
+ # Provide more comprehensive error for when no boot file
95
+ unless File.exist?(karafka_boot_file)
96
+ raise(Karafka::Errors::MissingBootFileError, karafka_boot_file)
97
+ end
98
+
99
+ if rails6plus
100
+ app.reloader.to_prepare do
101
+ # Load Karafka boot file, so it can be used in Rails server context
102
+ require karafka_boot_file
103
+ end
104
+ else
105
+ # Load Karafka main setup for older Rails versions
106
+ app.config.after_initialize do
107
+ require karafka_boot_file
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -10,14 +10,9 @@ module Karafka
10
10
  # end
11
11
  # end
12
12
  class Builder < Concurrent::Array
13
- # Consumer group consistency checking contract
14
- CONTRACT = Karafka::Contracts::ConsumerGroup.new.freeze
15
-
16
- private_constant :CONTRACT
17
-
18
13
  def initialize
19
- super
20
14
  @draws = Concurrent::Array.new
15
+ super
21
16
  end
22
17
 
23
18
  # Used to draw routes for Karafka
@@ -38,11 +33,7 @@ module Karafka
38
33
  instance_eval(&block)
39
34
 
40
35
  each do |consumer_group|
41
- hashed_group = consumer_group.to_h
42
- validation_result = CONTRACT.call(hashed_group)
43
- next if validation_result.success?
44
-
45
- raise Errors::InvalidConfigurationError, validation_result.errors.to_h
36
+ Contracts::ConsumerGroup.new.validate!(consumer_group.to_h)
46
37
  end
47
38
  end
48
39
 
@@ -59,30 +50,30 @@ module Karafka
59
50
  super
60
51
  end
61
52
 
62
- # Redraws all the routes for the in-process code reloading.
63
- # @note This won't allow registration of new topics without process restart but will trigger
64
- # cache invalidation so all the classes, etc are re-fetched after code reload
65
- def reload
66
- draws = @draws.dup
67
- clear
68
- draws.each { |block| draw(&block) }
69
- end
70
-
71
53
  private
72
54
 
73
55
  # Builds and saves given consumer group
74
56
  # @param group_id [String, Symbol] name for consumer group
75
57
  # @param block [Proc] proc that should be executed in the proxy context
76
58
  def consumer_group(group_id, &block)
77
- consumer_group = ConsumerGroup.new(group_id.to_s)
78
- self << Proxy.new(consumer_group, &block).target
59
+ consumer_group = find { |cg| cg.name == group_id.to_s }
60
+
61
+ if consumer_group
62
+ Proxy.new(consumer_group, &block).target
63
+ else
64
+ consumer_group = ConsumerGroup.new(group_id.to_s)
65
+ self << Proxy.new(consumer_group, &block).target
66
+ end
79
67
  end
80
68
 
69
+ # In case we use simple style of routing, all topics will be assigned to the same consumer
70
+ # group that will be based on the client_id
71
+ #
81
72
  # @param topic_name [String, Symbol] name of a topic from which we want to consumer
82
73
  # @param block [Proc] proc we want to evaluate in the topic context
83
74
  def topic(topic_name, &block)
84
- consumer_group(topic_name) do
85
- topic(topic_name, &block).tap(&:build)
75
+ consumer_group('app') do
76
+ topic(topic_name, &block)
86
77
  end
87
78
  end
88
79
  end
@@ -5,14 +5,10 @@ module Karafka
5
5
  # Object used to describe a single consumer group that is going to subscribe to
6
6
  # given topics
7
7
  # It is a part of Karafka's DSL
8
+ # @note A single consumer group represents Kafka consumer group, but it may not match 1:1 with
9
+ # subscription groups. There can be more subscription groups than consumer groups
8
10
  class ConsumerGroup
9
- extend Helpers::ConfigRetriever
10
-
11
- attr_reader(
12
- :topics,
13
- :id,
14
- :name
15
- )
11
+ attr_reader :id, :topics, :name
16
12
 
17
13
  # @param name [String, Symbol] raw name of this consumer group. Raw means, that it does not
18
14
  # yet have an application client_id namespace, this will be added here by default.
@@ -21,7 +17,7 @@ module Karafka
21
17
  def initialize(name)
22
18
  @name = name
23
19
  @id = Karafka::App.config.consumer_mapper.call(name)
24
- @topics = []
20
+ @topics = Topics.new([])
25
21
  end
26
22
 
27
23
  # @return [Boolean] true if this consumer group should be active in our current process
@@ -35,28 +31,24 @@ module Karafka
35
31
  # @return [Karafka::Routing::Topic] newly built topic instance
36
32
  def topic=(name, &block)
37
33
  topic = Topic.new(name, self)
38
- @topics << Proxy.new(topic, &block).target.tap(&:build)
34
+ @topics << Proxy.new(topic, &block).target
39
35
  @topics.last
40
36
  end
41
37
 
42
- Karafka::AttributesMap.consumer_group.each do |attribute|
43
- config_retriever_for(attribute)
38
+ # @return [Array<Routing::SubscriptionGroup>] all the subscription groups build based on
39
+ # the consumer group topics
40
+ def subscription_groups
41
+ App.config.internal.routing.subscription_groups_builder.call(topics)
44
42
  end
45
43
 
46
44
  # Hashed version of consumer group that can be used for validation purposes
47
45
  # @return [Hash] hash with consumer group attributes including serialized to hash
48
46
  # topics inside of it.
49
47
  def to_h
50
- result = {
48
+ {
51
49
  topics: topics.map(&:to_h),
52
50
  id: id
53
- }
54
-
55
- Karafka::AttributesMap.consumer_group.each do |attribute|
56
- result[attribute] = public_send(attribute)
57
- end
58
-
59
- result
51
+ }.freeze
60
52
  end
61
53
  end
62
54
  end
@@ -26,8 +26,7 @@ module Karafka
26
26
  # @param raw_consumer_group_name [String, Symbol] string or symbolized consumer group name
27
27
  # @return [String] remapped final consumer group name
28
28
  def call(raw_consumer_group_name)
29
- client_name = Karafka::Helpers::Inflector.map(Karafka::App.config.client_id.to_s)
30
- "#{client_name}_#{raw_consumer_group_name}"
29
+ "#{Karafka::App.config.client_id}_#{raw_consumer_group_name}"
31
30
  end
32
31
  end
33
32
  end
@@ -10,7 +10,7 @@ module Karafka
10
10
  # Find a proper topic based on full topic id
11
11
  # @param topic_id [String] proper topic id (already mapped, etc) for which we want to find
12
12
  # routing topic
13
- # @return [Karafka::Routing::Route] proper route details
13
+ # @return [Karafka::Routing::Topic] proper route details
14
14
  # @raise [Karafka::Topic::NonMatchingTopicError] raised if topic name does not match
15
15
  # any route defined by user using routes.draw
16
16
  def find(topic_id)
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ # Object representing a set of single consumer group topics that can be subscribed together
6
+ # with one connection.
7
+ #
8
+ # @note One subscription group will always belong to one consumer group, but one consumer
9
+ # group can have multiple subscription groups.
10
+ class SubscriptionGroup
11
+ attr_reader :id, :topics
12
+
13
+ # @param topics [Karafka::Routing::Topics] all the topics that share the same key settings
14
+ # @return [SubscriptionGroup] built subscription group
15
+ def initialize(topics)
16
+ @id = SecureRandom.uuid
17
+ @topics = topics
18
+ freeze
19
+ end
20
+
21
+ # @return [String] consumer group id
22
+ def consumer_group_id
23
+ kafka[:'group.id']
24
+ end
25
+
26
+ # @return [Integer] max messages fetched in a single go
27
+ def max_messages
28
+ @topics.first.max_messages
29
+ end
30
+
31
+ # @return [Integer] max milliseconds we can wait for incoming messages
32
+ def max_wait_time
33
+ @topics.first.max_wait_time
34
+ end
35
+
36
+ # @return [Hash] kafka settings are a bit special. They are exactly the same for all of the
37
+ # topics but they lack the group.id (unless explicitly) provided. To make it compatible
38
+ # with our routing engine, we inject it before it will go to the consumer
39
+ def kafka
40
+ kafka = @topics.first.kafka.dup
41
+
42
+ kafka[:'client.id'] ||= Karafka::App.config.client_id
43
+ kafka[:'group.id'] ||= @topics.first.consumer_group.id
44
+ kafka[:'auto.offset.reset'] ||= @topics.first.initial_offset
45
+ # Karafka manages the offsets based on the processing state, thus we do not rely on the
46
+ # rdkafka offset auto-storing
47
+ kafka[:'enable.auto.offset.store'] = false
48
+ kafka.freeze
49
+ kafka
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ # rdkafka allows us to group topics subscriptions when they have same settings.
6
+ # This builder groups topics from a single consumer group into subscription groups that can be
7
+ # subscribed with one rdkafka connection.
8
+ # This way we save resources as having several rdkafka consumers under the hood is not the
9
+ # cheapest thing in a bigger system.
10
+ #
11
+ # In general, if we can, we try to subscribe to as many topics with one rdkafka connection as
12
+ # possible, but if not possible, we divide.
13
+ class SubscriptionGroupsBuilder
14
+ # Keys used to build up a hash for subscription groups distribution.
15
+ # In order to be able to use the same rdkafka connection for several topics, those keys need
16
+ # to have same values.
17
+ DISTRIBUTION_KEYS = %i[
18
+ kafka
19
+ max_messages
20
+ max_wait_time
21
+ initial_offset
22
+ ].freeze
23
+
24
+ private_constant :DISTRIBUTION_KEYS
25
+
26
+ # @param topics [Karafka::Routing::Topics] all the topics based on which we want to build
27
+ # subscription groups
28
+ # @return [Array<SubscriptionGroup>] all subscription groups we need in separate threads
29
+ def call(topics)
30
+ topics
31
+ .map { |topic| [checksum(topic), topic] }
32
+ .group_by(&:first)
33
+ .values
34
+ .map { |value| value.map(&:last) }
35
+ .map { |topics_array| Routing::Topics.new(topics_array) }
36
+ .map { |grouped_topics| SubscriptionGroup.new(grouped_topics) }
37
+ end
38
+
39
+ private
40
+
41
+ # @param topic [Karafka::Routing::Topic] topic for which we compute the grouping checksum
42
+ # @return [Integer] checksum that we can use to check if topics have the same set of
43
+ # settings based on which we group
44
+ def checksum(topic)
45
+ accu = {}
46
+
47
+ DISTRIBUTION_KEYS.each { |key| accu[key] = topic.public_send(key) }
48
+
49
+ accu.hash
50
+ end
51
+ end
52
+ end
53
+ end
@@ -2,17 +2,24 @@
2
2
 
3
3
  module Karafka
4
4
  module Routing
5
- # Topic stores all the details on how we should interact with Kafka given topic
5
+ # Topic stores all the details on how we should interact with Kafka given topic.
6
6
  # It belongs to a consumer group as from 0.6 all the topics can work in the same consumer group
7
- # It is a part of Karafka's DSL
7
+ # It is a part of Karafka's DSL.
8
8
  class Topic
9
- extend Helpers::ConfigRetriever
10
- extend Forwardable
9
+ attr_reader :id, :name, :consumer_group
10
+ attr_writer :consumer
11
11
 
12
- attr_reader :id, :consumer_group
13
- attr_accessor :consumer
12
+ # Attributes we can inherit from the root unless they were defined on this level
13
+ INHERITABLE_ATTRIBUTES = %i[
14
+ kafka
15
+ deserializer
16
+ manual_offset_management
17
+ max_messages
18
+ max_wait_time
19
+ initial_offset
20
+ ].freeze
14
21
 
15
- def_delegator :@consumer_group, :batch_fetching
22
+ private_constant :INHERITABLE_ATTRIBUTES
16
23
 
17
24
  # @param [String, Symbol] name of a topic on which we want to listen
18
25
  # @param consumer_group [Karafka::Routing::ConsumerGroup] owning consumer group of this topic
@@ -22,40 +29,70 @@ module Karafka
22
29
  @attributes = {}
23
30
  # @note We use identifier related to the consumer group that owns a topic, because from
24
31
  # Karafka 0.6 we can handle multiple Kafka instances with the same process and we can
25
- # have same topic name across multiple Kafkas
32
+ # have same topic name across multiple consumer groups
26
33
  @id = "#{consumer_group.id}_#{@name}"
27
34
  end
28
35
 
29
- # Initializes default values for all the options that support defaults if their values are
30
- # not yet specified. This is need to be done (cannot be lazy loaded on first use) because
31
- # everywhere except Karafka server command, those would not be initialized on time - for
32
- # example for Sidekiq
33
- def build
34
- Karafka::AttributesMap.topic.each { |attr| send(attr) }
35
- self
36
+ INHERITABLE_ATTRIBUTES.each do |attribute|
37
+ attr_writer attribute
38
+
39
+ define_method attribute do
40
+ current_value = instance_variable_get(:"@#{attribute}")
41
+
42
+ return current_value unless current_value.nil?
43
+
44
+ value = Karafka::App.config.send(attribute)
45
+
46
+ instance_variable_set(:"@#{attribute}", value)
47
+ end
48
+ end
49
+
50
+ # @return [Class] consumer class that we should use
51
+ def consumer
52
+ if Karafka::App.config.consumer_persistence
53
+ # When persistence of consumers is on, no need to reload them
54
+ @consumer
55
+ else
56
+ # In order to support code reload without having to change the topic api, we re-fetch the
57
+ # class of a consumer based on its class name. This will support all the cases where the
58
+ # consumer class is defined with a name. It won't support code reload for anonymous
59
+ # consumer classes, but this is an edge case
60
+ begin
61
+ ::Object.const_get(@consumer.to_s)
62
+ rescue NameError
63
+ # It will only fail if the in case of anonymous classes
64
+ @consumer
65
+ end
66
+ end
36
67
  end
37
68
 
38
- # @return [Class, nil] Class (not an instance) of a responder that should respond from
39
- # consumer back to Kafka (useful for piping data flows)
40
- def responder
41
- @responder ||= Karafka::Responders::Builder.new(consumer).build
69
+ # @return [Class] consumer class that we should use
70
+ # @note This is just an alias to the `#consumer` method. We however want to use it internally
71
+ # instead of referencing the `#consumer`. We use this to indicate that this method returns
72
+ # class and not an instance. In the routing we want to keep the `#consumer Consumer`
73
+ # routing syntax, but for references outside, we should use this one.
74
+ def consumer_class
75
+ consumer
42
76
  end
43
77
 
44
- Karafka::AttributesMap.topic.each do |attribute|
45
- config_retriever_for(attribute)
78
+ # @return [Boolean] true if this topic offset is handled by the end user
79
+ def manual_offset_management?
80
+ manual_offset_management
46
81
  end
47
82
 
48
83
  # @return [Hash] hash with all the topic attributes
49
84
  # @note This is being used when we validate the consumer_group and its topics
50
85
  def to_h
51
- map = Karafka::AttributesMap.topic.map do |attribute|
86
+ map = INHERITABLE_ATTRIBUTES.map do |attribute|
52
87
  [attribute, public_send(attribute)]
53
88
  end
54
89
 
55
90
  Hash[map].merge!(
56
91
  id: id,
57
- consumer: consumer
58
- )
92
+ name: name,
93
+ consumer: consumer,
94
+ consumer_group_id: consumer_group.id
95
+ ).freeze
59
96
  end
60
97
  end
61
98
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # frozen_string_literal: true
4
+
5
+ module Karafka
6
+ module Routing
7
+ # Abstraction layer on top of groups of topics
8
+ class Topics
9
+ include Enumerable
10
+ extend Forwardable
11
+
12
+ def_delegators :@accumulator, :[], :size, :empty?, :last, :<<
13
+
14
+ # @param topics_array [Array<Karafka::Routing::Topic>] array with topics
15
+ def initialize(topics_array)
16
+ @accumulator = topics_array.dup
17
+ end
18
+
19
+ # Yields each topic
20
+ #
21
+ # @param [Proc] block we want to yield with on each topic
22
+ def each(&block)
23
+ @accumulator.each(&block)
24
+ end
25
+
26
+ # Finds topic by its name
27
+ #
28
+ # @param topic_name [String] topic name
29
+ # @return [Karafka::Routing::Topic]
30
+ # @raise [Karafka::Errors::TopicNotFoundError] this should never happen. If you see it,
31
+ # please create an issue.
32
+ def find(topic_name)
33
+ @accumulator.find { |topic| topic.name == topic_name } ||
34
+ raise(Karafka::Errors::TopicNotFoundError, topic_name)
35
+ end
36
+ end
37
+ end
38
+ end