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
-
#compact_messages(batch) ⇒ ::Array[Message]
Compact a batch of messages, taking only the last message for each unique key.
-
#consume_batch(payloads, metadata) ⇒ void
Handle a batch of Kafka messages.
-
#deleted_query(records) ⇒ ActiveRecord::Relation
Create an ActiveRecord relation that matches all of the passed records.
-
#key_columns(_klass) ⇒ ::Array[String]
Get the set of attribute names that uniquely identify messages in the batch.
-
#record_key(key) ⇒ ::Hash[untyped, untyped]
Get unique key for the ActiveRecord instance from the incoming key.
-
#remove_records(messages) ⇒ void
Delete any records with a tombstone.
-
#uncompacted_update(messages) ⇒ void
Perform database operations for a batch of messages without compaction.
-
#update_database(messages) ⇒ void
Perform database operations for a group of messages.
-
#upsert_records(messages) ⇒ void
Upsert any non-deleted records records to either be updated or inserted.
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.
104 105 106 107 108 |
# File 'lib/deimos/active_record_consume/batch_consumption.rb', line 104 def (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.
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, ) = 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(()) else uncompacted_update() 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.
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.
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.
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
222 223 224 225 226 227 228 |
# File 'lib/deimos/active_record_consume/batch_consumption.rb', line 222 def remove_records() Deimos::Utils::DeadlockRetry.wrap(Deimos.config.tracer.active_span.get_tag('topic')) do clause = deleted_query() 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.
115 116 117 118 119 |
# File 'lib/deimos/active_record_consume/batch_consumption.rb', line 115 def uncompacted_update() BatchSlicer. slice(). 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.
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() # Find all upserted records (i.e. that have a payload) and all # deleted record (no payload) removed, upserted = .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
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() record_list = build_records() 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 |