Module: Karafka::Pro::Processing::Strategies::Dlq::Default

Includes:
Karafka::Pro::Processing::Strategies::Default
Included in:
Aj::DlqLrjMom, Ftr, FtrMom, Lrj, Mom, Vp
Defined in:
lib/karafka/pro/processing/strategies/dlq/default.rb

Overview

Only dead letter queue enabled

Constant Summary collapse

FEATURES =

Features for this strategy

%i[
  dead_letter_queue
].freeze

Instance Method Summary collapse

Methods included from Karafka::Pro::Processing::Strategies::Default

#handle_before_consume, #handle_before_schedule_consume, #handle_before_schedule_tick, #handle_consume, #handle_revoked, #handle_tick, #mark_in_transaction, #store_offset_metadata, #transaction

Methods included from Karafka::Processing::Strategies::Default

#commit_offsets, #commit_offsets!, #handle_before_consume, #handle_consume, #handle_eofed, #handle_idle, #handle_revoked, #handle_shutdown

Methods included from Karafka::Processing::Strategies::Base

#handle_before_consume, #handle_consume, #handle_idle, #handle_revoked, #handle_shutdown

Instance Method Details

#apply_dlq_flowObject

Runs the DLQ strategy and based on it it performs certain operations

In case of :skip and :dispatch will run the exact flow provided in a block In case of :retry always #retry_after_pause is applied



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/karafka/pro/processing/strategies/dlq/default.rb', line 215

def apply_dlq_flow
  flow = topic.dead_letter_queue.strategy.call(errors_tracker, attempt)

  case flow
  when :retry
    retry_after_pause

    return
  when :skip
    @_dispatch_to_dlq = false
  when :dispatch
    @_dispatch_to_dlq = true
  else
    raise Karafka::UnsupportedCaseError, flow
  end

  yield

  # We reset the pause to indicate we will now consider it as "ok".
  coordinator.pause_tracker.reset

  # Always backoff after DLQ dispatch even on skip to prevent overloads on errors
  pause(coordinator.seek_offset, nil, false)
end

#build_dlq_message(skippable_message) ⇒ Hash

Returns dispatch DLQ message.

Parameters:

Returns:

  • (Hash)

    dispatch DLQ message



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/karafka/pro/processing/strategies/dlq/default.rb', line 155

def build_dlq_message(skippable_message)
  original_partition = skippable_message.partition.to_s

  dlq_message = {
    topic: topic.dead_letter_queue.topic,
    key: original_partition,
    payload: skippable_message.raw_payload,
    headers: skippable_message.raw_headers.merge(
      'original_topic' => topic.name,
      'original_partition' => original_partition,
      'original_offset' => skippable_message.offset.to_s,
      'original_consumer_group' => topic.consumer_group.id,
      'original_key' => skippable_message.raw_key.to_s,
      'original_attempts' => attempt.to_s
    )
  }

  # Optional method user can define in consumer to enhance the dlq message hash with
  # some extra details if needed or to replace payload, etc
  if respond_to?(:enhance_dlq_message, true)
    enhance_dlq_message(
      dlq_message,
      skippable_message
    )
  end

  dlq_message
end

#dispatch_if_needed_and_mark_as_consumedObject

Dispatches the message to the DLQ (when needed and when applicable based on settings) and marks this message as consumed for non MOM flows.

If producer is transactional and config allows, uses transaction to do that



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/karafka/pro/processing/strategies/dlq/default.rb', line 133

def dispatch_if_needed_and_mark_as_consumed
  skippable_message, = find_skippable_message

  dispatch = lambda do
    dispatch_to_dlq(skippable_message) if dispatch_to_dlq?

    if mark_after_dispatch?
      mark_dispatched_to_dlq(skippable_message)
    else
      coordinator.seek_offset = skippable_message.offset + 1
    end
  end

  if dispatch_in_a_transaction?
    transaction { dispatch.call }
  else
    dispatch.call
  end
end

#dispatch_in_a_transaction?Boolean

Returns should we use a transaction to move the data to the DLQ. We can do it only when producer is transactional and configuration for DLQ transactional dispatches is not set to false.

Returns:

  • (Boolean)

    should we use a transaction to move the data to the DLQ. We can do it only when producer is transactional and configuration for DLQ transactional dispatches is not set to false.



197
198
199
# File 'lib/karafka/pro/processing/strategies/dlq/default.rb', line 197

def dispatch_in_a_transaction?
  producer.transactional? && topic.dead_letter_queue.transactional?
end

#dispatch_to_dlq(skippable_message) ⇒ Object

Moves the broken message into a separate queue defined via the settings

Parameters:

Raises:



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/karafka/pro/processing/strategies/dlq/default.rb', line 107

def dispatch_to_dlq(skippable_message)
  # DLQ should never try to dispatch a message that was cleaned. It message was
  # cleaned, we will not have all the needed data. If you see this error, it means
  # that your processing flow is not as expected and you have cleaned message that
  # should not be cleaned as it should go to the DLQ
  raise(Cleaner::Errors::MessageCleanedError) if skippable_message.cleaned?

  producer.public_send(
    topic.dead_letter_queue.dispatch_method,
    build_dlq_message(
      skippable_message
    )
  )

  # Notify about dispatch on the events bus
  Karafka.monitor.instrument(
    'dead_letter_queue.dispatched',
    caller: self,
    message: skippable_message
  )
