Module: Deimos::ActiveRecordConsume::BatchConsumption

Included in:
Deimos::ActiveRecordConsumer
Defined in:
lib/deimos/active_record_consume/batch_consumption.rb,
sig/defs.rbs

Overview

Methods for consuming batches of messages and saving them to the database in bulk ActiveRecord operations.

Instance Method Summary collapse

Instance Method Details

#compact_messages(batch) ⇒ ::Array[Message]

Compact a batch of messages, taking only the last message for each unique key.

@param batch — Batch of messages.

@return — Compacted batch.

Parameters:

Returns:



104
105
106
107
108
# File 'lib/deimos/active_record_consume/batch_consumption.rb', line 104

def compact_messages(batch)
  return batch unless batch.first&.key.present?

  batch.reverse.uniq(&:key).reverse!
end

#consume_batch(payloads, metadata) ⇒ void

This method returns an undefined value.

Handle a batch of Kafka messages. Batches are split into "slices", which are groups of independent messages that can be processed together in a single database operation. If two messages in a batch have the same key, we cannot process them in the same operation as they would interfere with each other. Thus they are split

@param payloads — Decoded payloads

@param metadata — Information about batch, including keys.

Parameters:



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/deimos/active_record_consume/batch_consumption.rb', line 26

def consume_batch(payloads, )
  messages = payloads.
    zip([:keys]).
    map { |p, k| Deimos::Message.new(p, nil, key: k) }

  tag = [:topic]
  Deimos.config.tracer.active_span.set_tag('topic', tag)

  Deimos.instrument('ar_consumer.consume_batch', tag) do
    if @compacted || self.class.config[:no_keys]
      update_database(compact_messages(messages))
    else
      uncompacted_update(messages)
    end
  end
end

#deleted_query(records) ⇒ ActiveRecord::Relation

Create an ActiveRecord relation that matches all of the passed records. Used for bulk deletion.

@param records — List of messages.

@return — Matching relation.

Parameters:

Returns:

  • (ActiveRecord::Relation)


83
84
85
86
87
88
89
# File 'lib/deimos/active_record_consume/batch_consumption.rb', line 83

def deleted_query(records)
  keys = records.
    map { |m| record_key(m.key)[@klass.primary_key] }.
    reject(&:nil?)

  @klass.unscoped.where(@klass.primary_key => keys)
end

#key_columns(_klass) ⇒ ::Array[String]

Get the set of attribute names that uniquely identify messages in the batch. Requires at least one record.

@param records — Non-empty list of messages.

@return — List of attribute names.

Parameters:

Returns:

  • (::Array[String])


51
52
53
# File 'lib/deimos/active_record_consume/batch_consumption.rb', line 51

def key_columns(_klass)
  nil
end

#record_key(key) ⇒ ::Hash[untyped, untyped]

Get unique key for the ActiveRecord instance from the incoming key. Override this method (with super) to customize the set of attributes that uniquely identifies each record in the database.

@param key — The encoded key.

@return — The key attributes.

Parameters:

  • key (String)

Returns:

  • (::Hash[untyped, untyped])


67
68
69
70
71
72
73
74
75
76
77
# File 'lib/deimos/active_record_consume/batch_consumption.rb', line 67

def record_key(key)
  if key.nil?
    {}
  elsif key.is_a?(Hash)
    @key_converter.convert(key)
  elsif self.class.config[:key_field].nil?
    { @klass.primary_key => key }
  else
      { self.class.config[:key_field] => key }
  end
end

#remove_records(messages) ⇒ void

This method returns an undefined value.

Delete any records with a tombstone. deleted records.

@param messages — List of messages for a group of

Parameters:



222
223
224
225
226
227
228
# File 'lib/deimos/active_record_consume/batch_consumption.rb', line 222

def remove_records(messages)
  Deimos::Utils::DeadlockRetry.wrap(Deimos.config.tracer.active_span.get_tag('topic')) do
    clause = deleted_query(messages)

    clause.delete_all
  end
end

#uncompacted_update(messages) ⇒ void

This method returns an undefined value.

Perform database operations for a batch of messages without compaction. All messages are split into slices containing only unique keys, and each slice is handles as its own batch.

@param messages — List of messages.

Parameters:



115
116
117
118
119
# File 'lib/deimos/active_record_consume/batch_consumption.rb', line 115

def uncompacted_update(messages)
  BatchSlicer.
    slice(messages).
    each(&method(:update_database))
end

#update_database(messages) ⇒ void

This method returns an undefined value.

Perform database operations for a group of messages. All messages with payloads are passed to upsert_records. All tombstones messages are passed to remove_records.

@param messages — List of messages.

Parameters:



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/deimos/active_record_consume/batch_consumption.rb', line 126

def update_database(messages)
  # Find all upserted records (i.e. that have a payload) and all
  # deleted record (no payload)
  removed, upserted = messages.partition(&:tombstone?)

  max_db_batch_size = self.class.config[:max_db_batch_size]
  if upserted.any?
    if max_db_batch_size
      upserted.each_slice(max_db_batch_size) { |group| upsert_records(group) }
    else
      upsert_records(upserted)
    end
  end

  return if removed.empty?

  if max_db_batch_size
    removed.each_slice(max_db_batch_size) { |group| remove_records(group) }
  else
    remove_records(removed)
  end
end

#upsert_records(messages) ⇒ void

This method returns an undefined value.

Upsert any non-deleted records records to either be updated or inserted.

@param messages — List of messages for a group of

Parameters:



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/deimos/active_record_consume/batch_consumption.rb', line 153

def upsert_records(messages)
  record_list = build_records(messages)
  invalid = filter_records(record_list)
  if invalid.any?
    ActiveSupport::Notifications.instrument('batch_consumption.invalid_records', {
                                              records: invalid,
                                              consumer: self.class
                                            })
  end
  return if record_list.empty?

  key_col_proc = self.method(:key_columns).to_proc
  col_proc = self.method(:columns).to_proc

  updater = MassUpdater.new(@klass,
                            key_col_proc: key_col_proc,
                            col_proc: col_proc,
                            replace_associations: self.class.replace_associations,
                            bulk_import_id_generator: self.class.bulk_import_id_generator)
  ActiveSupport::Notifications.instrument('batch_consumption.valid_records', {
                                            records: updater.mass_update(record_list),
                                            consumer: self.class
                                          })
end