karafka 1.4.13 → 2.0.0

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 (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