karafka 2.3.2 → 2.4.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/workflows/ci.yml +12 -38
  4. data/CHANGELOG.md +65 -0
  5. data/Gemfile +6 -3
  6. data/Gemfile.lock +25 -23
  7. data/README.md +2 -2
  8. data/bin/integrations +1 -1
  9. data/config/locales/errors.yml +24 -2
  10. data/config/locales/pro_errors.yml +19 -0
  11. data/karafka.gemspec +4 -2
  12. data/lib/active_job/queue_adapters/karafka_adapter.rb +2 -0
  13. data/lib/karafka/admin/configs/config.rb +81 -0
  14. data/lib/karafka/admin/configs/resource.rb +88 -0
  15. data/lib/karafka/admin/configs.rb +103 -0
  16. data/lib/karafka/admin.rb +200 -89
  17. data/lib/karafka/base_consumer.rb +2 -2
  18. data/lib/karafka/cli/info.rb +9 -7
  19. data/lib/karafka/cli/server.rb +7 -7
  20. data/lib/karafka/cli/topics/align.rb +109 -0
  21. data/lib/karafka/cli/topics/base.rb +66 -0
  22. data/lib/karafka/cli/topics/create.rb +35 -0
  23. data/lib/karafka/cli/topics/delete.rb +30 -0
  24. data/lib/karafka/cli/topics/migrate.rb +31 -0
  25. data/lib/karafka/cli/topics/plan.rb +169 -0
  26. data/lib/karafka/cli/topics/repartition.rb +41 -0
  27. data/lib/karafka/cli/topics/reset.rb +18 -0
  28. data/lib/karafka/cli/topics.rb +13 -123
  29. data/lib/karafka/connection/client.rb +62 -37
  30. data/lib/karafka/connection/listener.rb +22 -17
  31. data/lib/karafka/connection/proxy.rb +93 -4
  32. data/lib/karafka/connection/status.rb +14 -2
  33. data/lib/karafka/contracts/config.rb +36 -1
  34. data/lib/karafka/contracts/topic.rb +1 -1
  35. data/lib/karafka/deserializers/headers.rb +15 -0
  36. data/lib/karafka/deserializers/key.rb +15 -0
  37. data/lib/karafka/deserializers/payload.rb +16 -0
  38. data/lib/karafka/embedded.rb +2 -0
  39. data/lib/karafka/helpers/async.rb +5 -2
  40. data/lib/karafka/helpers/colorize.rb +6 -0
  41. data/lib/karafka/instrumentation/callbacks/oauthbearer_token_refresh.rb +29 -0
  42. data/lib/karafka/instrumentation/logger_listener.rb +23 -3
  43. data/lib/karafka/instrumentation/notifications.rb +10 -0
  44. data/lib/karafka/instrumentation/vendors/appsignal/client.rb +16 -2
  45. data/lib/karafka/instrumentation/vendors/datadog/metrics_listener.rb +34 -4
  46. data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +20 -0
  47. data/lib/karafka/messages/batch_metadata.rb +1 -1
  48. data/lib/karafka/messages/builders/batch_metadata.rb +1 -1
  49. data/lib/karafka/messages/builders/message.rb +10 -6
  50. data/lib/karafka/messages/message.rb +2 -1
  51. data/lib/karafka/messages/metadata.rb +20 -4
  52. data/lib/karafka/messages/parser.rb +1 -1
  53. data/lib/karafka/pro/base_consumer.rb +12 -23
  54. data/lib/karafka/pro/encryption/cipher.rb +7 -3
  55. data/lib/karafka/pro/encryption/contracts/config.rb +1 -0
  56. data/lib/karafka/pro/encryption/errors.rb +4 -1
  57. data/lib/karafka/pro/encryption/messages/middleware.rb +13 -11
  58. data/lib/karafka/pro/encryption/messages/parser.rb +22 -20
  59. data/lib/karafka/pro/encryption/setup/config.rb +5 -0
  60. data/lib/karafka/pro/iterator/expander.rb +2 -1
  61. data/lib/karafka/pro/iterator/tpl_builder.rb +38 -0
  62. data/lib/karafka/pro/iterator.rb +28 -2
  63. data/lib/karafka/pro/loader.rb +3 -0
  64. data/lib/karafka/pro/processing/coordinator.rb +15 -2
  65. data/lib/karafka/pro/processing/expansions_selector.rb +2 -0
  66. data/lib/karafka/pro/processing/jobs_queue.rb +122 -5
  67. data/lib/karafka/pro/processing/periodic_job/consumer.rb +67 -0
  68. data/lib/karafka/pro/processing/piping/consumer.rb +126 -0
  69. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom.rb +1 -1
  70. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom_vp.rb +1 -1
  71. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom.rb +1 -1
  72. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom_vp.rb +1 -1
  73. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom.rb +1 -1
  74. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom_vp.rb +1 -1
  75. data/lib/karafka/pro/processing/strategies/aj/dlq_mom.rb +1 -1
  76. data/lib/karafka/pro/processing/strategies/aj/dlq_mom_vp.rb +1 -1
  77. data/lib/karafka/pro/processing/strategies/aj/lrj_mom_vp.rb +2 -0
  78. data/lib/karafka/pro/processing/strategies/default.rb +5 -1
  79. data/lib/karafka/pro/processing/strategies/dlq/default.rb +21 -5
  80. data/lib/karafka/pro/processing/strategies/lrj/default.rb +2 -0
  81. data/lib/karafka/pro/processing/strategies/lrj/mom.rb +2 -0
  82. data/lib/karafka/pro/processing/subscription_groups_coordinator.rb +52 -0
  83. data/lib/karafka/pro/routing/features/direct_assignments/config.rb +27 -0
  84. data/lib/karafka/pro/routing/features/direct_assignments/contracts/consumer_group.rb +53 -0
  85. data/lib/karafka/pro/routing/features/direct_assignments/contracts/topic.rb +108 -0
  86. data/lib/karafka/pro/routing/features/direct_assignments/subscription_group.rb +77 -0
  87. data/lib/karafka/pro/routing/features/direct_assignments/topic.rb +69 -0
  88. data/lib/karafka/pro/routing/features/direct_assignments.rb +25 -0
  89. data/lib/karafka/pro/routing/features/patterns/builder.rb +1 -1
  90. data/lib/karafka/pro/routing/features/swarm/config.rb +31 -0
  91. data/lib/karafka/pro/routing/features/swarm/contracts/routing.rb +76 -0
  92. data/lib/karafka/pro/routing/features/swarm/contracts/topic.rb +78 -0
  93. data/lib/karafka/pro/routing/features/swarm/topic.rb +77 -0
  94. data/lib/karafka/pro/routing/features/swarm.rb +36 -0
  95. data/lib/karafka/pro/swarm/liveness_listener.rb +20 -0
  96. data/lib/karafka/processing/coordinator.rb +17 -8
  97. data/lib/karafka/processing/coordinators_buffer.rb +5 -2
  98. data/lib/karafka/processing/executor.rb +6 -2
  99. data/lib/karafka/processing/executors_buffer.rb +5 -2
  100. data/lib/karafka/processing/jobs_queue.rb +9 -4
  101. data/lib/karafka/processing/strategies/aj_dlq_mom.rb +1 -1
  102. data/lib/karafka/processing/strategies/default.rb +7 -1
  103. data/lib/karafka/processing/strategies/dlq.rb +17 -2
  104. data/lib/karafka/processing/workers_batch.rb +4 -1
  105. data/lib/karafka/routing/builder.rb +6 -2
  106. data/lib/karafka/routing/consumer_group.rb +2 -1
  107. data/lib/karafka/routing/features/dead_letter_queue/config.rb +5 -0
  108. data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +8 -0
  109. data/lib/karafka/routing/features/dead_letter_queue/topic.rb +10 -2
  110. data/lib/karafka/routing/features/deserializers/config.rb +18 -0
  111. data/lib/karafka/routing/features/deserializers/contracts/topic.rb +31 -0
  112. data/lib/karafka/routing/features/deserializers/topic.rb +51 -0
  113. data/lib/karafka/routing/features/deserializers.rb +11 -0
  114. data/lib/karafka/routing/proxy.rb +9 -14
  115. data/lib/karafka/routing/router.rb +11 -2
  116. data/lib/karafka/routing/subscription_group.rb +22 -1
  117. data/lib/karafka/routing/topic.rb +0 -1
  118. data/lib/karafka/runner.rb +1 -1
  119. data/lib/karafka/setup/config.rb +51 -10
  120. data/lib/karafka/status.rb +7 -8
  121. data/lib/karafka/swarm/manager.rb +15 -3
  122. data/lib/karafka/swarm/node.rb +3 -3
  123. data/lib/karafka/swarm/pidfd.rb +20 -4
  124. data/lib/karafka/swarm/supervisor.rb +25 -8
  125. data/lib/karafka/templates/karafka.rb.erb +28 -1
  126. data/lib/karafka/version.rb +1 -1
  127. data.tar.gz.sig +0 -0
  128. metadata +42 -12
  129. metadata.gz.sig +0 -0
  130. data/lib/karafka/routing/consumer_mapper.rb +0 -23
  131. data/lib/karafka/serialization/json/deserializer.rb +0 -19
  132. data/lib/karafka/time_trackers/partition_usage.rb +0 -56
