karafka 1.2.8 → 1.4.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 (113) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/.coditsu/ci.yml +3 -0
  5. data/.console_irbrc +1 -3
  6. data/.diffend.yml +3 -0
  7. data/.github/FUNDING.yml +3 -0
  8. data/.github/ISSUE_TEMPLATE/bug_report.md +50 -0
  9. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  10. data/.github/workflows/ci.yml +52 -0
  11. data/.gitignore +1 -0
  12. data/.ruby-version +1 -1
  13. data/CHANGELOG.md +134 -14
  14. data/CODE_OF_CONDUCT.md +1 -1
  15. data/CONTRIBUTING.md +1 -1
  16. data/Gemfile +4 -5
  17. data/Gemfile.lock +92 -81
  18. data/README.md +9 -12
  19. data/bin/karafka +1 -1
  20. data/certs/mensfeld.pem +25 -0
  21. data/config/errors.yml +38 -5
  22. data/docker-compose.yml +17 -0
  23. data/karafka.gemspec +18 -17
  24. data/lib/karafka.rb +10 -16
  25. data/lib/karafka/app.rb +14 -6
  26. data/lib/karafka/attributes_map.rb +5 -10
  27. data/lib/karafka/base_consumer.rb +19 -30
  28. data/lib/karafka/base_responder.rb +45 -27
  29. data/lib/karafka/cli.rb +2 -2
  30. data/lib/karafka/cli/console.rb +11 -9
  31. data/lib/karafka/cli/flow.rb +9 -7
  32. data/lib/karafka/cli/info.rb +4 -2
  33. data/lib/karafka/cli/install.rb +30 -6
  34. data/lib/karafka/cli/server.rb +11 -6
  35. data/lib/karafka/code_reloader.rb +67 -0
  36. data/lib/karafka/connection/api_adapter.rb +22 -9
  37. data/lib/karafka/connection/batch_delegator.rb +55 -0
  38. data/lib/karafka/connection/builder.rb +5 -3
  39. data/lib/karafka/connection/client.rb +31 -31
  40. data/lib/karafka/connection/listener.rb +26 -15
  41. data/lib/karafka/connection/message_delegator.rb +36 -0
  42. data/lib/karafka/consumers/batch_metadata.rb +10 -0
  43. data/lib/karafka/consumers/callbacks.rb +32 -15
  44. data/lib/karafka/consumers/includer.rb +31 -18
  45. data/lib/karafka/consumers/responders.rb +2 -2
  46. data/lib/karafka/contracts.rb +10 -0
  47. data/lib/karafka/contracts/config.rb +21 -0
  48. data/lib/karafka/contracts/consumer_group.rb +206 -0
  49. data/lib/karafka/contracts/consumer_group_topic.rb +19 -0
  50. data/lib/karafka/contracts/responder_usage.rb +54 -0
  51. data/lib/karafka/contracts/server_cli_options.rb +31 -0
  52. data/lib/karafka/errors.rb +17 -16
  53. data/lib/karafka/fetcher.rb +28 -30
  54. data/lib/karafka/helpers/class_matcher.rb +12 -2
  55. data/lib/karafka/helpers/config_retriever.rb +1 -1
  56. data/lib/karafka/helpers/inflector.rb +26 -0
  57. data/lib/karafka/helpers/multi_delegator.rb +0 -1
  58. data/lib/karafka/instrumentation/logger.rb +9 -6
  59. data/lib/karafka/instrumentation/monitor.rb +15 -9
  60. data/lib/karafka/instrumentation/proctitle_listener.rb +36 -0
  61. data/lib/karafka/instrumentation/stdout_listener.rb +140 -0
  62. data/lib/karafka/params/batch_metadata.rb +26 -0
  63. data/lib/karafka/params/builders/batch_metadata.rb +30 -0
  64. data/lib/karafka/params/builders/params.rb +38 -0
  65. data/lib/karafka/params/builders/params_batch.rb +25 -0
  66. data/lib/karafka/params/metadata.rb +20 -0
  67. data/lib/karafka/params/params.rb +54 -0
  68. data/lib/karafka/params/params_batch.rb +35 -21
  69. data/lib/karafka/patches/ruby_kafka.rb +21 -8
  70. data/lib/karafka/persistence/client.rb +15 -11
  71. data/lib/karafka/persistence/{consumer.rb → consumers.rb} +20 -13
  72. data/lib/karafka/persistence/topics.rb +48 -0
  73. data/lib/karafka/process.rb +0 -2
  74. data/lib/karafka/responders/builder.rb +1 -1
  75. data/lib/karafka/responders/topic.rb +6 -8
  76. data/lib/karafka/routing/builder.rb +36 -8
  77. data/lib/karafka/routing/consumer_group.rb +1 -1
  78. data/lib/karafka/routing/consumer_mapper.rb +9 -9
  79. data/lib/karafka/routing/proxy.rb +10 -1
  80. data/lib/karafka/routing/topic.rb +5 -3
  81. data/lib/karafka/routing/topic_mapper.rb +16 -18
  82. data/lib/karafka/serialization/json/deserializer.rb +27 -0
  83. data/lib/karafka/serialization/json/serializer.rb +31 -0
  84. data/lib/karafka/server.rb +29 -28
  85. data/lib/karafka/setup/config.rb +67 -37
  86. data/lib/karafka/setup/configurators/water_drop.rb +7 -3
  87. data/lib/karafka/setup/dsl.rb +0 -1
  88. data/lib/karafka/status.rb +7 -3
  89. data/lib/karafka/templates/{application_consumer.rb.example → application_consumer.rb.erb} +2 -1
  90. data/lib/karafka/templates/{application_responder.rb.example → application_responder.rb.erb} +0 -0
  91. data/lib/karafka/templates/karafka.rb.erb +92 -0
  92. data/lib/karafka/version.rb +1 -1
  93. metadata +94 -72
  94. metadata.gz.sig +0 -0
  95. data/.travis.yml +0 -21
  96. data/lib/karafka/callbacks.rb +0 -30
  97. data/lib/karafka/callbacks/config.rb +0 -22
  98. data/lib/karafka/callbacks/dsl.rb +0 -16
  99. data/lib/karafka/connection/delegator.rb +0 -46
  100. data/lib/karafka/instrumentation/listener.rb +0 -112
  101. data/lib/karafka/loader.rb +0 -28
  102. data/lib/karafka/params/dsl.rb +0 -156
  103. data/lib/karafka/parsers/json.rb +0 -38
  104. data/lib/karafka/patches/dry_configurable.rb +0 -35
  105. data/lib/karafka/persistence/topic.rb +0 -29
  106. data/lib/karafka/schemas/config.rb +0 -24
  107. data/lib/karafka/schemas/consumer_group.rb +0 -78
  108. data/lib/karafka/schemas/consumer_group_topic.rb +0 -18
  109. data/lib/karafka/schemas/responder_usage.rb +0 -39
  110. data/lib/karafka/schemas/server_cli_options.rb +0 -43
  111. data/lib/karafka/setup/configurators/base.rb +0 -29
  112. data/lib/karafka/setup/configurators/params.rb +0 -25
  113. data/lib/karafka/templates/karafka.rb.example +0 -54
