karafka 2.2.13 → 2.3.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/workflows/ci.yml +38 -12
  4. data/.ruby-version +1 -1
  5. data/CHANGELOG.md +161 -125
  6. data/Gemfile.lock +12 -12
  7. data/README.md +0 -2
  8. data/SECURITY.md +23 -0
  9. data/config/locales/errors.yml +7 -1
  10. data/config/locales/pro_errors.yml +22 -0
  11. data/docker-compose.yml +3 -1
  12. data/karafka.gemspec +2 -2
  13. data/lib/karafka/admin/acl.rb +287 -0
  14. data/lib/karafka/admin.rb +118 -16
  15. data/lib/karafka/app.rb +12 -3
  16. data/lib/karafka/base_consumer.rb +32 -31
  17. data/lib/karafka/cli/base.rb +1 -1
  18. data/lib/karafka/connection/client.rb +94 -84
  19. data/lib/karafka/connection/conductor.rb +28 -0
  20. data/lib/karafka/connection/listener.rb +165 -46
  21. data/lib/karafka/connection/listeners_batch.rb +5 -11
  22. data/lib/karafka/connection/manager.rb +72 -0
  23. data/lib/karafka/connection/messages_buffer.rb +12 -0
  24. data/lib/karafka/connection/proxy.rb +17 -0
  25. data/lib/karafka/connection/status.rb +75 -0
  26. data/lib/karafka/contracts/config.rb +14 -10
  27. data/lib/karafka/contracts/consumer_group.rb +9 -1
  28. data/lib/karafka/contracts/topic.rb +3 -1
  29. data/lib/karafka/errors.rb +13 -0
  30. data/lib/karafka/instrumentation/assignments_tracker.rb +96 -0
  31. data/lib/karafka/instrumentation/callbacks/rebalance.rb +10 -7
  32. data/lib/karafka/instrumentation/logger_listener.rb +3 -9
  33. data/lib/karafka/instrumentation/notifications.rb +19 -9
  34. data/lib/karafka/instrumentation/vendors/appsignal/metrics_listener.rb +31 -28
  35. data/lib/karafka/instrumentation/vendors/datadog/logger_listener.rb +22 -3
  36. data/lib/karafka/instrumentation/vendors/datadog/metrics_listener.rb +15 -12
  37. data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +39 -36
  38. data/lib/karafka/pro/base_consumer.rb +47 -0
  39. data/lib/karafka/pro/connection/manager.rb +300 -0
  40. data/lib/karafka/pro/connection/multiplexing/listener.rb +40 -0
  41. data/lib/karafka/pro/instrumentation/performance_tracker.rb +85 -0
  42. data/lib/karafka/pro/iterator/tpl_builder.rb +1 -1
  43. data/lib/karafka/pro/iterator.rb +1 -6
  44. data/lib/karafka/pro/loader.rb +16 -2
  45. data/lib/karafka/pro/processing/coordinator.rb +2 -1
  46. data/lib/karafka/pro/processing/executor.rb +37 -0
  47. data/lib/karafka/pro/processing/expansions_selector.rb +32 -0
  48. data/lib/karafka/pro/processing/jobs/periodic.rb +41 -0
  49. data/lib/karafka/pro/processing/jobs/periodic_non_blocking.rb +32 -0
  50. data/lib/karafka/pro/processing/jobs_builder.rb +14 -3
  51. data/lib/karafka/pro/processing/offset_metadata/consumer.rb +44 -0
  52. data/lib/karafka/pro/processing/offset_metadata/fetcher.rb +131 -0
  53. data/lib/karafka/pro/processing/offset_metadata/listener.rb +46 -0
  54. data/lib/karafka/pro/processing/schedulers/base.rb +143 -0
  55. data/lib/karafka/pro/processing/schedulers/default.rb +107 -0
  56. data/lib/karafka/pro/processing/strategies/aj/lrj_mom_vp.rb +1 -1
  57. data/lib/karafka/pro/processing/strategies/default.rb +136 -3
  58. data/lib/karafka/pro/processing/strategies/dlq/default.rb +35 -0
  59. data/lib/karafka/pro/processing/strategies/lrj/default.rb +1 -1
  60. data/lib/karafka/pro/processing/strategies/lrj/mom.rb +1 -1
  61. data/lib/karafka/pro/processing/strategies/vp/default.rb +60 -26
  62. data/lib/karafka/pro/processing/virtual_offset_manager.rb +41 -11
  63. data/lib/karafka/pro/routing/features/long_running_job/topic.rb +2 -0
  64. data/lib/karafka/pro/routing/features/multiplexing/config.rb +38 -0
  65. data/lib/karafka/pro/routing/features/multiplexing/contracts/topic.rb +114 -0
  66. data/lib/karafka/pro/routing/features/multiplexing/patches/contracts/consumer_group.rb +42 -0
  67. data/lib/karafka/pro/routing/features/multiplexing/proxy.rb +38 -0
  68. data/lib/karafka/pro/routing/features/multiplexing/subscription_group.rb +42 -0
  69. data/lib/karafka/pro/routing/features/multiplexing/subscription_groups_builder.rb +40 -0
  70. data/lib/karafka/pro/routing/features/multiplexing.rb +59 -0
  71. data/lib/karafka/pro/routing/features/non_blocking_job/topic.rb +32 -0
  72. data/lib/karafka/pro/routing/features/non_blocking_job.rb +37 -0
  73. data/lib/karafka/pro/routing/features/offset_metadata/config.rb +33 -0
  74. data/lib/karafka/pro/routing/features/offset_metadata/contracts/topic.rb +42 -0
  75. data/lib/karafka/pro/routing/features/offset_metadata/topic.rb +65 -0
  76. data/lib/karafka/pro/routing/features/offset_metadata.rb +40 -0
  77. data/lib/karafka/pro/routing/features/patterns/contracts/consumer_group.rb +4 -0
  78. data/lib/karafka/pro/routing/features/patterns/detector.rb +18 -10
  79. data/lib/karafka/pro/routing/features/periodic_job/config.rb +37 -0
  80. data/lib/karafka/pro/routing/features/periodic_job/contracts/topic.rb +44 -0
  81. data/lib/karafka/pro/routing/features/periodic_job/topic.rb +94 -0
  82. data/lib/karafka/pro/routing/features/periodic_job.rb +27 -0
  83. data/lib/karafka/pro/routing/features/virtual_partitions/config.rb +1 -0
  84. data/lib/karafka/pro/routing/features/virtual_partitions/contracts/topic.rb +1 -0
  85. data/lib/karafka/pro/routing/features/virtual_partitions/topic.rb +7 -2
  86. data/lib/karafka/process.rb +5 -3
  87. data/lib/karafka/processing/coordinator.rb +5 -1
  88. data/lib/karafka/processing/executor.rb +43 -13
  89. data/lib/karafka/processing/executors_buffer.rb +22 -7
  90. data/lib/karafka/processing/jobs/base.rb +19 -2
  91. data/lib/karafka/processing/jobs/consume.rb +3 -3
  92. data/lib/karafka/processing/jobs/idle.rb +5 -0
  93. data/lib/karafka/processing/jobs/revoked.rb +5 -0
  94. data/lib/karafka/processing/jobs/shutdown.rb +5 -0
  95. data/lib/karafka/processing/jobs_queue.rb +19 -8
  96. data/lib/karafka/processing/schedulers/default.rb +42 -0
  97. data/lib/karafka/processing/strategies/base.rb +13 -4
  98. data/lib/karafka/processing/strategies/default.rb +23 -7
  99. data/lib/karafka/processing/strategies/dlq.rb +36 -0
  100. data/lib/karafka/processing/worker.rb +4 -1
  101. data/lib/karafka/routing/builder.rb +12 -2
  102. data/lib/karafka/routing/consumer_group.rb +5 -5
  103. data/lib/karafka/routing/features/base.rb +44 -8
  104. data/lib/karafka/routing/features/dead_letter_queue/config.rb +6 -1
  105. data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +1 -0
  106. data/lib/karafka/routing/features/dead_letter_queue/topic.rb +9 -2
  107. data/lib/karafka/routing/proxy.rb +4 -3
  108. data/lib/karafka/routing/subscription_group.rb +2 -2
  109. data/lib/karafka/routing/subscription_groups_builder.rb +11 -2
  110. data/lib/karafka/routing/topic.rb +8 -10
  111. data/lib/karafka/routing/topics.rb +1 -1
  112. data/lib/karafka/runner.rb +13 -3
  113. data/lib/karafka/server.rb +5 -9
  114. data/lib/karafka/setup/config.rb +21 -1
  115. data/lib/karafka/status.rb +23 -14
  116. data/lib/karafka/templates/karafka.rb.erb +7 -0
  117. data/lib/karafka/time_trackers/partition_usage.rb +56 -0
  118. data/lib/karafka/version.rb +1 -1
  119. data.tar.gz.sig +0 -0
  120. metadata +47 -13
  121. metadata.gz.sig +0 -0
  122. data/lib/karafka/connection/consumer_group_coordinator.rb +0 -48
  123. data/lib/karafka/pro/performance_tracker.rb +0 -84
  124. data/lib/karafka/pro/processing/scheduler.rb +0 -74
  125. data/lib/karafka/processing/scheduler.rb +0 -38