@@ -20,7 +20,9 @@ module Karafka
20
20
  # the end. It also allows for signaling, when a given message should be last out of certain
21
21
  # partition, but we still want to continue iterating in other messages.
22
22
  #
23
- # It does **not** create a consumer group and does not have any offset management.
23
+ # It does **not** create a consumer group and does not have any offset management until first
24
+ # consumer offset marking happens. So can be use for quick seeks as well as iterative,
25
+ # repetitive data fetching from rake, etc.
24
26
  class Iterator
25
27
  # A simple API allowing to iterate over topic/partition data, without having to subscribe
26
28
  # and deal with rebalances. This API allows for multi-partition streaming and is optimized
@@ -92,6 +94,7 @@ module Karafka
92
94
  end
93
95
  end
94
96
 
97
+ @current_consumer.commit_offsets(async: false) if @stored_offsets
95
98
  @current_message = nil
96
99
  @current_consumer = nil
97
100
  end
@@ -127,6 +130,29 @@ module Karafka
127
130
  )
128
131
  end
129
132
 
133
+ # Stops all the iterating
134
+ # @note `break` can also be used but in such cases commits stored async will not be flushed
135
+ # to Kafka. This is why `#stop` is the recommended method.
136
+ def stop
137
+ @stopped = true
138
+ end
139
+
140
+ # Marks given message as consumed.
141
+ #
142
+ # @param message [Karafka::Messages::Message] message that we want to mark as processed
143
+ def mark_as_consumed(message)
144
+ @current_consumer.store_offset(message, nil)
145
+ @stored_offsets = true
146
+ end
147
+
148
+ # Marks given message as consumed and commits offsets
149
+ #
150
+ # @param message [Karafka::Messages::Message] message that we want to mark as processed
151
+ def mark_as_consumed!(message)
152
+ mark_as_consumed(message)
153
+ @current_consumer.commit_offsets(async: false)
154
+ end
155
+
130
156
  private