@@ -5,6 +5,11 @@ module Karafka
5
5
  class Cli < Thor
6
6
  # Server Karafka Cli action
7
7
  class Server < Base
8
+ # Server config settings contract
9
+ CONTRACT = Contracts::ServerCliOptions.new.freeze
10
+
11
+ private_constant :CONTRACT
12
+
8
13
  desc 'Start the Karafka server (short-cut alias: "s")'
9
14
  option aliases: 's'
10
15
  option :daemon, default: false, type: :boolean, aliases: :d
@@ -13,11 +18,10 @@ module Karafka
13
18
 
14
19
  # Start the Karafka server
15
20
  def call
16
- validate!
17
-
18
- puts 'Starting Karafka server'
19
21
  cli.info
20
22
 
23
+ validate!
24
+
21
25
  if cli.options[:daemon]
22
26
  FileUtils.mkdir_p File.dirname(cli.options[:pid])
23
27
  daemonize
@@ -31,7 +35,7 @@ module Karafka
31
35
  # We want to delay the moment in which the pidfile is removed as much as we can,
32
36
  # so instead of removing it after the server stops running, we rely on the gc moment
33
37
  # when this object gets removed (it is a bit later), so it is closer to the actual
34
- # system process end. We do that, so monitoring and deployment tools that rely on pids
38
+ # system process end. We do that, so monitoring and deployment tools that rely on a pid
35
39
  # won't alarm or start new system process up until the current one is finished