data/lib/karafka/app.rb CHANGED
@@ -36,6 +36,13 @@ module Karafka
36
36
  # Just a nicer name for the consumer groups
37
37
  alias routes consumer_groups
38
38
 
39
+ # Returns current assignments of this process. Both topics and partitions
40
+ #
41
+ # @return [Hash<Karafka::Routing::Topic, Array<Integer>>]
42
+ def assignments
43
+ Instrumentation::AssignmentsTracker.instance.current
44
+ end
45
+
39
46
  # Allow for easier status management via `Karafka::App` by aliasing status methods here
40
47
  Status::STATES.each do |state, transition|
41
48
  class_eval <<~RUBY, __FILE__, __LINE__ + 1
@@ -68,9 +75,11 @@ module Karafka
68
75
  monitor
69
76
  pro?
70
77
  ].each do |delegated|
71
- define_method(delegated) do
72
- Karafka.send(delegated)
73
- end
78
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
79
+ def #{delegated}
80
+ Karafka.#{delegated}
81
+ end
82
+ RUBY
74
83
  end
75
84
  end
76
85
  end
@@ -11,6 +11,9 @@ module Karafka
11
11
 
12
12
  def_delegators :@coordinator, :topic, :partition
13
13
 
14
+ def_delegators :producer, :produce_async, :produce_sync, :produce_many_async,
15
+ :produce_many_sync
16
+
14
17
  # @return [String] id of the current consumer