131
157
 
132
158
  # @return [Rdkafka::Consumer::Message, nil] message or nil if nothing to do
@@ -158,7 +184,7 @@ module Karafka
158
184
  # Do we have all the data we wanted or did every topic partition has reached eof.
159
185
  # @return [Boolean]
160
186
  def done?
161
- @stopped_partitions >= @total_partitions
187
+ (@stopped_partitions >= @total_partitions) || @stopped
162
188
  end
163
189
  end
164
190
  end
@@ -63,6 +63,9 @@ module Karafka
63
63
  # @param config [Karafka::Core::Configurable::Node]
64
64
  def post_setup_all(config)
65
65
  features.each { |feature| feature.post_setup(config) }
66
+
67
+ # We initialize it here so we don't initialize it during multi-threading work
68
+ Processing::SubscriptionGroupsCoordinator.instance
66
69
  end
67
70
 
68
71
  private
@@ -99,8 +99,9 @@ module Karafka
99
99
  end
100
100
 
101
101
  # @return [Boolean] is the coordinated work finished or not
102
+ # @note Used only in the consume operation context
102
103
  def finished?
103
- @running_jobs.zero?
104
+ @running_jobs[:consume].zero?
104
105
  end
105
106
 
106
107
  # Runs synchronized code once for a collective of virtual partitions prior to work being
@@ -122,7 +123,7 @@ module Karafka
122
123
  end
123
124
  end
124
125
 
125
- # Runs once when all the work that is suppose to be coordinated is finished
126
+ # Runs given code once when all the work that is suppose to be coordinated is finished
126
127
  # It runs once per all the coordinated jobs and should be used to run any type of post
127
128
  # jobs coordination processing execution
128
129
  def on_finished
@@ -143,6 +144,18 @@ module Karafka
143
144
  end
144
145
  end
145
146
 