36
40
  ObjectSpace.define_finalizer(self, proc { send(:clean) })
37
41
 
@@ -43,9 +47,10 @@ module Karafka
43
47
  # Checks the server cli configuration
44
48
  # options validations in terms of app setup (topics, pid existence, etc)
45
49
  def validate!
46
- result = Schemas::ServerCliOptions.call(cli.options)
50
+ result = CONTRACT.call(cli.options)
47
51
  return if result.success?
48
- raise Errors::InvalidConfiguration, result.errors
52
+
53
+ raise Errors::InvalidConfigurationError, result.errors.to_h
49
54
  end
50
55
 
51
56
  # Detaches current process into background and writes its pidfile
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ # Special type of a listener, that is not an instrumentation one, but one that triggers
5
+ # code reload in the development mode after each fetched batch (or message)
6
+ #
7
+ # Please refer to the development code reload sections for details on the benefits and downsides
8
+ # of the in-process code reloading
9
+ class CodeReloader
10
+ # This mutex is needed as we might have an application that has multiple consumer groups
11
+ # running in separate threads and we should not trigger reload before fully reloading the app
12
+ # in previous thread
13
+ MUTEX = Mutex.new
14
+
15
+ private_constant :MUTEX
16
+
17
+ # @param reloaders [Array<Object>] any code loaders that we use in this app. Whether it is
18
+ # the Rails loader, Zeitwerk or anything else that allows reloading triggering
19
+ # @param block [Proc] yields given block just before reloading. This can be used to hook custom
20
+ # reloading stuff, that ain't reloaders (for example for resetting dry-events registry)
21
+ def initialize(*reloaders, &block)
22
+ @reloaders = reloaders
23
+ @block = block
24
+ end
25
+
26
+ # Binds to the instrumentation events and triggers reload
27
+ # @param _event [Dry::Event] empty dry event
28
+ # @note Since we de-register all the user defined objects and redraw routes, it means that
29
+ # we won't be able to do a multi-batch buffering in the development mode as each of the
30
+ # batches will be buffered on a newly created "per fetch" instance.
31
+ def on_connection_listener_fetch_loop(_event)
32
+ reload
33
+ end
34
+
35
+ private
36
+
37
+ # Triggers reload of both standard and Rails reloaders as well as expires all internals of
38
+ # Karafka, so it can be rediscovered and rebuilt
39
+ def reload
40
+ MUTEX.synchronize do
41
+ if @reloaders[0].respond_to?(:execute)
42
+ reload_with_rails
43
+ else
44
+ reload_without_rails
45
+ end
46
+ end
47
+ end
48
+
49
+ # Rails reloading procedure
50
+ def reload_with_rails
51
+ updatable = @reloaders.select(&:updated?)
52
+
53
+ return if updatable.empty?
54
+
55
+ updatable.each(&:execute)
56
+ @block&.call
57
+ Karafka::App.reload
58
+ end
59
+
60
+ # Zeitwerk and other reloaders
61
+ def reload_without_rails
62
+ @reloaders.each(&:reload)
63
+ @block&.call
64
+ Karafka::App.reload
65
+ end
66
+ end
67
+ end
@@ -14,11 +14,12 @@ module Karafka
14
14
  module ApiAdapter