15
18
  attr_reader :id
16
19
  # @return [Karafka::Routing::Topic] topic to which a given consumer is subscribed
@@ -34,16 +37,9 @@ module Karafka
34
37
  # @note This should not be used by the end users as it is part of the lifecycle of things and
35
38
  # not as a part of the public api. This should not perform any extensive operations as it is
36
39
  # blocking and running in the listener thread.
37
- def on_before_enqueue
40
+ def on_before_schedule_consume
38
41
  @used = true
39
- handle_before_enqueue
40
- rescue StandardError => e
41
- Karafka.monitor.instrument(
42
- 'error.occurred',
43
- error: e,
44
- caller: self,
45
- type: 'consumer.before_enqueue.error'
46
- )
42
+ handle_before_schedule_consume
47
43
  end
48
44
 
49
45
  # Can be used to run preparation code in the worker
@@ -59,13 +55,6 @@ module Karafka
59
55
  # We run this after the full metadata setup, so we can use all the messages information
60
56
  # if needed
61
57
  handle_before_consume
62
- rescue StandardError => e
63
- Karafka.monitor.instrument(
64
- 'error.occurred',
65
- error: e,
66
- caller: self,
67
- type: 'consumer.before_consume.error'
68
- )
69
58
  end
70
59
 
71
60
  # Executes the default consumer flow.