147
+ # @param interval [Integer] milliseconds of activity
148
+ # @return [Boolean] was this partition in activity within last `interval` milliseconds
149
+ # @note Will return true also if currently active
150
+ def active_within?(interval)
151
+ # its always active if there's any job related to this coordinator that is still
152
+ # enqueued or running
153
+ return true if @running_jobs.values.any?(:positive?)
154
+
155
+ # Otherwise we check last time any job of this coordinator was active
156
+ @changed_at + interval > monotonic_now
157
+ end
158
+
146
159
  private
147
160
 
148
161
  # Checks if given action is executable once. If it is and true is returned, this method
@@ -23,7 +23,9 @@ module Karafka
23
23
  def find(topic)
24
24
  # Start with the non-pro expansions
25
25
  expansions = super
26
+ expansions << Pro::Processing::Piping::Consumer
26
27
  expansions << Pro::Processing::OffsetMetadata::Consumer if topic.offset_metadata?
28
+ expansions << Pro::Processing::PeriodicJob::Consumer if topic.periodic_job?
27
29
  expansions
28
30
  end
29
31
  end
@@ -19,17 +19,36 @@ module Karafka
19
19
  #
20
20
  # Aside from the OSS queue capabilities it allows for jobless locking for advanced schedulers
21
21
  class JobsQueue < Karafka::Processing::JobsQueue
22
+ include Core::Helpers::Time
23
+
22
24
  attr_accessor :in_processing
23
25
 
26
+ # How long should we keep async lock (31 years)
27
+ WAIT_TIMEOUT = 10_000_000_000
28
+
29
+ private_constant :WAIT_TIMEOUT
30
+
24
31
  # @return [Karafka::Pro::Processing::JobsQueue]
25
32
  def initialize
26
33
  super
27
34
 
28
35
  @in_waiting = Hash.new { |h, k| h[k] = [] }
36
+ @locks = Hash.new { |h, k| h[k] = {} }
37
+ @async_locking = false
29
38
 
30
39
  @statistics[:waiting] = 0
31
40
  end
32
41
 
42
+ # Registers semaphore and a lock hash
43
+ #
44
+ # @param group_id [String]
45
+ def register(group_id)
46
+ super
47
+ @mutex.synchronize do
48
+ @locks[group_id]
49
+ end
50
+ end
51
+
33
52
  # Method that allows us to lock queue on a given subscription group without enqueuing the a
34
53
  # job. This can be used when building complex schedulers that want to postpone enqueuing
35
54
  # before certain conditions are met.
@@ -64,6 +83,48 @@ module Karafka
64
83
  end
65
84
  end
66
85
 
86
+ # Allows for explicit locking of the queue of a given subscription group.
87
+ #
88
+ # This can be used for cross-topic synchronization.
89
+ #
90
+ # @param group_id [String] id of the group we want to lock
91
+ # @param lock_id [Object] unique id we want to use to identify our lock
92
+ # @param timeout [Integer] number of ms how long this lock should be valid. Useful for
93
+ # auto-expiring locks used to delay further processing without explicit pausing on
94
+ # the consumer
95
+ #
96
+ # @note We do not raise `Errors::JobsQueueSynchronizationError` similar to `#lock` here
97
+ # because we want to have ability to prolong time limited locks
98
+ def lock_async(group_id, lock_id, timeout: WAIT_TIMEOUT)
99
+ return if @queue.closed?
100
+
101
+ @async_locking = true
102
+
103
+ @mutex.synchronize do
104
+ @locks[group_id][lock_id] = monotonic_now + timeout
105
+
106
+ # We need to tick so our new time sensitive lock can reload time constraints on sleep
107
+ tick(group_id)
108
+ end
109
+ end
110
+
111
+ # Allows for explicit unlocking of locked queue of a group
112
+ #
113
+ # @param group_id [String] id of the group we want to unlock
114
+ # @param lock_id [Object] unique id we want to use to identify our lock
115
+ #
116
+ def unlock_async(group_id, lock_id)
117
+ @mutex.synchronize do
118
+ if @locks[group_id].delete(lock_id)
119
+ tick(group_id)
120
+
121
+ return
122
+ end
123
+
124
+ raise(Errors::JobsQueueSynchronizationError, [group_id, lock_id])
125
+ end
126
+ end
127
+
67
128
  # Clears the processing states for a provided group. Useful when a recovery happens and we
68
129
  # need to clean up state but only for a given subscription group.
69
130
  #
@@ -74,6 +135,8 @@ module Karafka
74
135
 