15
15
  class << self
16
16
  # Builds all the configuration settings for Kafka.new method
17
+ # @param consumer_group [Karafka::Routing::ConsumerGroup] consumer group details
17
18
  # @return [Array<Hash>] Array with all the client arguments including hash with all
18
19
  # the settings required by Kafka.new method
19
20
  # @note We return array, so we can inject any arguments we want, in case of changes in the
20
21
  # raw driver
21
- def client
22
+ def client(consumer_group)
22
23
  # This one is a default that takes all the settings except special
23
24
  # cases defined in the map
24
25
  settings = {
@@ -26,14 +27,17 @@ module Karafka
26
27
  client_id: ::Karafka::App.config.client_id
27
28
  }
28
29
 
29
- kafka_configs.each do |setting_name, setting_value|
30
+ kafka_configs.each_key do |setting_name|
30
31
  # All options for config adapter should be ignored as we're just interested
31
32
  # in what is left, as we want to pass all the options that are "typical"
32
33
  # and not listed in the api_adapter special cases mapping. All the values
33
34
  # from the api_adapter mapping go somewhere else, not to the client directly
34
35
  next if AttributesMap.api_adapter.values.flatten.include?(setting_name)
35
36
 
36
- settings[setting_name] = setting_value
37
+ # Settings for each consumer group are either defined per consumer group or are
38
+ # inherited from the global/general settings level, thus we don't have to fetch them
39
+ # from the kafka settings as they are already on a consumer group level
40
+ settings[setting_name] = consumer_group.public_send(setting_name)
37
41
  end
38
42
 
39
43
  settings_hash = sanitize(settings)
@@ -87,7 +91,11 @@ module Karafka
87
91
  [
88
92
  Karafka::App.config.topic_mapper.outgoing(topic),
89
93
  partition,
90
- { timeout: consumer_group.pause_timeout }
94
+ {
95
+ timeout: consumer_group.pause_timeout,
96
+ max_timeout: consumer_group.pause_max_timeout,
97
+ exponential_backoff: consumer_group.pause_exponential_backoff
98
+ }
91
99
  ]
92
100
  end
93
101
 
@@ -98,13 +106,16 @@ module Karafka
98
106
  # @note When default empty topic mapper is used, no need for any conversion as the
99
107
  # internal and external format are exactly the same
100
108
  def mark_message_as_processed(params)
101
- # Majority of non heroku users don't use custom topic mappers. No need to change
102
- # anything when it is a default mapper that does not change anything
103
- return [params] if Karafka::App.config.topic_mapper == Karafka::Routing::TopicMapper
109
+ # Majority of users don't use custom topic mappers. No need to change anything when it
110
+ # is a default mapper that does not change anything. Only some cloud providers require
111
+ # topics to be remapped
112
+ return [params.metadata] if Karafka::App.config.topic_mapper.is_a?(
113
+ Karafka::Routing::TopicMapper
114
+ )
104
115
 
105
116
  # @note We don't use tap as it is around 13% slower than non-dup version
106
- dupped = params.dup
107
- dupped['topic'] = Karafka::App.config.topic_mapper.outgoing(params.topic)
117
+ dupped = params.metadata.dup
118
+ dupped['topic'] = Karafka::App.config.topic_mapper.outgoing(params.metadata.topic)
108
119
  [dupped]
109
120
  end
110
121
 
@@ -119,9 +130,11 @@ module Karafka
119
130
  kafka_configs.each_key do |setting_name|
120
131
  # Ignore settings that are not related to our namespace
121
132
  next unless AttributesMap.api_adapter[namespace_key].include?(setting_name)
133
+
122
134
  # Ignore settings that are already initialized
123
135
  # In case they are in preexisting settings fetched differently
124
136
  next if preexisting_settings.key?(setting_name)
