karafka 2.0.0.alpha6 → 2.0.0.beta3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +42 -2
- data/Gemfile.lock +9 -9
- data/bin/integrations +36 -14
- data/bin/scenario +29 -0
- data/config/errors.yml +1 -0
- data/docker-compose.yml +3 -0
- data/karafka.gemspec +1 -1
- data/lib/active_job/karafka.rb +2 -2
- data/lib/karafka/active_job/routing/extensions.rb +31 -0
- data/lib/karafka/base_consumer.rb +74 -6
- data/lib/karafka/connection/client.rb +39 -16
- data/lib/karafka/connection/listener.rb +103 -34
- data/lib/karafka/connection/listeners_batch.rb +24 -0
- data/lib/karafka/connection/messages_buffer.rb +48 -61
- data/lib/karafka/connection/pauses_manager.rb +2 -2
- data/lib/karafka/connection/raw_messages_buffer.rb +101 -0
- data/lib/karafka/contracts/config.rb +10 -1
- data/lib/karafka/helpers/async.rb +33 -0
- data/lib/karafka/instrumentation/logger_listener.rb +37 -10
- data/lib/karafka/instrumentation/monitor.rb +4 -0
- data/lib/karafka/licenser.rb +26 -7
- data/lib/karafka/messages/batch_metadata.rb +26 -3
- data/lib/karafka/messages/builders/batch_metadata.rb +17 -29
- data/lib/karafka/messages/builders/message.rb +1 -0
- data/lib/karafka/messages/builders/messages.rb +4 -12
- data/lib/karafka/pro/active_job/consumer.rb +48 -0
- data/lib/karafka/pro/active_job/dispatcher.rb +3 -3
- data/lib/karafka/pro/active_job/job_options_contract.rb +2 -2
- data/lib/karafka/pro/base_consumer_extensions.rb +66 -0
- data/lib/karafka/pro/loader.rb +27 -4
- data/lib/karafka/pro/performance_tracker.rb +80 -0
- data/lib/karafka/pro/processing/jobs/consume_non_blocking.rb +37 -0
- data/lib/karafka/pro/processing/jobs_builder.rb +31 -0
- data/lib/karafka/pro/routing/extensions.rb +32 -0
- data/lib/karafka/pro/scheduler.rb +54 -0
- data/lib/karafka/processing/executor.rb +26 -11
- data/lib/karafka/processing/executors_buffer.rb +15 -7
- data/lib/karafka/processing/jobs/base.rb +28 -0
- data/lib/karafka/processing/jobs/consume.rb +11 -4
- data/lib/karafka/processing/jobs_builder.rb +28 -0
- data/lib/karafka/processing/jobs_queue.rb +28 -16
- data/lib/karafka/processing/worker.rb +39 -10
- data/lib/karafka/processing/workers_batch.rb +5 -0
- data/lib/karafka/routing/consumer_group.rb +1 -1
- data/lib/karafka/routing/subscription_group.rb +2 -2
- data/lib/karafka/routing/subscription_groups_builder.rb +3 -2
- data/lib/karafka/routing/topics.rb +38 -0
- data/lib/karafka/runner.rb +19 -27
- data/lib/karafka/scheduler.rb +20 -0
- data/lib/karafka/server.rb +24 -23
- data/lib/karafka/setup/config.rb +6 -1
- data/lib/karafka/status.rb +1 -3
- data/lib/karafka/time_trackers/pause.rb +10 -2
- data/lib/karafka/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +19 -4
- metadata.gz.sig +0 -0
- data/lib/karafka/active_job/routing_extensions.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 86b352cc7737bde8484567662d546165793496adb6a923e33aa7e4823e4a456f
|
4
|
+
data.tar.gz: 2a180ba7b177db9f4c67de387262d47ac3eb2580860a548992912cd89a049da7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 207700c3e1fab4d3370de7eddae72453fc180c65d817f4d7c021929327e5464fcd8a9bb1610c641080f82a60bc038e4517dc7bc86c0aa4c83852dd9467441e75
|
7
|
+
data.tar.gz: ab94239255ff841e0728c6e6585d4145a9c4f42dfd02d8df062975bd28ad3caf2cef50f5488d30a1356ad49e2529f772379bc947ba5804a15f12e56989cf9a7a
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
3.1.
|
1
|
+
3.1.2
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,45 @@
|
|
1
1
|
# Karafka framework changelog
|
2
2
|
|
3
|
+
## 2.0.0-beta3 (2022-06-14)
|
4
|
+
- Jobs building responsibility extracted out of the listener code base.
|
5
|
+
- Fix a case where specs supervisor would try to kill no longer running process (#868)
|
6
|
+
- Fix an instable integration spec that could misbehave under load
|
7
|
+
- Commit offsets prior to pausing partitions to ensure that the latest offset is always committed
|
8
|
+
- Fix a case where consecutive CTRL+C (non-stop) would case an exception during forced shutdown
|
9
|
+
- Add missing `consumer.prepared.error` into `LoggerListener`
|
10
|
+
- Delegate partition resuming from the consumers to listeners threads.
|
11
|
+
- Add support for Long Running Jobs (LRJ) for ActiveJob [PRO]
|
12
|
+
- Add support for Long Running Jobs for consumers [PRO]
|
13
|
+
- Allow `active_job_topic` to accept a block for extra topic related settings
|
14
|
+
- Remove no longer needed logger threads
|
15
|
+
- Auto-adapt number of processes for integration specs based on the number of CPUs
|
16
|
+
- Introduce an integration spec runner that prints everything to stdout (better for development)
|
17
|
+
- Introduce extra integration specs for various ActiveJob usage scenarios
|
18
|
+
- Rename consumer method `#prepared` to `#prepare` to reflect better its use-case
|
19
|
+
- For test and dev raise an error when expired license key is used (never for non dev)
|
20
|
+
- Add worker related monitor events (`worker.process` and `worker.processed`)
|
21
|
+
- Update `LoggerListener` to include more useful information about processing and polling messages
|
22
|
+
|
23
|
+
## 2.0.0-beta2 (2022-06-07)
|
24
|
+
- Abstract away notion of topics groups (until now it was just an array)
|
25
|
+
- Optimize how jobs queue is closed. Since we enqueue jobs only from the listeners, we can safely close jobs queue once listeners are done. By extracting this responsibility from listeners, we remove corner cases and race conditions. Note here: for non-blocking jobs we do wait for them to finish while running the `poll`. This ensures, that for async jobs that are long-living, we do not reach `max.poll.interval`.
|
26
|
+
- `Shutdown` jobs are executed in workers to align all the jobs behaviours.
|
27
|
+
- `Shutdown` jobs are always blocking.
|
28
|
+
- Notion of `ListenersBatch` was introduced similar to `WorkersBatch` to abstract this concept.
|
29
|
+
- Change default `shutdown_timeout` to be more than `max_wait_time` not to cause forced shutdown when no messages are being received from Kafka.
|
30
|
+
- Abstract away scheduling of revocation and shutdown jobs for both default and pro schedulers
|
31
|
+
- Introduce a second (internal) messages buffer to distinguish between raw messages buffer and karafka messages buffer
|
32
|
+
- Move messages and their metadata remap process to the listener thread to allow for their inline usage
|
33
|
+
- Change how we wait in the shutdown phase, so shutdown jobs can still use Kafka connection even if they run for a longer period of time. This will prevent us from being kicked out from the group early.
|
34
|
+
- Introduce validation that ensures, that `shutdown_timeout` is more than `max_wait_time`. This will prevent users from ending up with a config that could lead to frequent forceful shutdowns.
|
35
|
+
|
36
|
+
## 2.0.0-beta1 (2022-05-22)
|
37
|
+
- Update the jobs queue blocking engine and allow for non-blocking jobs execution
|
38
|
+
- Provide `#prepared` hook that always runs before the fetching loop is unblocked
|
39
|
+
- [Pro] Introduce performance tracker for scheduling optimizer
|
40
|
+
- Provide ability to pause (`#pause`) and resume (`#resume`) given partitions from the consumers
|
41
|
+
- Small integration specs refactoring + specs for pausing scenarios
|
42
|
+
|
3
43
|
## 2.0.0-alpha6 (2022-04-17)
|
4
44
|
- Fix a bug, where upon missing boot file and Rails, railtie would fail with a generic exception (#818)
|
5
45
|
- Fix an issue with parallel pristine specs colliding with each other during `bundle install` (#820)
|
@@ -26,12 +66,12 @@
|
|
26
66
|
|
27
67
|
## 2.0.0-alpha2 (2022-02-19)
|
28
68
|
- Require `kafka` keys to be symbols
|
29
|
-
- Added ActiveJob Pro adapter
|
69
|
+
- [Pro] Added ActiveJob Pro adapter
|
30
70
|
- Small updates to the license and docs
|
31
71
|
|
32
72
|
## 2.0.0-alpha1 (2022-01-30)
|
33
73
|
- Change license to `LGPL-3.0`
|
34
|
-
- Introduce a Pro subscription
|
74
|
+
- [Pro] Introduce a Pro subscription
|
35
75
|
- Switch from `ruby-kafka` to `librdkafka` as an underlying driver
|
36
76
|
- Introduce fully automatic integration tests that go through the whole server lifecycle
|
37
77
|
- Integrate WaterDrop tightly with autoconfiguration inheritance and an option to redefine it
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
karafka (2.0.0.
|
4
|
+
karafka (2.0.0.beta3)
|
5
5
|
dry-configurable (~> 0.13)
|
6
6
|
dry-monitor (~> 0.5)
|
7
7
|
dry-validation (~> 1.7)
|
@@ -13,10 +13,10 @@ PATH
|
|
13
13
|
GEM
|
14
14
|
remote: https://rubygems.org/
|
15
15
|
specs:
|
16
|
-
activejob (7.0.
|
17
|
-
activesupport (= 7.0.
|
16
|
+
activejob (7.0.3)
|
17
|
+
activesupport (= 7.0.3)
|
18
18
|
globalid (>= 0.3.6)
|
19
|
-
activesupport (7.0.
|
19
|
+
activesupport (7.0.3)
|
20
20
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
21
21
|
i18n (>= 1.6, < 2)
|
22
22
|
minitest (>= 5.1)
|
@@ -25,7 +25,7 @@ GEM
|
|
25
25
|
concurrent-ruby (1.1.10)
|
26
26
|
diff-lcs (1.5.0)
|
27
27
|
docile (1.4.0)
|
28
|
-
dry-configurable (0.
|
28
|
+
dry-configurable (0.15.0)
|
29
29
|
concurrent-ruby (~> 1.0)
|
30
30
|
dry-core (~> 0.6)
|
31
31
|
dry-container (0.9.0)
|
@@ -45,7 +45,7 @@ GEM
|
|
45
45
|
dry-configurable (~> 0.13, >= 0.13.0)
|
46
46
|
dry-core (~> 0.5, >= 0.5)
|
47
47
|
dry-events (~> 0.2)
|
48
|
-
dry-schema (1.9.
|
48
|
+
dry-schema (1.9.2)
|
49
49
|
concurrent-ruby (~> 1.0)
|
50
50
|
dry-configurable (~> 0.13, >= 0.13.0)
|
51
51
|
dry-core (~> 0.5, >= 0.5)
|
@@ -58,12 +58,12 @@ GEM
|
|
58
58
|
dry-core (~> 0.5, >= 0.5)
|
59
59
|
dry-inflector (~> 0.1, >= 0.1.2)
|
60
60
|
dry-logic (~> 1.0, >= 1.0.2)
|
61
|
-
dry-validation (1.8.
|
61
|
+
dry-validation (1.8.1)
|
62
62
|
concurrent-ruby (~> 1.0)
|
63
63
|
dry-container (~> 0.7, >= 0.7.1)
|
64
64
|
dry-core (~> 0.5, >= 0.5)
|
65
65
|
dry-initializer (~> 3.0)
|
66
|
-
dry-schema (~> 1.
|
66
|
+
dry-schema (~> 1.8, >= 1.8.0)
|
67
67
|
factory_bot (6.2.1)
|
68
68
|
activesupport (>= 5.0.0)
|
69
69
|
ffi (1.15.5)
|
@@ -121,4 +121,4 @@ DEPENDENCIES
|
|
121
121
|
simplecov
|
122
122
|
|
123
123
|
BUNDLED WITH
|
124
|
-
2.3.
|
124
|
+
2.3.11
|
data/bin/integrations
CHANGED
@@ -11,21 +11,21 @@ require 'open3'
|
|
11
11
|
require 'fileutils'
|
12
12
|
require 'pathname'
|
13
13
|
require 'tmpdir'
|
14
|
+
require 'etc'
|
14
15
|
|
15
16
|
ROOT_PATH = Pathname.new(File.expand_path(File.join(File.dirname(__FILE__), '../')))
|
16
17
|
|
17
|
-
# Raised from the parent process if any of the integration tests fails
|
18
|
-
IntegrationTestError = Class.new(StandardError)
|
19
|
-
|
20
18
|
# How many child processes with integration specs do we want to run in parallel
|
21
|
-
# When the value is high, there's a problem with thread allocation on Github
|
22
|
-
|
19
|
+
# When the value is high, there's a problem with thread allocation on Github CI, tht is why
|
20
|
+
# we limit it. Locally we can run a lot of those, as many of them have sleeps and do not use a lot
|
21
|
+
# of CPU
|
22
|
+
CONCURRENCY = ENV.key?('CI') ? 5 : Etc.nprocessors * 2
|
23
23
|
|
24
24
|
# Abstraction around a single test scenario execution process
|
25
25
|
class Scenario
|
26
26
|
# How long a scenario can run before we kill it
|
27
27
|
# This is a fail-safe just in case something would hang
|
28
|
-
MAX_RUN_TIME =
|
28
|
+
MAX_RUN_TIME = 3 * 60 # 3 minutes tops
|
29
29
|
|
30
30
|
# There are rare cases where Karafka may force shutdown for some of the integration cases
|
31
31
|
# This includes exactly those
|
@@ -73,7 +73,12 @@ class Scenario
|
|
73
73
|
# If the thread is running too long, kill it
|
74
74
|
if current_time - @started_at > MAX_RUN_TIME
|
75
75
|
@wait_thr.kill
|
76
|
-
|
76
|
+
|
77
|
+
begin
|
78
|
+
Process.kill('TERM', pid)
|
79
|
+
# It may finish right after we want to kill it, that's why we ignore this
|
80
|
+
rescue Errno::ESRCH
|
81
|
+
end
|
77
82
|
end
|
78
83
|
|
79
84
|
# We read it so it won't grow as we use our default logger that prints to both test.log and
|
@@ -106,14 +111,15 @@ class Scenario
|
|
106
111
|
|
107
112
|
# Prints a status report when scenario is finished and stdout if it failed
|
108
113
|
def report
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
+
if success?
|
115
|
+
print "\e[#{32}m#{'.'}\e[0m"
|
116
|
+
else
|
117
|
+
puts
|
118
|
+
puts "\e[#{31}m#{'[FAILED]'}\e[0m #{name}"
|
114
119
|
puts "Exit code: #{exit_code}"
|
115
120
|
puts @stdout_tail
|
116
121
|
puts @stderr.read
|
122
|
+
puts
|
117
123
|
end
|
118
124
|
end
|
119
125
|
|
@@ -204,5 +210,21 @@ while finished_scenarios.size < scenarios.size
|
|
204
210
|
sleep(0.1)
|
205
211
|
end
|
206
212
|
|
207
|
-
|
208
|
-
|
213
|
+
failed_scenarios = finished_scenarios.reject(&:success?)
|
214
|
+
|
215
|
+
# Report once more on the failed jobs
|
216
|
+
# This will only list scenarios that failed without printing their stdout here.
|
217
|
+
if failed_scenarios.empty?
|
218
|
+
puts
|
219
|
+
else
|
220
|
+
puts "\nFailed scenarios:\n\n"
|
221
|
+
|
222
|
+
failed_scenarios.each do |scenario|
|
223
|
+
puts "\e[#{31}m#{'[FAILED]'}\e[0m #{scenario.name}"
|
224
|
+
end
|
225
|
+
|
226
|
+
puts
|
227
|
+
|
228
|
+
# Exit with 1 if not all scenarios were successful
|
229
|
+
exit 1
|
230
|
+
end
|
data/bin/scenario
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Runner for non-parallel execution of a single scenario.
|
4
|
+
# It prints all the info stdout, etc and basically replaces itself with the scenario execution.
|
5
|
+
# It is useful when we work with a single spec and we need all the debug info
|
6
|
+
|
7
|
+
raise 'This code needs to be executed WITHOUT bundle exec' if Kernel.const_defined?(:Bundler)
|
8
|
+
|
9
|
+
require 'open3'
|
10
|
+
require 'fileutils'
|
11
|
+
require 'pathname'
|
12
|
+
require 'tmpdir'
|
13
|
+
require 'etc'
|
14
|
+
|
15
|
+
ROOT_PATH = Pathname.new(File.expand_path(File.join(File.dirname(__FILE__), '../')))
|
16
|
+
|
17
|
+
# Load all the specs
|
18
|
+
specs = Dir[ROOT_PATH.join('spec/integrations/**/*.rb')]
|
19
|
+
|
20
|
+
# If filters is provided, apply
|
21
|
+
# Allows to provide several filters one after another and applies all of them
|
22
|
+
ARGV.each do |filter|
|
23
|
+
specs.delete_if { |name| !name.include?(filter) }
|
24
|
+
end
|
25
|
+
|
26
|
+
raise ArgumentError, "No integration specs with filters: #{ARGV.join(', ')}" if specs.empty?
|
27
|
+
raise ArgumentError, "Many specs found with filters: #{ARGV.join(', ')}" if specs.size != 1
|
28
|
+
|
29
|
+
exec("bundle exec ruby -r #{ROOT_PATH}/spec/integrations_helper.rb #{specs[0]}")
|
data/config/errors.yml
CHANGED
@@ -2,6 +2,7 @@ en:
|
|
2
2
|
dry_validation:
|
3
3
|
errors:
|
4
4
|
max_timeout_vs_pause_max_timeout: pause_timeout must be less or equal to pause_max_timeout
|
5
|
+
shutdown_timeout_vs_max_wait_time: shutdown_timeout must be more than max_wait_time
|
5
6
|
topics_names_not_unique: all topic names within a single consumer group must be unique
|
6
7
|
required_usage_count: Given topic must be used at least once
|
7
8
|
consumer_groups_inclusion: Unknown consumer group
|
data/docker-compose.yml
CHANGED
data/karafka.gemspec
CHANGED
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
|
|
14
14
|
spec.homepage = 'https://karafka.io'
|
15
15
|
spec.summary = 'Ruby based framework for working with Apache Kafka'
|
16
16
|
spec.description = 'Framework used to simplify Apache Kafka based Ruby applications development'
|
17
|
-
spec.
|
17
|
+
spec.licenses = ['LGPL-3.0', 'Commercial']
|
18
18
|
|
19
19
|
spec.add_dependency 'dry-configurable', '~> 0.13'
|
20
20
|
spec.add_dependency 'dry-monitor', '~> 0.5'
|
data/lib/active_job/karafka.rb
CHANGED
@@ -14,8 +14,8 @@ begin
|
|
14
14
|
# We extend routing builder by adding a simple wrapper for easier jobs topics defining
|
15
15
|
# This needs to be extended here as it is going to be used in karafka routes, hence doing that in
|
16
16
|
# the railtie initializer would be too late
|
17
|
-
::Karafka::Routing::Builder.include ::Karafka::ActiveJob::
|
18
|
-
::Karafka::Routing::Proxy.include ::Karafka::ActiveJob::
|
17
|
+
::Karafka::Routing::Builder.include ::Karafka::ActiveJob::Routing::Extensions
|
18
|
+
::Karafka::Routing::Proxy.include ::Karafka::ActiveJob::Routing::Extensions
|
19
19
|
rescue LoadError
|
20
20
|
# We extend ActiveJob stuff in the railtie
|
21
21
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Karafka
|
4
|
+
# ActiveJob related Karafka stuff
|
5
|
+
module ActiveJob
|
6
|
+
# Karafka routing ActiveJob related components
|
7
|
+
module Routing
|
8
|
+
# Routing extensions for ActiveJob
|
9
|
+
module Extensions
|
10
|
+
# This method simplifies routes definition for ActiveJob topics / queues by auto-injecting
|
11
|
+
# the consumer class
|
12
|
+
# @param name [String, Symbol] name of the topic where ActiveJobs jobs should go
|
13
|
+
# @param block [Proc] block that we can use for some extra configuration
|
14
|
+
def active_job_topic(name, &block)
|
15
|
+
topic(name) do
|
16
|
+
consumer App.config.internal.active_job.consumer
|
17
|
+
|
18
|
+
next unless block
|
19
|
+
|
20
|
+
instance_eval(&block)
|
21
|
+
|
22
|
+
# This is handled by our custom ActiveJob consumer
|
23
|
+
# Without this, default behaviour would cause messages to skip upon shutdown as the
|
24
|
+
# offset would be committed for the last message
|
25
|
+
manual_offset_management true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -10,13 +10,38 @@ module Karafka
|
|
10
10
|
attr_accessor :messages
|
11
11
|
# @return [Karafka::Connection::Client] kafka connection client
|
12
12
|
attr_accessor :client
|
13
|
-
# @return [Karafka::TimeTrackers::Pause] current topic partition pause
|
14
|
-
attr_accessor :
|
13
|
+
# @return [Karafka::TimeTrackers::Pause] current topic partition pause tracker
|
14
|
+
attr_accessor :pause_tracker
|
15
15
|
# @return [Waterdrop::Producer] producer instance
|
16
16
|
attr_accessor :producer
|
17
17
|
|
18
|
+
# Can be used to run preparation code
|
19
|
+
#
|
20
|
+
# @private
|
21
|
+
# @note This should not be used by the end users as it is part of the lifecycle of things but
|
22
|
+
# not as part of the public api. This can act as a hook when creating non-blocking
|
23
|
+
# consumers and doing other advanced stuff
|
24
|
+
def on_prepare
|
25
|
+
Karafka.monitor.instrument('consumer.prepared', caller: self) do
|
26
|
+
prepare
|
27
|
+
end
|
28
|
+
|
29
|
+
true
|
30
|
+
rescue StandardError => e
|
31
|
+
Karafka.monitor.instrument(
|
32
|
+
'error.occurred',
|
33
|
+
error: e,
|
34
|
+
caller: self,
|
35
|
+
type: 'consumer.prepare.error'
|
36
|
+
)
|
37
|
+
|
38
|
+
false
|
39
|
+
end
|
40
|
+
|
18
41
|
# Executes the default consumer flow.
|
19
42
|
#
|
43
|
+
# @return [Boolean] true if there was no exception, otherwise false.
|
44
|
+
#
|
20
45
|
# @note We keep the seek offset tracking, and use it to compensate for async offset flushing
|
21
46
|
# that may not yet kick in when error occurs. That way we pause always on the last processed
|
22
47
|
# message.
|
@@ -24,15 +49,17 @@ module Karafka
|
|
24
49
|
Karafka.monitor.instrument('consumer.consumed', caller: self) do
|
25
50
|
consume
|
26
51
|
|
27
|
-
|
52
|
+
pause_tracker.reset
|
28
53
|
|
29
54
|
# Mark as consumed only if manual offset management is not on
|
30
|
-
|
55
|
+
next if topic.manual_offset_management
|
31
56
|
|
32
57
|
# We use the non-blocking one here. If someone needs the blocking one, can implement it
|
33
58
|
# with manual offset management
|
34
59
|
mark_as_consumed(messages.last)
|
35
60
|
end
|
61
|
+
|
62
|
+
true
|
36
63
|
rescue StandardError => e
|
37
64
|
Karafka.monitor.instrument(
|
38
65
|
'error.occurred',
|
@@ -40,8 +67,10 @@ module Karafka
|
|
40
67
|
caller: self,
|
41
68
|
type: 'consumer.consume.error'
|
42
69
|
)
|
43
|
-
|
44
|
-
pause.
|
70
|
+
|
71
|
+
pause(@seek_offset || messages.first.offset)
|
72
|
+
|
73
|
+
false
|
45
74
|
end
|
46
75
|
|
47
76
|
# Trigger method for running on shutdown.
|
@@ -51,6 +80,8 @@ module Karafka
|
|
51
80
|
Karafka.monitor.instrument('consumer.revoked', caller: self) do
|
52
81
|
revoked
|
53
82
|
end
|
83
|
+
|
84
|
+
true
|
54
85
|
rescue StandardError => e
|
55
86
|
Karafka.monitor.instrument(
|
56
87
|
'error.occurred',
|
@@ -58,6 +89,8 @@ module Karafka
|
|
58
89
|
caller: self,
|
59
90
|
type: 'consumer.revoked.error'
|
60
91
|
)
|
92
|
+
|
93
|
+
false
|
61
94
|
end
|
62
95
|
|
63
96
|
# Trigger method for running on shutdown.
|
@@ -67,6 +100,8 @@ module Karafka
|
|
67
100
|
Karafka.monitor.instrument('consumer.shutdown', caller: self) do
|
68
101
|
shutdown
|
69
102
|
end
|
103
|
+
|
104
|
+
true
|
70
105
|
rescue StandardError => e
|
71
106
|
Karafka.monitor.instrument(
|
72
107
|
'error.occurred',
|
@@ -74,10 +109,16 @@ module Karafka
|
|
74
109
|
caller: self,
|
75
110
|
type: 'consumer.shutdown.error'
|
76
111
|
)
|
112
|
+
|
113
|
+
false
|
77
114
|
end
|
78
115
|
|
79
116
|
private
|
80
117
|
|
118
|
+
# Method that gets called in the blocking flow allowing to setup any type of resources or to
|
119
|
+
# send additional commands to Kafka before the proper execution starts.
|
120
|
+
def prepare; end
|
121
|
+
|
81
122
|
# Method that will perform business logic and on data received from Kafka (it will consume
|
82
123
|
# the data)
|
83
124
|
# @note This method needs bo be implemented in a subclass. We stub it here as a failover if
|
@@ -97,6 +138,10 @@ module Karafka
|
|
97
138
|
# Marks message as consumed in an async way.
|
98
139
|
#
|
99
140
|
# @param message [Messages::Message] last successfully processed message.
|
141
|
+
# @note We keep track of this offset in case we would mark as consumed and got error when
|
142
|
+
# processing another message. In case like this we do not pause on the message we've already
|
143
|
+
# processed but rather at the next one. This applies to both sync and async versions of this
|
144
|
+
# method.
|
100
145
|
def mark_as_consumed(message)
|
101
146
|
client.mark_as_consumed(message)
|
102
147
|
@seek_offset = message.offset + 1
|
@@ -110,6 +155,29 @@ module Karafka
|
|
110
155
|
@seek_offset = message.offset + 1
|
111
156
|
end
|
112
157
|
|
158
|
+
# Pauses processing on a given offset for the current topic partition
|
159
|
+
#
|
160
|
+
# After given partition is resumed, it will continue processing from the given offset
|
161
|
+
# @param offset [Integer] offset from which we want to restart the processing
|
162
|
+
# @param timeout [Integer, nil] how long in milliseconds do we want to pause or nil to use the
|
163
|
+
# default exponential pausing strategy defined for retries
|
164
|
+
def pause(offset, timeout = nil)
|
165
|
+
timeout ? pause_tracker.pause(timeout) : pause_tracker.pause
|
166
|
+
|
167
|
+
client.pause(
|
168
|
+
messages.metadata.topic,
|
169
|
+
messages.metadata.partition,
|
170
|
+
offset
|
171
|
+
)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Resumes processing of the current topic partition
|
175
|
+
def resume
|
176
|
+
# This is sufficient to expire a partition pause, as with it will be resumed by the listener
|
177
|
+
# thread before the next poll.
|
178
|
+
pause_tracker.expire
|
179
|
+
end
|
180
|
+
|
113
181
|
# Seeks in the context of current topic and partition
|
114
182
|
#
|
115
183
|
# @param offset [Integer] offset where we want to seek
|
@@ -30,7 +30,7 @@ module Karafka
|
|
30
30
|
@mutex = Mutex.new
|
31
31
|
@closed = false
|
32
32
|
@subscription_group = subscription_group
|
33
|
-
@buffer =
|
33
|
+
@buffer = RawMessagesBuffer.new
|
34
34
|
@rebalance_manager = RebalanceManager.new
|
35
35
|
@kafka = build_consumer
|
36
36
|
# Marks if we need to offset. If we did not store offsets, we should not commit the offset
|
@@ -86,8 +86,7 @@ module Karafka
|
|
86
86
|
# @param message [Karafka::Messages::Message]
|
87
87
|
def store_offset(message)
|
88
88
|
@mutex.synchronize do
|
89
|
-
|
90
|
-
@kafka.store_offset(message)
|
89
|
+
internal_store_offset(message)
|
91
90
|
end
|
92
91
|
end
|
93
92
|
|
@@ -104,14 +103,7 @@ module Karafka
|
|
104
103
|
def commit_offsets(async: true)
|
105
104
|
@mutex.lock
|
106
105
|
|
107
|
-
|
108
|
-
|
109
|
-
@kafka.commit(nil, async)
|
110
|
-
@offsetting = false
|
111
|
-
rescue Rdkafka::RdkafkaError => e
|
112
|
-
return if e.code == :no_offset
|
113
|
-
|
114
|
-
raise e
|
106
|
+
internal_commit_offsets(async: async)
|
115
107
|
ensure
|
116
108
|
@mutex.unlock
|
117
109
|
end
|
@@ -128,7 +120,11 @@ module Karafka
|
|
128
120
|
#
|
129
121
|
# @param message [Messages::Message, Messages::Seek] message to which we want to seek to
|
130
122
|
def seek(message)
|
123
|
+
@mutex.lock
|
124
|
+
|
131
125
|
@kafka.seek(message)
|
126
|
+
ensure
|
127
|
+
@mutex.unlock
|
132
128
|
end
|
133
129
|
|
134
130
|
# Pauses given partition and moves back to last successful offset processed.
|
@@ -144,15 +140,17 @@ module Karafka
|
|
144
140
|
# Do not pause if the client got closed, would not change anything
|
145
141
|
return if @closed
|
146
142
|
|
143
|
+
pause_msg = Messages::Seek.new(topic, partition, offset)
|
144
|
+
|
145
|
+
internal_commit_offsets(async: false)
|
146
|
+
|
147
147
|
tpl = topic_partition_list(topic, partition)
|
148
148
|
|
149
149
|
return unless tpl
|
150
150
|
|
151
151
|
@kafka.pause(tpl)
|
152
152
|
|
153
|
-
|
154
|
-
|
155
|
-
seek(pause_msg)
|
153
|
+
@kafka.seek(pause_msg)
|
156
154
|
ensure
|
157
155
|
@mutex.unlock
|
158
156
|
end
|
@@ -166,6 +164,11 @@ module Karafka
|
|
166
164
|
|
167
165
|
return if @closed
|
168
166
|
|
167
|
+
# Always commit synchronously offsets if any when we resume
|
168
|
+
# This prevents resuming without offset in case it would not be committed prior
|
169
|
+
# We can skip performance penalty since resuming should not happen too often
|
170
|
+
internal_commit_offsets(async: false)
|
171
|
+
|
169
172
|
tpl = topic_partition_list(topic, partition)
|
170
173
|
|
171
174
|
return unless tpl
|
@@ -214,11 +217,31 @@ module Karafka
|
|
214
217
|
|
215
218
|
private
|
216
219
|
|
220
|
+
# Non thread-safe offset storing method
|
221
|
+
# @param message [Karafka::Messages::Message]
|
222
|
+
def internal_store_offset(message)
|
223
|
+
@offsetting = true
|
224
|
+
@kafka.store_offset(message)
|
225
|
+
end
|
226
|
+
|
227
|
+
# Non thread-safe message committing method
|
228
|
+
# @param async [Boolean] should the commit happen async or sync (async by default)
|
229
|
+
def internal_commit_offsets(async: true)
|
230
|
+
return unless @offsetting
|
231
|
+
|
232
|
+
@kafka.commit(nil, async)
|
233
|
+
@offsetting = false
|
234
|
+
rescue Rdkafka::RdkafkaError => e
|
235
|
+
return if e.code == :no_offset
|
236
|
+
|
237
|
+
raise e
|
238
|
+
end
|
239
|
+
|
217
240
|
# Commits the stored offsets in a sync way and closes the consumer.
|
218
241
|
def close
|
219
|
-
commit_offsets!
|
220
|
-
|
221
242
|
@mutex.synchronize do
|
243
|
+
internal_commit_offsets(async: false)
|
244
|
+
|
222
245
|
@closed = true
|
223
246
|
|
224
247
|
# Remove callbacks runners that were registered
|