75
136
  @statistics[:waiting] -= @in_waiting[group_id].size
76
137
  @in_waiting[group_id].clear
138
+ @locks[group_id].clear
139
+ @async_locking = false
77
140
 
78
141
  # We unlock it just in case it was blocked when clearing started
79
142
  tick(group_id)
@@ -87,21 +150,75 @@ module Karafka
87
150
  def empty?(group_id)
88
151
  @mutex.synchronize do
89
152
  @in_processing[group_id].empty? &&
90
- @in_waiting[group_id].empty?
153
+ @in_waiting[group_id].empty? &&
154
+ !locked_async?(group_id)
155
+ end
156
+ end
157
+
158
+ # Blocks when there are things in the queue in a given group and waits until all the
159
+ # blocking jobs from a given group are completed or any of the locks times out
160
+ # @param group_id [String] id of the group in which jobs we're interested.
161
+ # @see `Karafka::Processing::JobsQueue`
162
+ #
163
+ # @note Because checking that async locking is on happens on regular ticking, first lock
164
+ # on a group can take up to one tick. That is expected.
165
+ #
166
+ # @note This implementation takes into consideration temporary async locks that can happen.
167
+ # Thanks to the fact that we use the minimum lock time as a timeout, we do not have to
168
+ # wait a whole ticking period to unlock async locks.
169
+ def wait(group_id)
170
+ return super unless @async_locking
171
+
172
+ # We do not generalize this flow because this one is more expensive as it has to allocate
173
+ # extra objects. That's why we only use it when locks are actually in use
174
+ base_interval = tick_interval / 1_000.0
175
+
176
+ while wait?(group_id)
177
+ yield if block_given?
178
+
179
+ now = monotonic_now
180
+
181
+ wait_times = @locks[group_id].values.map! do |lock_time|
182
+ # Convert ms to seconds, seconds are required by Ruby queue engine
183
+ (lock_time - now) / 1_000
184
+ end
185
+
186
+ wait_times.delete_if(&:negative?)
187
+ wait_times << base_interval
188
+
189
+ @semaphores.fetch(group_id).pop(timeout: wait_times.min)
91
190
  end
92
191
  end
93
192
 
94
193
  private
95
194
 
195
+ # Tells us if given group is locked
196
+ #
197
+ # @param group_id [String] id of the group in which we're interested.
198
+ # @return [Boolean] true if there are any active locks on the group, otherwise false
199
+ def locked_async?(group_id)
200
+ return false unless @async_locking
201
+
202
+ group = @locks[group_id]
203
+
204
+ return false if group.empty?
205
+
206
+ now = monotonic_now
207
+
208
+ group.delete_if { |_, wait_timeout| wait_timeout < now }
209
+
210
+ !group.empty?
211
+ end
212
+
96
213
  # @param group_id [String] id of the group in which jobs we're interested.
97
214
  # @return [Boolean] should we keep waiting or not
98
215
  # @note We do not wait for non-blocking jobs. Their flow should allow for `poll` running
99
216
  # as they may exceed `max.poll.interval`
100
217
  def wait?(group_id)
101
- !(
102
- @in_processing[group_id].all?(&:non_blocking?) &&
103
- @in_waiting[group_id].all?(&:non_blocking?)
104
- )
218
+ return true unless @in_processing[group_id].all?(&:non_blocking?)
219
+ return true unless @in_waiting[group_id].all?(&:non_blocking?)
220
+
221
+ locked_async?(group_id)
105
222
  end
106
223
  end