137
+
125
138
  # Fetch all the settings from a given layer object. Objects can handle the fallback
126
139
  # to the kafka settings, so
127
140
  preexisting_settings[setting_name] = route_layer.send(setting_name)
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Connection
5
+ # Class that delegates processing of batch received messages for which we listen to
6
+ # a proper processor
7
+ module BatchDelegator
8
+ class << self
9
+ # Delegates messages (does something with them)
10
+ # It will either schedule or run a proper processor action for messages
11
+ # @param group_id [String] group_id of a group from which a given message came
12
+ # @param kafka_batch [<Kafka::FetchedBatch>] raw messages fetched batch
13
+ # @note This should be looped to obtain a constant delegating of new messages
14
+ def call(group_id, kafka_batch)
15
+ topic = Persistence::Topics.fetch(group_id, kafka_batch.topic)
16
+ consumer = Persistence::Consumers.fetch(topic, kafka_batch.partition)
17
+
18
+ Karafka.monitor.instrument(
19
+ 'connection.batch_delegator.call',
20
+ caller: self,
21
+ consumer: consumer,
22
+ kafka_batch: kafka_batch
23
+ ) do
24
+ # Due to how ruby-kafka is built, we have the metadata that is stored on the batch
25
+ # level only available for batch consuming
26
+ consumer.batch_metadata = Params::Builders::BatchMetadata.from_kafka_batch(
27
+ kafka_batch,
28
+ topic
29
+ )
30
+
31
+ kafka_messages = kafka_batch.messages
32
+
33
+ # Depending on a case (persisted or not) we might use new consumer instance per
34
+ # each batch, or use the same one for all of them (for implementing buffering, etc.)
35
+ if topic.batch_consuming
36
+ consumer.params_batch = Params::Builders::ParamsBatch.from_kafka_messages(
37
+ kafka_messages,
38
+ topic
39
+ )
40
+ consumer.call
41
+ else
42
+ kafka_messages.each do |kafka_message|
43
+ consumer.params_batch = Params::Builders::ParamsBatch.from_kafka_messages(
44
+ [kafka_message],
45
+ topic
46
+ )
47
+ consumer.call
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -5,10 +5,12 @@ module Karafka
5
5
  # Builder used to construct Kafka client
6
6
  module Builder
7
7
  class << self
8
- # Builds a Kafka::Cient instance that we use to work with Kafka cluster
8
+ # Builds a Kafka::Client instance that we use to work with Kafka cluster
9
+ # @param consumer_group [Karafka::Routing::ConsumerGroup] consumer group for which we want
10
+ # to have a new Kafka client
9
11
  # @return [::Kafka::Client] returns a Kafka client
10
- def call
11
- Kafka.new(*ApiAdapter.client)
12
+ def call(consumer_group)
13
+ Kafka.new(*ApiAdapter.client(consumer_group))
12
14
  end
13
15
  end
14
16
  end
@@ -7,7 +7,13 @@ module Karafka
7
7
  class Client
8
8
  extend Forwardable
9
9
 
10
- def_delegator :kafka_consumer, :seek
10
+ %i[
11
+ seek
12
+ trigger_heartbeat
13
+ trigger_heartbeat!
14
+ ].each do |delegated_method|
15
+ def_delegator :kafka_consumer, delegated_method
16
+ end
11
17
 
12
18
  # Creates a queue consumer client that will pull the data from Kafka
13
19
  # @param consumer_group [Karafka::Routing::ConsumerGroup] consumer group for which
@@ -20,40 +26,32 @@ module Karafka
20
26
  end
21
27
 
22
28
  # Opens connection, gets messages and calls a block for each of the incoming messages
23
- # @yieldparam [Array<Kafka::FetchedMessage>] kafka fetched messages
29
+ # @yieldparam [Array<Kafka::FetchedMessage>, Symbol] kafka response with an info about
30
+ # the type of the fetcher that is being used
24
31
  # @note This will yield with raw messages - no preprocessing or reformatting.
