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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.ruby-version +1 -1
  4. data/CHANGELOG.md +42 -2
  5. data/Gemfile.lock +9 -9
  6. data/bin/integrations +36 -14
  7. data/bin/scenario +29 -0
  8. data/config/errors.yml +1 -0
  9. data/docker-compose.yml +3 -0
  10. data/karafka.gemspec +1 -1
  11. data/lib/active_job/karafka.rb +2 -2
  12. data/lib/karafka/active_job/routing/extensions.rb +31 -0
  13. data/lib/karafka/base_consumer.rb +74 -6
  14. data/lib/karafka/connection/client.rb +39 -16
  15. data/lib/karafka/connection/listener.rb +103 -34
  16. data/lib/karafka/connection/listeners_batch.rb +24 -0
  17. data/lib/karafka/connection/messages_buffer.rb +48 -61
  18. data/lib/karafka/connection/pauses_manager.rb +2 -2
  19. data/lib/karafka/connection/raw_messages_buffer.rb +101 -0
  20. data/lib/karafka/contracts/config.rb +10 -1
  21. data/lib/karafka/helpers/async.rb +33 -0
  22. data/lib/karafka/instrumentation/logger_listener.rb +37 -10
  23. data/lib/karafka/instrumentation/monitor.rb +4 -0
  24. data/lib/karafka/licenser.rb +26 -7
  25. data/lib/karafka/messages/batch_metadata.rb +26 -3
  26. data/lib/karafka/messages/builders/batch_metadata.rb +17 -29
  27. data/lib/karafka/messages/builders/message.rb +1 -0
  28. data/lib/karafka/messages/builders/messages.rb +4 -12
  29. data/lib/karafka/pro/active_job/consumer.rb +48 -0
  30. data/lib/karafka/pro/active_job/dispatcher.rb +3 -3
  31. data/lib/karafka/pro/active_job/job_options_contract.rb +2 -2
  32. data/lib/karafka/pro/base_consumer_extensions.rb +66 -0
  33. data/lib/karafka/pro/loader.rb +27 -4
  34. data/lib/karafka/pro/performance_tracker.rb +80 -0
  35. data/lib/karafka/pro/processing/jobs/consume_non_blocking.rb +37 -0
  36. data/lib/karafka/pro/processing/jobs_builder.rb +31 -0
  37. data/lib/karafka/pro/routing/extensions.rb +32 -0
  38. data/lib/karafka/pro/scheduler.rb +54 -0
  39. data/lib/karafka/processing/executor.rb +26 -11
  40. data/lib/karafka/processing/executors_buffer.rb +15 -7
  41. data/lib/karafka/processing/jobs/base.rb +28 -0
  42. data/lib/karafka/processing/jobs/consume.rb +11 -4
  43. data/lib/karafka/processing/jobs_builder.rb +28 -0
  44. data/lib/karafka/processing/jobs_queue.rb +28 -16
  45. data/lib/karafka/processing/worker.rb +39 -10
  46. data/lib/karafka/processing/workers_batch.rb +5 -0
  47. data/lib/karafka/routing/consumer_group.rb +1 -1
  48. data/lib/karafka/routing/subscription_group.rb +2 -2
  49. data/lib/karafka/routing/subscription_groups_builder.rb +3 -2
  50. data/lib/karafka/routing/topics.rb +38 -0
  51. data/lib/karafka/runner.rb +19 -27
  52. data/lib/karafka/scheduler.rb +20 -0
  53. data/lib/karafka/server.rb +24 -23
  54. data/lib/karafka/setup/config.rb +6 -1
  55. data/lib/karafka/status.rb +1 -3
  56. data/lib/karafka/time_trackers/pause.rb +10 -2
  57. data/lib/karafka/version.rb +1 -1
  58. data.tar.gz.sig +0 -0
  59. metadata +19 -4
  60. metadata.gz.sig +0 -0
  61. 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: 862df94b26c24809f82e07f71c39433b90ef08f68f053a004a87aa466b872dac
4
- data.tar.gz: 2c533cbd6c271fe282f59c2030d9cc885555242bb8bc9316d0264a5ccfd694a0
3
+ metadata.gz: 86b352cc7737bde8484567662d546165793496adb6a923e33aa7e4823e4a456f
4
+ data.tar.gz: 2a180ba7b177db9f4c67de387262d47ac3eb2580860a548992912cd89a049da7
5
5
  SHA512:
6
- metadata.gz: 8ce3720e535d65f121bcbfd957286cbc41404f2aff2751622183367862db55f8231c286d5bbede4a18649eafcd085952a7fc8e7569a74dbd3633c9db906114e5
7
- data.tar.gz: e77819ccd2be263b02958fcee71ce9228c93c7dee62fabbea61711fbbdbffa997bb9a85a7f668cf8bb596b7a98a773ace560ad5075c46e70f20cd302b354b0fe
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.0
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.alpha6)
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.2.3)
17
- activesupport (= 7.0.2.3)
16
+ activejob (7.0.3)
17
+ activesupport (= 7.0.3)
18
18
  globalid (>= 0.3.6)
19
- activesupport (7.0.2.3)
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.14.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.1)
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.0)
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.9, >= 1.9.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.10
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
- CONCURRENCY = 4
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 = 60 * 2
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
- Process.kill('TERM', pid)
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
- result = success? ? "\e[#{32}m#{'OK'}\e[0m" : "\e[#{31}m#{'FAILED'}\e[0m"
110
-
111
- puts "#{result} #{name}"
112
-
113
- unless success?
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
- # Fail all if any of the tests does not have expected exit code
208
- raise IntegrationTestError unless finished_scenarios.all?(&:success?)
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
@@ -16,6 +16,9 @@ services:
16
16
  KAFKA_CREATE_TOPICS:
17
17
  "integrations_0_02:2:1,\
18
18
  integrations_1_02:2:1,\
19
+ integrations_2_02:2:1,\
20
+ integrations_3_02:2:1,\
21
+ integrations_4_02:2:1,\
19
22
  integrations_0_03:3:1,\
20
23
  integrations_1_03:3:1,\
21
24
  integrations_2_03:3:1,\
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.license = 'LGPL-3.0'
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'
@@ -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::RoutingExtensions
18
- ::Karafka::Routing::Proxy.include ::Karafka::ActiveJob::RoutingExtensions
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 :pause
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
- pause.reset
52
+ pause_tracker.reset
28
53
 
29
54
  # Mark as consumed only if manual offset management is not on
30
- return if topic.manual_offset_management
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
- client.pause(topic.name, messages.first.partition, @seek_offset || messages.first.offset)
44
- pause.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 = MessagesBuffer.new
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
- @offsetting = true
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
- return unless @offsetting
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
- pause_msg = Messages::Seek.new(topic, partition, offset)
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