107
224
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This Karafka component is a Pro component under a commercial license.
4
+ # This Karafka component is NOT licensed under LGPL.
5
+ #
6
+ # All of the commercial components are present in the lib/karafka/pro directory of this
7
+ # repository and their usage requires commercial license agreement.
8
+ #
9
+ # Karafka has also commercial-friendly license, commercial support and commercial components.
10
+ #
11
+ # By sending a pull request to the pro components, you are agreeing to transfer the copyright of
12
+ # your code to Maciej Mensfeld.
13
+
14
+ module Karafka
15
+ module Pro
16
+ module Processing
17
+ # Namespace for periodic jobs related processing APIs
18
+ module PeriodicJob
19
+ # Consumer extra methods useful only when periodic jobs are in use
20
+ module Consumer
21
+ class << self
22
+ # Defines an empty `#tick` method if not present
23
+ #
24
+ # We define it that way due to our injection strategy flow.
25
+ #
26
+ # @param consumer_singleton_class [Karafka::BaseConsumer] consumer singleton class
27
+ # that is being enriched with periodic jobs API
28
+ def included(consumer_singleton_class)
29
+ # Do not define empty tick method on consumer if it already exists
30
+ # We only define it when it does not exist to have empty periodic ticking
31
+ #
32
+ # We need to check both cases (public and private) since user is not expected to
33
+ # have this method public
34
+ return if consumer_singleton_class.instance_methods.include?(:tick)
35
+ return if consumer_singleton_class.private_instance_methods.include?(:tick)
36
+
37
+ # Create empty ticking method
38
+ consumer_singleton_class.class_eval do
39
+ def tick; end
40
+ end
41
+ end
42
+ end
43
+
44
+ # Runs the on-schedule tick periodic operations
45
+ # This method is an alias but is part of the naming convention used for other flows, this
46
+ # is why we do not reference the `handle_before_schedule_tick` directly
47
+ def on_before_schedule_tick
48
+ handle_before_schedule_tick
49
+ end
50
+
51
+ # Used by the executor to trigger consumer tick
52
+ # @private
53
+ def on_tick
54
+ handle_tick
55
+ rescue StandardError => e
56
+ Karafka.monitor.instrument(
57
+ 'error.occurred',
58
+ error: e,
59
+ caller: self,
60
+ type: 'consumer.tick.error'
61
+ )
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This Karafka component is a Pro component under a commercial license.
4
+ # This Karafka component is NOT licensed under LGPL.
5
+ #
6
+ # All of the commercial components are present in the lib/karafka/pro directory of this
7
+ # repository and their usage requires commercial license agreement.
8
+ #
9
+ # Karafka has also commercial-friendly license, commercial support and commercial components.
10
+ #
11
+ # By sending a pull request to the pro components, you are agreeing to transfer the copyright of
12
+ # your code to Maciej Mensfeld.
13
+
14
+ module Karafka
15
+ module Pro
16
+ module Processing
17
+ # All code needed for messages piping in Karafka
18
+ module Piping
19
+ # Consumer piping functionality
20
+ #
21
+ # It provides way to pipe data in a consistent way with extra traceability headers similar
22
+ # to those in the enhanced DLQ.
23
+ module Consumer
24
+ # Empty hash to save on memory allocations
25
+ EMPTY_HASH = {}.freeze
26
+
27
+ private_constant :EMPTY_HASH
28
+
29
+ # Pipes given message to the provided topic with expected details. Useful for
30
+ # pass-through operations where deserialization is not needed. Upon usage it will include
31
+ # all the original headers + meta headers about the source of message.
32
+ #
33
+ # @param topic [String, Symbol] where we want to send the message
34
+ # @param message [Karafka::Messages::Message] original message to pipe
35
+ #
36
+ # @note It will NOT deserialize the payload so it is fast
37
+ #
38
+ # @note We assume that there can be different number of partitions in the target topic,
39
+ # this is why we use `key` based on the original topic partition number and not the
40
+ # partition id itself. This will not utilize partitions beyond the number of partitions
41
+ # of original topic, but will accommodate for topics with less partitions.
42
+ def pipe_async(topic:, message:)
43
+ produce_async(
44
+ build_pipe_message(topic: topic, message: message)
45
+ )
46
+ end
47
+
48
+ # Sync version of pipe for one message
49
+ #
50
+ # @param topic [String, Symbol] where we want to send the message
51
+ # @param message [Karafka::Messages::Message] original message to pipe
52
+ # @see [#pipe_async]
53
+ def pipe_sync(topic:, message:)
54
+ produce_sync(
55
+ build_pipe_message(topic: topic, message: message)
56
+ )
57
+ end
58
+
59
+ # Async multi-message pipe
60
+ #
61
+ # @param topic [String, Symbol] where we want to send the message
62
+ # @param messages [Array<Karafka::Messages::Message>] original messages to pipe
63
+ #
64
+ # @note If transactional producer in use and dispatch is not wrapped with a transaction,
65
+ # it will automatically wrap the dispatch with a transaction
66
+ def pipe_many_async(topic:, messages:)
67
+ messages = messages.map do |message|
68
+ build_pipe_message(topic: topic, message: message)
69
+ end
70
+
71
+ produce_many_async(messages)
72
+ end
73
+
74
+ # Sync multi-message pipe
75
+ #
76
+ # @param topic [String, Symbol] where we want to send the message
77
+ # @param messages [Array<Karafka::Messages::Message>] original messages to pipe
78
+ #
79
+ # @note If transactional producer in use and dispatch is not wrapped with a transaction,
80
+ # it will automatically wrap the dispatch with a transaction
81
+ def pipe_many_sync(topic:, messages:)
82
+ messages = messages.map do |message|
83
+ build_pipe_message(topic: topic, message: message)
84
+ end
85
+
86
+ produce_many_sync(messages)
87
+ end
88
+
89
+ private
90
+
91
+ # @param topic [String, Symbol] where we want to send the message
92
+ # @param message [Karafka::Messages::Message] original message to pipe
93
+ # @return [Hash] hash with message to pipe.
94
+ #
95
+ # @note If you need to alter this, please define the `#enhance_pipe_message` method
96
+ def build_pipe_message(topic:, message:)
97
+ original_partition = message.partition.to_s
98
+
99
+ pipe_message = {
100
+ topic: topic,
101
+ key: original_partition,
102
+ payload: message.raw_payload,
103
+ headers: message.headers.merge(
104
+ 'original_topic' => message.topic,
105
+ 'original_partition' => original_partition,
106
+ 'original_offset' => message.offset.to_s,
107
+ 'original_consumer_group' => self.topic.consumer_group.id
108
+ )
109
+ }
110
+
111
+ # Optional method user can define in consumer to enhance the dlq message hash with
112
+ # some extra details if needed or to replace payload, etc
113
+ if respond_to?(:enhance_pipe_message, true)
114
+ enhance_pipe_message(
115
+ pipe_message,
116
+ message
117
+ )
118
+ end
119
+
120
+ pipe_message
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -55,7 +55,7 @@ module Karafka
55
55
  apply_dlq_flow do