25
32
  def fetch_loop
26
33
  settings = ApiAdapter.consumption(consumer_group)
27
34
 
28
35
  if consumer_group.batch_fetching
29
- kafka_consumer.each_batch(*settings) { |batch| yield(batch.messages) }
36
+ kafka_consumer.each_batch(*settings) { |batch| yield(batch, :batch) }
30
37
  else
31
- # always yield an array of messages, so we have consistent API (always a batch)
32
- kafka_consumer.each_message(*settings) { |message| yield([message]) }
38
+ kafka_consumer.each_message(*settings) { |message| yield(message, :message) }
33
39
  end
34
- rescue Kafka::ProcessingError => error
40
+ # @note We catch only the processing errors as any other are considered critical (exceptions)
41
+ # and should require a client restart with a backoff
42
+ rescue Kafka::ProcessingError => e
35
43
  # If there was an error during consumption, we have to log it, pause current partition
36
44
  # and process other things
37
45
  Karafka.monitor.instrument(
38
46
  'connection.client.fetch_loop.error',
39
47
  caller: self,
40
- error: error.cause
41
- )
42
- pause(error.topic, error.partition)
43
- retry
44
- # This is on purpose - see the notes for this method
45
- # rubocop:disable RescueException
46
- rescue Exception => error
47
- # rubocop:enable RescueException
48
- Karafka.monitor.instrument(
49
- 'connection.client.fetch_loop.error',
50
- caller: self,
51
- error: error
48
+ error: e.cause
52
49
  )
50
+ pause(e.topic, e.partition)
53
51
  retry
54
52
  end
55
53
 
56
- # Gracefuly stops topic consumption
54
+ # Gracefully stops topic consumption
57
55
  # @note Stopping running consumers without a really important reason is not recommended
58
56
  # as until all the consumers are stopped, the server will keep running serving only
59
57
  # part of the messages
@@ -69,25 +67,27 @@ module Karafka
69
67
  kafka_consumer.pause(*ApiAdapter.pause(topic, partition, consumer_group))
70
68
  end
71
69
 
72
- # Marks a given message as consumed and commit the offsets
73
- # @note In opposite to ruby-kafka, we commit the offset for each manual marking to be sure
74
- # that offset commit happen asap in case of a crash
70
+ # Marks given message as consumed
75
71
  # @param [Karafka::Params::Params] params message that we want to mark as processed
72
+ # @note This method won't trigger automatic offsets commits, rather relying on the ruby-kafka
73
+ # offsets time-interval based committing
76
74
  def mark_as_consumed(params)
77
75
  kafka_consumer.mark_message_as_processed(
78
76
  *ApiAdapter.mark_message_as_processed(params)
79
77
  )
78
+ end
79
+
80
+ # Marks a given message as consumed and commit the offsets in a blocking way
81
+ # @param [Karafka::Params::Params] params message that we want to mark as processed
82
+ # @note This method commits the offset for each manual marking to be sure
83
+ # that offset commit happen asap in case of a crash
84
+ def mark_as_consumed!(params)
85
+ mark_as_consumed(params)
80
86
  # Trigger an immediate, blocking offset commit in order to minimize the risk of crashing
81
87
  # before the automatic triggers have kicked in.
82
88
  kafka_consumer.commit_offsets
83
89
  end
84
90
 
85
- # Triggers a non-optional blocking heartbeat that notifies Kafka about the fact, that this
86
- # consumer / client is still up and running
87
- def trigger_heartbeat
88
- kafka_consumer.trigger_heartbeat!
89
- end
90
-
91
91
  private
92
92
 
93
93
  attr_reader :consumer_group
@@ -97,7 +97,7 @@ module Karafka
97
97
  def kafka_consumer
98
98
  # @note We don't cache the connection internally because we cache kafka_consumer that uses
99
99
  # kafka client object instance