@@ -94,13 +83,13 @@ module Karafka
94
83
  # not as part of the public api.
95
84
  def on_after_consume
96
85
  handle_after_consume
97
- rescue StandardError => e
98
- Karafka.monitor.instrument(
99
- 'error.occurred',
100
- error: e,
101
- caller: self,
102
- type: 'consumer.after_consume.error'
103
- )
86
+ end
87
+
88
+ # Can be used to run code prior to scheduling of idle execution
89
+ #
90
+ # @private
91
+ def on_before_schedule_idle
92
+ handle_before_schedule_idle
104
93
  end
105
94
 
106
95
  # Trigger method for running on idle runs without messages
@@ -108,13 +97,13 @@ module Karafka
108
97
  # @private
109
98
  def on_idle
110
99
  handle_idle
111
- rescue StandardError => e
112
- Karafka.monitor.instrument(
113
- 'error.occurred',
114
- error: e,
115
- caller: self,
116
- type: 'consumer.idle.error'
117
- )
100
+ end
101
+
102
+ # Can be used to run code prior to scheduling of revoked execution
103
+ #
104
+ # @private
105
+ def on_before_schedule_revoked
106
+ handle_before_schedule_revoked
118
107
  end
119
108
 
120
109
  # Trigger method for running on partition revocation.
@@ -131,6 +120,13 @@ module Karafka
131
120
  )
132
121
  end
133
122
 
123
+ # Can be used to run code prior to scheduling of revoked execution
124
+ #
125
+ # @private
126
+ def on_before_schedule_shutdown
127
+ handle_before_schedule_shutdown
128
+ end
129
+
134
130
  # Trigger method for running on shutdown.
135
131
  #
136
132
  # @private
@@ -226,9 +222,14 @@ module Karafka
226
222
  # @param manual_seek [Boolean] Flag to differentiate between user seek and system/strategy
227
223
  # based seek. User seek operations should take precedence over system actions, hence we need
228
224
  # to know who invoked it.
225
+ # @param reset_offset [Boolean] should we reset offset when seeking backwards. It is false by
226
+ # default to prevent marking in the offset that was earlier than the highest marked offset
227
+ # for given consumer group. It can be set to true if we want to reprocess data once again and
228
+ # want to make sure that the marking starts from where we moved to.
229
229
  # @note Please note, that if you are seeking to a time offset, getting the offset is blocking
230
- def seek(offset, manual_seek = true)
230
+ def seek(offset, manual_seek = true, reset_offset: false)
231
231
  coordinator.manual_seek if manual_seek
232
+ coordinator.seek_offset = nil if reset_offset
232
233
 