end

#dispatch_to_dlq?Boolean

Returns should we dispatch the message to DLQ or not. When the dispatch topic is set to false, we will skip the dispatch, effectively ignoring the broken message without taking any action.

Returns:

  • (Boolean)

    should we dispatch the message to DLQ or not. When the dispatch topic is set to false, we will skip the dispatch, effectively ignoring the broken message without taking any action.



187
188
189
190
191
192
# File 'lib/karafka/pro/processing/strategies/dlq/default.rb', line 187

def dispatch_to_dlq?
  return false unless topic.dead_letter_queue.topic
  return false unless @_dispatch_to_dlq

  true
end

#find_skippable_messageArray<Karafka::Messages::Message, Boolean>

Finds the message may want to skip (all, starting from first)

Returns:

  • (Array<Karafka::Messages::Message, Boolean>)

    message we may want to skip and information if this message was from marked offset or figured out via mom flow



91
92
93
94
95
96
97
98
99
100
101
# File 'lib/karafka/pro/processing/strategies/dlq/default.rb', line 91

def find_skippable_message
  skippable_message = messages.find do |msg|
    coordinator.marked? && msg.offset == coordinator.seek_offset
  end

  # If we don't have the message matching the last comitted offset, it means that
  # user operates with manual offsets and we're beyond the batch in which things
  # broke for the first time. Then we skip the first (as no markings) and we
  # move on one by one.
  skippable_message ? [skippable_message, true] : [messages.first, false]
end

#handle_after_consumeObject

When we encounter non-recoverable message, we skip it and go on with our lives



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/karafka/pro/processing/strategies/dlq/default.rb', line 69

def handle_after_consume
  coordinator.on_finished do |last_group_message|
    return if revoked?

    if coordinator.success?
      coordinator.pause_tracker.reset

      return if coordinator.manual_pause?

      mark_as_consumed(last_group_message)
    else
      apply_dlq_flow do
        dispatch_if_needed_and_mark_as_consumed
      end
    end
  end
end

#mark_after_dispatch?Boolean

Returns should we mark given message as consumed after dispatch. For default non MOM strategies if user did not explicitly tell us not to, we mark it. Default is nil, which means true in this case. If user provided alternative value, we go with it.

Returns:

  • (Boolean)

    should we mark given message as consumed after dispatch. For default non MOM strategies if user did not explicitly tell us not to, we mark it. Default is nil, which means true in this case. If user provided alternative value, we go with it.



205
206
207
208
209
# File 'lib/karafka/pro/processing/strategies/dlq/default.rb', line 205

def mark_after_dispatch?
  return true if topic.dead_letter_queue.mark_after_dispatch.nil?

  topic.dead_letter_queue.mark_after_dispatch
end

#mark_as_consumed(message, offset_metadata = @_current_offset_metadata) ⇒ Object

Override of the standard #mark_as_consumed in order to handle the pause tracker reset in case DLQ is marked as fully independent. When DLQ is marked independent, any offset marking causes the pause count tracker to reset. This is useful when the error is not due to the collective batch operations state but due to intermediate “crawling” errors that move with it

Parameters:

  • message (Messages::Message)
  • offset_metadata (String, nil) (defaults to: @_current_offset_metadata)

See Also:

  • for more details


38
39
40
41
42
43
44
45
46
47
48
# File 'lib/karafka/pro/processing/strategies/dlq/default.rb', line 38

def mark_as_consumed(message,  = @_current_offset_metadata)
  return super unless retrying?
  return super unless topic.dead_letter_queue.independent?
  return false unless super

  coordinator.pause_tracker.reset

  true
ensure
  @_current_offset_metadata = nil
end

#mark_as_consumed!(message, offset_metadata = @_current_offset_metadata) ⇒ Object

Override of the standard #mark_as_consumed!. Resets the pause tracker count in case DLQ was configured with the independent flag.

Parameters:

  • message (Messages::Message)
  • offset_metadata (String, nil) (defaults to: @_current_offset_metadata)

See Also:

  • for more details


56
57
58
59
60
61
62
63
64
65
66
# File 'lib/karafka/pro/processing/strategies/dlq/default.rb', line 56

def mark_as_consumed!(message,  = @_current_offset_metadata)
  return super unless retrying?
  return super unless topic.dead_letter_queue.independent?
  return false unless super

  coordinator.pause_tracker.reset

  true
ensure
  @_current_offset_metadata = nil
end

#mark_dispatched_to_dlq(skippable_message) ⇒ Object

Marks message that went to DLQ (if applicable) based on the requested method

Parameters:



242
243
244
245
246
247
248
249
250
251
252
# File 'lib/karafka/pro/processing/strategies/dlq/default.rb', line 242

def mark_dispatched_to_dlq(skippable_message)
  case topic.dead_letter_queue.marking_method
  when :mark_as_consumed
    mark_as_consumed(skippable_message)
  when :mark_as_consumed!
    mark_as_consumed!(skippable_message)
  else
    # This should never happen. Bug if encountered. Please report
    raise Karafka::Errors::UnsupportedCaseError
  end
end