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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.coditsu/ci.yml +3 -0
- data/.console_irbrc +1 -3
- data/.diffend.yml +3 -0
- data/.github/FUNDING.yml +3 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +50 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.github/workflows/ci.yml +52 -0
- data/.gitignore +1 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +134 -14
- data/CODE_OF_CONDUCT.md +1 -1
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +4 -5
- data/Gemfile.lock +92 -81
- data/README.md +9 -12
- data/bin/karafka +1 -1
- data/certs/mensfeld.pem +25 -0
- data/config/errors.yml +38 -5
- data/docker-compose.yml +17 -0
- data/karafka.gemspec +18 -17
- data/lib/karafka.rb +10 -16
- data/lib/karafka/app.rb +14 -6
- data/lib/karafka/attributes_map.rb +5 -10
- data/lib/karafka/base_consumer.rb +19 -30
- data/lib/karafka/base_responder.rb +45 -27
- data/lib/karafka/cli.rb +2 -2
- data/lib/karafka/cli/console.rb +11 -9
- data/lib/karafka/cli/flow.rb +9 -7
- data/lib/karafka/cli/info.rb +4 -2
- data/lib/karafka/cli/install.rb +30 -6
- data/lib/karafka/cli/server.rb +11 -6
- data/lib/karafka/code_reloader.rb +67 -0
- data/lib/karafka/connection/api_adapter.rb +22 -9
- data/lib/karafka/connection/batch_delegator.rb +55 -0
- data/lib/karafka/connection/builder.rb +5 -3
- data/lib/karafka/connection/client.rb +31 -31
- data/lib/karafka/connection/listener.rb +26 -15
- data/lib/karafka/connection/message_delegator.rb +36 -0
- data/lib/karafka/consumers/batch_metadata.rb +10 -0
- data/lib/karafka/consumers/callbacks.rb +32 -15
- data/lib/karafka/consumers/includer.rb +31 -18
- data/lib/karafka/consumers/responders.rb +2 -2
- data/lib/karafka/contracts.rb +10 -0
- data/lib/karafka/contracts/config.rb +21 -0
- data/lib/karafka/contracts/consumer_group.rb +206 -0
- data/lib/karafka/contracts/consumer_group_topic.rb +19 -0
- data/lib/karafka/contracts/responder_usage.rb +54 -0
- data/lib/karafka/contracts/server_cli_options.rb +31 -0
- data/lib/karafka/errors.rb +17 -16
- data/lib/karafka/fetcher.rb +28 -30
- data/lib/karafka/helpers/class_matcher.rb +12 -2
- data/lib/karafka/helpers/config_retriever.rb +1 -1
- data/lib/karafka/helpers/inflector.rb +26 -0
- data/lib/karafka/helpers/multi_delegator.rb +0 -1
- data/lib/karafka/instrumentation/logger.rb +9 -6
- data/lib/karafka/instrumentation/monitor.rb +15 -9
- data/lib/karafka/instrumentation/proctitle_listener.rb +36 -0
- data/lib/karafka/instrumentation/stdout_listener.rb +140 -0
- data/lib/karafka/params/batch_metadata.rb +26 -0
- data/lib/karafka/params/builders/batch_metadata.rb +30 -0
- data/lib/karafka/params/builders/params.rb +38 -0
- data/lib/karafka/params/builders/params_batch.rb +25 -0
- data/lib/karafka/params/metadata.rb +20 -0
- data/lib/karafka/params/params.rb +54 -0
- data/lib/karafka/params/params_batch.rb +35 -21
- data/lib/karafka/patches/ruby_kafka.rb +21 -8
- data/lib/karafka/persistence/client.rb +15 -11
- data/lib/karafka/persistence/{consumer.rb → consumers.rb} +20 -13
- data/lib/karafka/persistence/topics.rb +48 -0
- data/lib/karafka/process.rb +0 -2
- data/lib/karafka/responders/builder.rb +1 -1
- data/lib/karafka/responders/topic.rb +6 -8
- data/lib/karafka/routing/builder.rb +36 -8
- data/lib/karafka/routing/consumer_group.rb +1 -1
- data/lib/karafka/routing/consumer_mapper.rb +9 -9
- data/lib/karafka/routing/proxy.rb +10 -1
- data/lib/karafka/routing/topic.rb +5 -3
- data/lib/karafka/routing/topic_mapper.rb +16 -18
- data/lib/karafka/serialization/json/deserializer.rb +27 -0
- data/lib/karafka/serialization/json/serializer.rb +31 -0
- data/lib/karafka/server.rb +29 -28
- data/lib/karafka/setup/config.rb +67 -37
- data/lib/karafka/setup/configurators/water_drop.rb +7 -3
- data/lib/karafka/setup/dsl.rb +0 -1
- data/lib/karafka/status.rb +7 -3
- data/lib/karafka/templates/{application_consumer.rb.example → application_consumer.rb.erb} +2 -1
- data/lib/karafka/templates/{application_responder.rb.example → application_responder.rb.erb} +0 -0
- data/lib/karafka/templates/karafka.rb.erb +92 -0
- data/lib/karafka/version.rb +1 -1
- metadata +94 -72
- metadata.gz.sig +0 -0
- data/.travis.yml +0 -21
- data/lib/karafka/callbacks.rb +0 -30
- data/lib/karafka/callbacks/config.rb +0 -22
- data/lib/karafka/callbacks/dsl.rb +0 -16
- data/lib/karafka/connection/delegator.rb +0 -46
- data/lib/karafka/instrumentation/listener.rb +0 -112
- data/lib/karafka/loader.rb +0 -28
- data/lib/karafka/params/dsl.rb +0 -156
- data/lib/karafka/parsers/json.rb +0 -38
- data/lib/karafka/patches/dry_configurable.rb +0 -35
- data/lib/karafka/persistence/topic.rb +0 -29
- data/lib/karafka/schemas/config.rb +0 -24
- data/lib/karafka/schemas/consumer_group.rb +0 -78
- data/lib/karafka/schemas/consumer_group_topic.rb +0 -18
- data/lib/karafka/schemas/responder_usage.rb +0 -39
- data/lib/karafka/schemas/server_cli_options.rb +0 -43
- data/lib/karafka/setup/configurators/base.rb +0 -29
- data/lib/karafka/setup/configurators/params.rb +0 -25
- data/lib/karafka/templates/karafka.rb.example +0 -54
data/lib/karafka/cli/server.rb
CHANGED
@@ -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
|
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 =
|
50
|
+
result = CONTRACT.call(cli.options)
|
47
51
|
return if result.success?
|
48
|
-
|
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.
|
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
|
-
|
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
|
-
{
|
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
|
102
|
-
#
|
103
|
-
|
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::
|
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
|
-
|
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
|
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
|
36
|
+
kafka_consumer.each_batch(*settings) { |batch| yield(batch, :batch) }
|
30
37
|
else
|
31
|
-
|
32
|
-
kafka_consumer.each_message(*settings) { |message| yield([message]) }
|
38
|
+
kafka_consumer.each_message(*settings) { |message| yield(message, :message) }
|
33
39
|
end
|
34
|
-
|
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:
|
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
|
-
#
|
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
|
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
|
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
|
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
|
20
|
-
|
21
|
-
|
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
|
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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
Karafka
|
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
|
-
|
50
|
-
|
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
|