233
234
  client.seek(
234
235
  Karafka::Messages::Seek.new(
@@ -101,7 +101,7 @@ module Karafka
101
101
 
102
102
  # @return [Array<String>] names and aliases for command matching
103
103
  def names
104
- ((@aliases || []) << name).flatten.map(&:to_s)
104
+ ((@aliases || []) << name).flatten.map(&:to_s).uniq
105
105
  end
106
106
  end
107
107
  end
@@ -10,6 +10,10 @@ module Karafka
10
10
  class Client
11
11
  attr_reader :rebalance_manager
12
12
 
13
+ # @return [Karafka::Routing::SubscriptionGroup] subscription group to which this client
14
+ # belongs to
15
+ attr_reader :subscription_group
16
+
13
17
  # @return [String] underlying consumer name
14
18
  # @note Consumer name may change in case we regenerate it
15
19
  attr_reader :name
@@ -20,16 +24,7 @@ module Karafka
20
24
  # How many times should we retry polling in case of a failure
21
25
  MAX_POLL_RETRIES = 20
22
26
 
23
- # 1 minute of max wait for the first rebalance before a forceful attempt
24
- # This applies only to a case when a short-lived Karafka instance with a client would be
25
- # closed before first rebalance. Mitigates a librdkafka bug.
26
- COOPERATIVE_STICKY_MAX_WAIT = 60_000
27
-
28
- # We want to make sure we never close several clients in the same moment to prevent
29
- # potential race conditions and other issues
30
- SHUTDOWN_MUTEX = Mutex.new
31
-
32
- private_constant :MAX_POLL_RETRIES, :SHUTDOWN_MUTEX, :COOPERATIVE_STICKY_MAX_WAIT
27
+ private_constant :MAX_POLL_RETRIES
33
28
 
34
29
  # Creates a new consumer instance.
35
30
  #
@@ -45,12 +40,8 @@ module Karafka
45
40
  @buffer = RawMessagesBuffer.new
46
41
  @tick_interval = ::Karafka::App.config.internal.tick_interval
47
42
  @rebalance_manager = RebalanceManager.new(@subscription_group.id)
48
- @rebalance_callback = Instrumentation::Callbacks::Rebalance.new(
49
- @subscription_group.id,
50
- @subscription_group.consumer_group.id
51
- )
43
+ @rebalance_callback = Instrumentation::Callbacks::Rebalance.new(@subscription_group)
52
44
  @events_poller = Helpers::IntervalRunner.new { events_poll }
53
- @kafka = build_consumer
54
45
  # There are few operations that can happen in parallel from the listener threads as well
55
46
  # as from the workers. They are not fully thread-safe because they may be composed out of
56
47
  # few calls to Kafka or out of few internal state changes. That is why we mutex them.
@@ -125,13 +116,19 @@ module Karafka
125
116
  # Stores offset for a given partition of a given topic based on the provided message.
126
117
  #
127
118
  # @param message [Karafka::Messages::Message]
128
- def store_offset(message)
129
- internal_store_offset(message)
119
+ # @param offset_metadata [String, nil] offset storage metadata or nil if none
120
+ def store_offset(message, offset_metadata = nil)
121
+ internal_store_offset(message, offset_metadata)
130
122
  end
131
123
 
132
124
  # @return [Boolean] true if our current assignment has been lost involuntarily.
133
125
  def assignment_lost?
134
- @kafka.assignment_lost?
126
+ kafka.assignment_lost?
127
+ end
128
+
129
+ # @return [Rdkafka::Consumer::TopicPartitionList] current active assignment
130
+ def assignment
131
+ kafka.assignment
135
132
  end
136
133
 
137
134
  # Commits the offset on a current consumer in a non-blocking or blocking way.
@@ -202,7 +199,7 @@ module Karafka
202
199
 
203
200
  @paused_tpls[topic][partition] = tpl
204
201
 
205
- @kafka.pause(tpl)
202
+ kafka.pause(tpl)
206
203
 
207
204
  # If offset is not provided, will pause where it finished.
208
205
  # This makes librdkafka not purge buffers and can provide significant network savings
@@ -243,43 +240,23 @@ module Karafka
243
240
  partition: partition
244
241
  )
245
242
 
246
- @kafka.resume(tpl)
243
+ kafka.resume(tpl)
247
244
  end
248
245
  end
249
246
 
250
247
  # Gracefully stops topic consumption.
251
- #
252
- # @note Stopping running consumers without a really important reason is not recommended
253
- # as until all the consumers are stopped, the server will keep running serving only
254
- # part of the messages
255
248
  def stop
256
- # This ensures, that we do not stop the underlying client until it passes the first
257
- # rebalance for cooperative-sticky. Otherwise librdkafka may crash
258
- #
259
- # We set a timeout just in case the rebalance would never happen or would last for an
260
- # extensive time period.
261
- #
262
- # @see https://github.com/confluentinc/librdkafka/issues/4312
249
+ # In case of cooperative-sticky, there is a bug in librdkafka that may hang it.
250
+ # To mitigate it we first need to unsubscribe so we will not receive any assignments and
251
+ # only then we should be good to go.
252
+ # @see https://github.com/confluentinc/librdkafka/issues/4527
263
253
  if @subscription_group.kafka[:'partition.assignment.strategy'] == 'cooperative-sticky'
264
- active_wait = false
265
-
266
- (COOPERATIVE_STICKY_MAX_WAIT / 100).times do
267
- # If we're past the first rebalance, no need to wait
268
- if @rebalance_manager.active?
269
- # We give it a a bit of time because librdkafka has a tendency to do some-post
270
- # callback work that from its perspective is still under rebalance
271
- sleep(5) if active_wait
272
-
273
- break
274
- end
275
-
276
- active_wait = true
277
-
278
- # poll to trigger potential rebalances that could occur during stopping and to trigger
279
- # potential callbacks
280
- poll(100)
254
+ unsubscribe
281
255
 
256
+ until assignment.empty?
282
257
  sleep(0.1)
258
+
259
+ ping
283
260
  end
284
261
  end
285
262
 
@@ -288,33 +265,40 @@ module Karafka
288
265
 
289
266
  # Marks given message as consumed.
290
267
  #
291
- # @param [Karafka::Messages::Message] message that we want to mark as processed
268
+ # @param message [Karafka::Messages::Message] message that we want to mark as processed
269
+ # @param metadata [String, nil] offset storage metadata or nil if none
292
270
  # @return [Boolean] true if successful. False if we no longer own given partition
293
271
  # @note This method won't trigger automatic offsets commits, rather relying on the offset
294
272
  # check-pointing trigger that happens with each batch processed. It will however check the
295
273
  # `librdkafka` assignment ownership to increase accuracy for involuntary revocations.
296
- def mark_as_consumed(message)
297
- store_offset(message) && !assignment_lost?
274
+ def mark_as_consumed(message, metadata = nil)
275
+ store_offset(message, metadata) && !assignment_lost?
298
276
  end
299
277
 
300
278
  # Marks a given message as consumed and commits the offsets in a blocking way.
301
279
  #
302
- # @param [Karafka::Messages::Message] message that we want to mark as processed
280
+ # @param message [Karafka::Messages::Message] message that we want to mark as processed
281
+ # @param metadata [String, nil] offset storage metadata or nil if none
303
282
  # @return [Boolean] true if successful. False if we no longer own given partition
304
- def mark_as_consumed!(message)
305
- return false unless mark_as_consumed(message)
283
+ def mark_as_consumed!(message, metadata = nil)
284
+ return false unless mark_as_consumed(message, metadata)
306
285
 
307
286
  commit_offsets!
308
287
  end
309
288
 
310
289
  # Closes and resets the client completely.
311
290
  def reset
312
- close
291
+ Karafka.monitor.instrument(
292
+ 'client.reset',
293
+ caller: self,
294
+ subscription_group: @subscription_group
295
+ ) do
296
+ close
313
297
 
314
- @events_poller.reset
315
- @closed = false
316
- @paused_tpls.clear
317
- @kafka = build_consumer
298
+ @events_poller.reset
299
+ @closed = false
300
+ @paused_tpls.clear
301
+ end
318
302
  end
319
303
 
320
304
  # Runs a single poll on the main queue and consumer queue ignoring all the potential errors
@@ -340,7 +324,27 @@ module Karafka
340
324
  # @note It is non-blocking when timeout 0 and will not wait if queue empty. It costs up to
341
325
  # 2ms when no callbacks are triggered.
342
326
  def events_poll(timeout = 0)
343
- @kafka.events_poll(timeout)
327
+ kafka.events_poll(timeout)
328
+ end
329
+
330
+ # Returns pointer to the consumer group metadata. It is used only in the context of
331
+ # exactly-once-semantics in transactions, this is why it is never remapped to Ruby
332
+ # @return [FFI::Pointer]
333
+ def consumer_group_metadata_pointer
334
+ kafka.consumer_group_metadata_pointer
335
+ end
336
+
337
+ # Return the current committed offset per partition for this consumer group.
338
+ # The offset field of each requested partition will either be set to stored offset or to
339
+ # -1001 in case there was no stored offset for that partition.
340
+ #
341
+ # @param tpl [Rdkafka::Consumer::TopicPartitionList] for which we want to get committed
342
+ # @return [Rdkafka::Consumer::TopicPartitionList]
343
+ # @raise [Rdkafka::RdkafkaError] When getting the committed positions fails.
344
+ # @note It is recommended to use this only on rebalances to get positions with metadata
345
+ # when working with metadata as this is synchronous
346
+ def committed(tpl = nil)
347
+ Proxy.new(kafka).committed(tpl)
344
348
  end
345
349
 
346
350
  private
@@ -349,9 +353,10 @@ module Karafka
349
353
  #
350
354
  # Non thread-safe offset storing method
351
355
  # @param message [Karafka::Messages::Message]
356
+ # @param metadata [String, nil] offset storage metadata or nil if none
352
357
  # @return [Boolean] true if we could store the offset (if we still own the partition)
353
- def internal_store_offset(message)
354
- @kafka.store_offset(message)
358
+ def internal_store_offset(message, metadata)
359
+ kafka.store_offset(message, metadata)
355
360
  true
356
361
  rescue Rdkafka::RdkafkaError => e
357
362
  return false if e.code == :assignment_lost
@@ -367,7 +372,7 @@ module Karafka
367
372
  # even when no stored, because with sync commit, it refreshes the ownership state of the
368
373
  # consumer in a sync way.
369
374
  def internal_commit_offsets(async: true)
370
- @kafka.commit(nil, async)
375
+ kafka.commit(nil, async)
371
376
 
372
377
  true
373
378
  rescue Rdkafka::RdkafkaError => e
@@ -404,7 +409,7 @@ module Karafka
404
409
  message.partition => message.offset
405
410
  )
