karafka 2.2.13 → 2.3.0.alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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