56
56
  skippable_message, = find_skippable_message
57
57
  dispatch_to_dlq(skippable_message) if dispatch_to_dlq?
58
- mark_as_consumed(skippable_message)
58
+ mark_dispatched_to_dlq(skippable_message)
59
59
  end
60
60
  end
61
61
  end
@@ -61,7 +61,7 @@ module Karafka
61
61
  apply_dlq_flow do
62
62
  skippable_message, = find_skippable_message
63
63
  dispatch_to_dlq(skippable_message) if dispatch_to_dlq?
64
- mark_as_consumed(skippable_message)
64
+ mark_dispatched_to_dlq(skippable_message)
65
65
  end
66
66
  end
67
67
  end
@@ -57,7 +57,7 @@ module Karafka
57
57
  # We can commit the offset here because we know that we skip it "forever" and
58
58
  # since AJ consumer commits the offset after each job, we also know that the
59
59
  # previous job was successful
60
- mark_as_consumed(skippable_message)
60
+ mark_dispatched_to_dlq(skippable_message)
61
61
  end
62
62
  end
63
63
  end
@@ -55,7 +55,7 @@ module Karafka
55
55
  # We can commit the offset here because we know that we skip it "forever" and
56
56
  # since AJ consumer commits the offset after each job, we also know that the
57
57
  # previous job was successful
58
- mark_as_consumed(skippable_message)
58
+ mark_dispatched_to_dlq(skippable_message)
59
59
  end
60
60
  end
61
61
  end
@@ -51,7 +51,7 @@ module Karafka
51
51
  apply_dlq_flow do
52
52
  skippable_message, = find_skippable_message
53
53
  dispatch_to_dlq(skippable_message) if dispatch_to_dlq?
54
- mark_as_consumed(skippable_message)
54
+ mark_dispatched_to_dlq(skippable_message)
55
55
  end
56
56
  end
57
57
  end
@@ -55,7 +55,7 @@ module Karafka
55
55
  apply_dlq_flow do
56
56
  skippable_message, = find_skippable_message
57
57
  dispatch_to_dlq(skippable_message) if dispatch_to_dlq?
58
- mark_as_consumed(skippable_message)
58
+ mark_dispatched_to_dlq(skippable_message)
59
59
  end
60
60
  end
61
61
  end
@@ -49,7 +49,7 @@ module Karafka
49
49
  # We can commit the offset here because we know that we skip it "forever" and
50
50
  # since AJ consumer commits the offset after each job, we also know that the
51
51
  # previous job was successful
52
- mark_as_consumed(skippable_message)
52
+ mark_dispatched_to_dlq(skippable_message)
53
53
  end
54
54
  end
55
55
  end