406
411
 
407
- proxy = Proxy.new(@kafka)
412
+ proxy = Proxy.new(kafka)
408
413
 
409
414
  # Now we can overwrite the seek message offset with our resolved offset and we can
410
415
  # then seek to the appropriate message
@@ -426,29 +431,29 @@ module Karafka
426
431
  # seeking and pausing
427
432
  return if message.offset == topic_partition_position(message.topic, message.partition)
428
433
 
429
- @kafka.seek(message)
434
+ kafka.seek(message)
430
435
  end
431
436
 
432
437
  # Commits the stored offsets in a sync way and closes the consumer.
433
438
  def close
434
- # Allow only one client to be closed at the same time
435
- SHUTDOWN_MUTEX.synchronize do
436
- # Once client is closed, we should not close it again
437
- # This could only happen in case of a race-condition when forceful shutdown happens
438
- # and triggers this from a different thread
439
- return if @closed
439
+ # Once client is closed, we should not close it again
440
+ # This could only happen in case of a race-condition when forceful shutdown happens
441
+ # and triggers this from a different thread
442
+ return if @closed
440
443
 
441
- @closed = true
444
+ @closed = true
442
445
 
443
- # Remove callbacks runners that were registered
444
- ::Karafka::Core::Instrumentation.statistics_callbacks.delete(@subscription_group.id)
445
- ::Karafka::Core::Instrumentation.error_callbacks.delete(@subscription_group.id)
446
+ return unless @kafka
446
447
 