100
- @kafka_consumer ||= Builder.call.consumer(
100
+ @kafka_consumer ||= Builder.call(consumer_group).consumer(
101
101
  *ApiAdapter.consumer(consumer_group)
102
102
  ).tap do |consumer|
103
103
  consumer_group.topics.each do |topic|
@@ -105,10 +105,10 @@ module Karafka
105
105
  end
106
106
  end
107
107
  rescue Kafka::ConnectionError
108
- # If we would not wait it would totally spam log file with failed
108
+ # If we would not wait it will spam log file with failed
109
109
  # attempts if Kafka is down
110
110
  sleep(consumer_group.reconnect_timeout)
111
- # We don't log and just reraise - this will be logged
111
+ # We don't log and just re-raise - this will be logged
112
112
  # down the road
113
113
  raise
114
114
  end
@@ -16,9 +16,10 @@ module Karafka
16
16
 
17
17
  # Runs prefetch callbacks and executes the main listener fetch loop
18
18
  def call
19
- Karafka::Callbacks.before_fetch_loop(
20
- @consumer_group,
21
- client
19
+ Karafka.monitor.instrument(
20
+ 'connection.listener.before_fetch_loop',
21
+ consumer_group: @consumer_group,
22
+ client: client
22
23
  )
23
24
  fetch_loop
24
25
  end
@@ -26,28 +27,38 @@ module Karafka
26
27
  private
27
28
 
28
29
  # Opens connection, gets messages and calls a block for each of the incoming messages
29
- # @yieldparam [String] consumer group id
30
- # @yieldparam [Array<Kafka::FetchedMessage>] kafka fetched messages
31
- # @note This will yield with a raw message - no preprocessing or reformatting
32
30
  # @note We catch all the errors here, so they don't affect other listeners (or this one)
33
31
  # so we will be able to listen and consume other incoming messages.
34
32
  # Since it is run inside Karafka::Connection::ActorCluster - catching all the exceptions
35
- # won't crash the whole cluster. Here we mostly focus on catchin the exceptions related to
33
+ # won't crash the whole cluster. Here we mostly focus on catching the exceptions related to
36
34
  # Kafka connections / Internet connection issues / Etc. Business logic problems should not
37
35
  # propagate this far
38
36
  def fetch_loop
39
- client.fetch_loop do |raw_messages|
40
- # @note What happens here is a delegation of processing to a proper processor based
41
- # on the incoming messages characteristics
42
- Karafka::Connection::Delegator.call(@consumer_group.id, raw_messages)
37
+ # @note What happens here is a delegation of processing to a proper processor based
38
+ # on the incoming messages characteristics
39
+ client.fetch_loop do |raw_data, type|
40
+ Karafka.monitor.instrument('connection.listener.fetch_loop')
41
+
42
+ case type
43
+ when :message
44
+ MessageDelegator.call(@consumer_group.id, raw_data)
45
+ when :batch
46
+ BatchDelegator.call(@consumer_group.id, raw_data)
47
+ end
43
48
  end
44
49
  # This is on purpose - see the notes for this method
45
- # rubocop:disable RescueException
50
+ # rubocop:disable Lint/RescueException
46
51
  rescue Exception => e
47
52
  Karafka.monitor.instrument('connection.listener.fetch_loop.error', caller: self, error: e)
48
- # rubocop:enable RescueException
49
- @client&.stop
50
- retry if @client
53
+ # rubocop:enable Lint/RescueException
54
+ # We can stop client without a problem, as it will reinitialize itself when running the
55
+ # `fetch_loop` again
56
+ @client.stop
57
+ # We need to clear the consumers cache for current connection when fatal error happens and
58
+ # we reset the connection. Otherwise for consumers with manual offset management, the
59
+ # persistence might have stored some data that would be reprocessed
60
+ Karafka::Persistence::Consumers.clear
61
+ sleep(@consumer_group.reconnect_timeout) && retry
51
62
  end
52
63
 
53
64
  # @return [Karafka::Connection::Client] wrapped kafka consuming client for a given topic