@@ -54,7 +54,7 @@ module Karafka
54
54
  # Aj::DlqMom
55
55
  skippable_message, = find_skippable_message
56
56
  dispatch_to_dlq(skippable_message) if dispatch_to_dlq?
57
- mark_as_consumed(skippable_message)
57
+ mark_dispatched_to_dlq(skippable_message)
58
58
  end
59
59
  end
60
60
  end
@@ -73,6 +73,8 @@ module Karafka
73
73
  Karafka.monitor.instrument('consumer.revoked', caller: self) do
74
74
  revoked
75
75
  end
76
+ ensure
77
+ coordinator.decrement(:revoked)
76
78
  end
77
79
  end
78
80
  end
@@ -220,7 +220,7 @@ module Karafka
220
220
  ensure
221
221
  # We need to decrease number of jobs that this coordinator coordinates as it has
222
222
  # finished
223
- coordinator.decrement
223
+ coordinator.decrement(:consume)
224
224
  end
225
225
 
226
226
  # Standard flow without any features
@@ -254,6 +254,8 @@ module Karafka
254
254
  Karafka.monitor.instrument('consumer.revoked', caller: self) do
255
255
  revoked
256
256
  end
257
+ ensure
258
+ coordinator.decrement(:revoked)
257
259
  end
258
260
 
259
261
  # No action needed for the tick standard flow
@@ -269,6 +271,8 @@ module Karafka
269
271
  Karafka.monitor.instrument('consumer.ticked', caller: self) do
270
272
  tick
271
273
  end
274
+ ensure
275
+ coordinator.decrement(:periodic)
272
276
  end
273
277
  end
274
278
  end
@@ -111,7 +111,8 @@ module Karafka
111
111
  # should not be cleaned as it should go to the DLQ
112
112
  raise(Cleaner::Errors::MessageCleanedError) if skippable_message.cleaned?
113
113
 
114
- producer.produce_async(
114
+ producer.public_send(
115
+ topic.dead_letter_queue.dispatch_method,
115
116
  build_dlq_message(
116
117
  skippable_message
117
118
  )
@@ -134,7 +135,7 @@ module Karafka
134
135
 
135
136
  dispatch = lambda do
136
137
  dispatch_to_dlq(skippable_message) if dispatch_to_dlq?
137
- mark_as_consumed(skippable_message)
138
+ mark_dispatched_to_dlq(skippable_message)
138
139
  end
139
140
 
140
141
  if dispatch_in_a_transaction?
@@ -157,7 +158,8 @@ module Karafka
157
158
  'original_topic' => topic.name,
158
159
  'original_partition' => original_partition,
159
160
  'original_offset' => skippable_message.offset.to_s,
160
- 'original_consumer_group' => topic.consumer_group.id
161
+ 'original_consumer_group' => topic.consumer_group.id,
162
+ 'original_attempts' => attempt.to_s
161
163
  )
162
164
  }
163
165
 
@@ -210,14 +212,28 @@ module Karafka
210
212
  raise Karafka::UnsupportedCaseError, flow
211
213
  end
212
214
 
215
+ yield
216
+
213
217
  # We reset the pause to indicate we will now consider it as "ok".
214
218
  coordinator.pause_tracker.reset
215
219
 
216
- yield
217
-
218
220
  # Always backoff after DLQ dispatch even on skip to prevent overloads on errors
219
221
  pause(coordinator.seek_offset, nil, false)
220
222
  end
223
+
224
+ # Marks message that went to DLQ (if applicable) based on the requested method
225
+ # @param skippable_message [Karafka::Messages::Message]
226
+ def mark_dispatched_to_dlq(skippable_message)
227
+ case topic.dead_letter_queue.marking_method
228
+ when :mark_as_consumed
229
+ mark_as_consumed(skippable_message)
230
+ when :mark_as_consumed!
231
+ mark_as_consumed!(skippable_message)
232
+ else
233
+ # This should never happen. Bug if encountered. Please report
234
+ raise Karafka::Errors::UnsupportedCaseError
235
+ end
236
+ end
221
237
  end
222
238
  end
223
239
  end
@@ -76,6 +76,8 @@ module Karafka
76
76
  Karafka.monitor.instrument('consumer.revoked', caller: self) do
77
77
  revoked
78
78
  end
79
+ ensure
80
+ coordinator.decrement(:revoked)
79
81
  end
80
82
 
81
83
  # Allows for LRJ to synchronize its work. It may be needed because LRJ can run