447
- @kafka.close
448
- @buffer.clear
449
- # @note We do not clear rebalance manager here as we may still have revocation info
450
- # here that we want to consider valid prior to running another reconnection
451
- end
448
+ # Remove callbacks runners that were registered
449
+ ::Karafka::Core::Instrumentation.statistics_callbacks.delete(@subscription_group.id)
450
+ ::Karafka::Core::Instrumentation.error_callbacks.delete(@subscription_group.id)
451
+
452
+ kafka.close
453
+ @kafka = nil
454
+ @buffer.clear
455
+ # @note We do not clear rebalance manager here as we may still have revocation info
456
+ # here that we want to consider valid prior to running another reconnection
452
457
  end
453
458
 
454
459
  # Unsubscribes from all the subscriptions
@@ -456,7 +461,7 @@ module Karafka
456
461
  # @note We do not re-raise since this is supposed to be only used on close and can be safely
457
462
  # ignored. We do however want to instrument on it
458
463
  def unsubscribe
459
- @kafka.unsubscribe
464
+ kafka.unsubscribe
460
465
  rescue ::Rdkafka::RdkafkaError => e
461
466
  Karafka.monitor.instrument(
462
467
  'error.occurred',
@@ -470,7 +475,7 @@ module Karafka
470
475
  # @param partition [Integer]
471
476
  # @return [Rdkafka::Consumer::TopicPartitionList]
472
477
  def topic_partition_list(topic, partition)
473
- rdkafka_partition = @kafka
478
+ rdkafka_partition = kafka
474
479
  .assignment
475
480
  .to_h[topic]
476
481
  &.detect { |part| part.partition == partition }
@@ -489,7 +494,7 @@ module Karafka
489
494
  rd_partition = ::Rdkafka::Consumer::Partition.new(partition, nil, 0)
490
495
  tpl = ::Rdkafka::Consumer::TopicPartitionList.new(topic => [rd_partition])
491
496
 
492
- @kafka.position(tpl).to_h.fetch(topic).first.offset || -1
497
+ kafka.position(tpl).to_h.fetch(topic).first.offset || -1
493
498
  end
494
499
 
495
500
  # Performs a single poll operation and handles retries and errors
@@ -517,7 +522,7 @@ module Karafka
517
522
  # blocking events from being handled.
518
523
  poll_tick = timeout > @tick_interval ? @tick_interval : timeout
519
524
 
520
- result = @kafka.poll(poll_tick)
525
+ result = kafka.poll(poll_tick)
521
526
 
522
527
  # If we've got a message, we can return it
523
528
  return result if result
@@ -644,6 +649,11 @@ module Karafka
644
649
 
645
650
  @buffer.uniq!
646
651
  end
652
+
653
+ # @return [Rdkafka::Consumer] librdkafka consumer instance
654
+ def kafka
655
+ @kafka ||= build_consumer
656
+ end
647
657
  end
648
658
  end
649
659
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Connection
5
+ # Conductor is responsible for time orchestration of listeners manager.
6
+ # It blocks when manager is not needed as there were no state changes that could cause any
7
+ # listeners config changes and unblocks when things change or when certain time passed.
8
+ # The time based unblocking allows for building of complex managers that could be state aware
9
+ class Conductor
10
+ # @param max_interval [Integer] after how many milliseconds of doing nothing should we wake
11
+ # up the manager despite no state changes
12
+ def initialize(max_interval = 30_000)
13
+ @lock = RUBY_VERSION < '3.2' ? Processing::TimedQueue.new : Queue.new
14
+ @timeout = max_interval / 1_000.0
15
+ end
16
+
17
+ # Waits in a blocking way until it is time to manage listeners
18
+ def wait
19
+ @lock.pop(timeout: @timeout)
20
+ end
21
+
22
+ # Releases wait lock on state change
23
+ def signal
24
+ @lock << true
25
+ end
26
+ end
27
+ end
28
+ end