deimos-ruby 1.24.2 → 1.24.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2be475993ca3cec87d78682f7724b973f5aadb885612f98d5978169da2932bf
4
- data.tar.gz: 5ab0f052676184f800965bf68aebecac80583d1da2f38561a305e5f5b57a68c9
3
+ metadata.gz: de8ca3bf7233f799c3bf9169fa647dabebd8e08cd2b05ec00511c132a5f3920d
4
+ data.tar.gz: 03177d9ac87184fe3ce6ef916bce0ad39e3b81a6576f60ba71c33b298f82dfd8
5
5
  SHA512:
6
- metadata.gz: 918a363d9994537a221ccc6d00131c75a97f36c8cf89bdda47e0a6e6770b36fd1c3b07e5177d0ef284494020d90dd9081bc9d40ec41f4dc8bbe45e9c4165ea1f
7
- data.tar.gz: 60d316ef05ce427234e366e32f44addd7188adb45345b025bfb9fd21f339d0c579f317f1e55db20185b9f5f825af3226c27cb53d5e9336ec805200d2a9ce5bd9
6
+ metadata.gz: 40ecdddd9c0b8f5f9675f8058f4a8c8adf1eb69a293e04870a46b9d8ae1972cdc70088611c898f0ab91e072f6627c4760bca5ca5399b698316edda8f3212f5aa
7
+ data.tar.gz: 3f2fa463d5c777ce7e4dfc4cf610a7dd4ed3f6dba5b6dd459f1ca8fad037f0baa47278436c65c91dc90679f88e9d42a792b84ccbdc2cde4b58d1239d03b2fd4a
data/CHANGELOG.md CHANGED
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## UNRELEASED
9
9
 
10
+ # 1.24.3 - 2024-05-13
11
+
12
+ - Feature: Enable `producers.persistent_connections` phobos setting
13
+ - Feature: Add consumer configuration, `save_associations_first` to save associated records of primary class prior to upserting primary records. Foreign key of associated records are assigned to the record class prior to saving the record class
14
+
10
15
  # 1.24.2 - 2024-05-01
11
16
  - Fix: Deprecation notice with Rails 7.
12
17
 
@@ -102,6 +102,7 @@ heartbeat_interval|10|Interval between heartbeats; must be less than the session
102
102
  backoff|`(1000..60_000)`|Range representing the minimum and maximum number of milliseconds to back off after a consumer error.
103
103
  replace_associations|nil| Whether to delete existing associations for records during bulk consumption for this consumer. If no value is specified the provided/default value from the `consumers` configuration will be used.
104
104
  bulk_import_id_generator|nil| Block to determine the `bulk_import_id` generated during bulk consumption. If no block is specified the provided/default block from the `consumers` configuration will be used.
105
+ save_associations_first|false|Whether to save associated records of primary class prior to upserting primary records. Foreign key of associated records are assigned to the record class prior to saving the record class
105
106
 
106
107
  ## Defining Database Pollers
107
108
 
@@ -83,7 +83,7 @@ module Deimos
83
83
  def deleted_query(records)
84
84
  keys = records.
85
85
  map { |m| record_key(m.key)[@klass.primary_key] }.
86
- reject(&:nil?)
86
+ compact
87
87
 
88
88
  @klass.unscoped.where(@klass.primary_key => keys)
89
89
  end
@@ -168,7 +168,9 @@ module Deimos
168
168
  key_col_proc: key_col_proc,
169
169
  col_proc: col_proc,
170
170
  replace_associations: self.class.replace_associations,
171
- bulk_import_id_generator: self.class.bulk_import_id_generator)
171
+ bulk_import_id_generator: self.class.bulk_import_id_generator,
172
+ save_associations_first: self.class.save_associations_first,
173
+ bulk_import_id_column: self.class.bulk_import_id_column)
172
174
  ActiveSupport::Notifications.instrument('batch_consumption.valid_records', {
173
175
  records: updater.mass_update(record_list),
174
176
  consumer: self.class
@@ -20,10 +20,13 @@ module Deimos
20
20
  # @param col_proc [Proc<Class < ActiveRecord::Base>]
21
21
  # @param replace_associations [Boolean]
22
22
  def initialize(klass, key_col_proc: nil, col_proc: nil,
23
- replace_associations: true, bulk_import_id_generator: nil)
23
+ replace_associations: true, bulk_import_id_generator: nil, save_associations_first: false,
24
+ bulk_import_id_column: nil)
24
25
  @klass = klass
25
26
  @replace_associations = replace_associations
26
27
  @bulk_import_id_generator = bulk_import_id_generator
28
+ @save_associations_first = save_associations_first
29
+ @bulk_import_id_column = bulk_import_id_column&.to_s
27
30
 
28
31
  @key_cols = {}
29
32
  @key_col_proc = key_col_proc
@@ -84,15 +87,67 @@ module Deimos
84
87
  end
85
88
  end
86
89
 
90
+ # Assign associated records to corresponding primary records
91
+ # @param record_list [BatchRecordList] RecordList of primary records for this consumer
92
+ # @return [Hash]
93
+ def assign_associations(record_list)
94
+ associations_info = {}
95
+ record_list.associations.each do |assoc|
96
+ col = @bulk_import_id_column if assoc.klass.column_names.include?(@bulk_import_id_column)
97
+ associations_info[[assoc, col]] = []
98
+ end
99
+ record_list.batch_records.each do |primary_batch_record|
100
+ associations_info.each_key do |assoc, col|
101
+ batch_record = BatchRecord.new(klass: assoc.klass,
102
+ attributes: primary_batch_record.associations[assoc.name],
103
+ bulk_import_column: col,
104
+ bulk_import_id_generator: @bulk_import_id_generator)
105
+ # Associate this associated batch record's record with the primary record to
106
+ # retrieve foreign_key after associated records have been saved and primary
107
+ # keys have been filled
108
+ primary_batch_record.record.assign_attributes({ assoc.name => batch_record.record })
109
+ associations_info[[assoc, col]] << batch_record
110
+ end
111
+ end
112
+ associations_info
113
+ end
114
+
115
+ # Save associated records and fill foreign keys on RecordList records
116
+ # @param record_list [BatchRecordList] RecordList of primary records for this consumer
117
+ # @param associations_info [Hash] Contains association info
118
+ def save_associations_first(record_list, associations_info)
119
+ associations_info.each_value do |records|
120
+ assoc_record_list = BatchRecordList.new(records)
121
+ Deimos::Utils::DeadlockRetry.wrap(Deimos.config.tracer.active_span.get_tag('topic')) do
122
+ save_records_to_database(assoc_record_list)
123
+ end
124
+ import_associations(assoc_record_list)
125
+ end
126
+ record_list.records.each do |record|
127
+ associations_info.each_key do |assoc, _|
128
+ record.assign_attributes({ assoc.foreign_key => record.send(assoc.name).id })
129
+ end
130
+ end
131
+ end
132
+
87
133
  # @param record_list [BatchRecordList]
88
134
  # @return [Array<ActiveRecord::Base>]
89
135
  def mass_update(record_list)
90
136
  # The entire batch should be treated as one transaction so that if
91
137
  # any message fails, the whole thing is rolled back or retried
92
138
  # if there is deadlock
93
- Deimos::Utils::DeadlockRetry.wrap(Deimos.config.tracer.active_span.get_tag('topic')) do
94
- save_records_to_database(record_list)
95
- import_associations(record_list) if record_list.associations.any?
139
+
140
+ if @save_associations_first
141
+ associations_info = assign_associations(record_list)
142
+ save_associations_first(record_list, associations_info)
143
+ Deimos::Utils::DeadlockRetry.wrap(Deimos.config.tracer.active_span.get_tag('topic')) do
144
+ save_records_to_database(record_list)
145
+ end
146
+ else
147
+ Deimos::Utils::DeadlockRetry.wrap(Deimos.config.tracer.active_span.get_tag('topic')) do
148
+ save_records_to_database(record_list)
149
+ import_associations(record_list) if record_list.associations.any?
150
+ end
96
151
  end
97
152
  record_list.records
98
153
  end
@@ -45,6 +45,11 @@ module Deimos
45
45
  config[:replace_associations]
46
46
  end
47
47
 
48
+ # @return [Boolean]
49
+ def save_associations_first
50
+ config[:save_associations_first]
51
+ end
52
+
48
53
  # @param val [Boolean] Turn pre-compaction of the batch on or off. If true,
49
54
  # only the last message for each unique key in a batch is processed.
50
55
  # @return [void]
@@ -97,7 +97,8 @@ module Deimos # rubocop:disable Metrics/ModuleLength
97
97
  kafka_config.replace_associations
98
98
  end,
99
99
  bulk_import_id_generator: kafka_config.bulk_import_id_generator ||
100
- Deimos.config.consumers.bulk_import_id_generator
100
+ Deimos.config.consumers.bulk_import_id_generator,
101
+ save_associations_first: kafka_config.save_associations_first
101
102
  )
102
103
  end
103
104
  end
@@ -476,6 +477,11 @@ module Deimos # rubocop:disable Metrics/ModuleLength
476
477
  # @return [Block]
477
478
  setting :bulk_import_id_generator, nil
478
479
 
480
+ # If enabled save associated records prior to saving the main record class
481
+ # This will also set foreign keys for associated records
482
+ # @return [Boolean]
483
+ setting :save_associations_first, false
484
+
479
485
  # These are the phobos "listener" configs. See CONFIGURATION.md for more
480
486
  # info.
481
487
  setting :group_id
@@ -52,7 +52,8 @@ module Deimos
52
52
  compression_threshold: self.producers.compression_threshold,
53
53
  max_queue_size: self.producers.max_queue_size,
54
54
  delivery_threshold: self.producers.delivery_threshold,
55
- delivery_interval: self.producers.delivery_interval
55
+ delivery_interval: self.producers.delivery_interval,
56
+ persistent_connections: self.producers.persistent_connections
56
57
  },
57
58
  consumer: {
58
59
  session_timeout: self.consumers.session_timeout,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Deimos
4
- VERSION = '1.24.2'
4
+ VERSION = '1.24.3'
5
5
  end
@@ -114,5 +114,142 @@ RSpec.describe Deimos::ActiveRecordConsume::MassUpdater do
114
114
 
115
115
  end
116
116
 
117
+ context 'with save_associations_first' do
118
+ before(:all) do
119
+ ActiveRecord::Base.connection.create_table(:fidgets, force: true) do |t|
120
+ t.string(:test_id)
121
+ t.integer(:some_int)
122
+ t.string(:bulk_import_id)
123
+ t.timestamps
124
+ end
125
+
126
+ ActiveRecord::Base.connection.create_table(:fidget_details, force: true) do |t|
127
+ t.string(:title)
128
+ t.string(:bulk_import_id)
129
+ t.belongs_to(:fidget)
130
+
131
+ t.index(%i(title), unique: true)
132
+ end
133
+
134
+ ActiveRecord::Base.connection.create_table(:widget_fidgets, force: true, id: false) do |t|
135
+ t.belongs_to(:fidget)
136
+ t.belongs_to(:widget)
137
+ t.string(:bulk_import_id)
138
+ t.string(:note)
139
+ t.index(%i(widget_id fidget_id), unique: true)
140
+ end
141
+ end
142
+
143
+ after(:all) do
144
+ ActiveRecord::Base.connection.drop_table(:fidgets)
145
+ ActiveRecord::Base.connection.drop_table(:fidget_details)
146
+ ActiveRecord::Base.connection.drop_table(:widget_fidgets)
147
+ end
148
+
149
+ let(:fidget_detail_class) do
150
+ Class.new(ActiveRecord::Base) do
151
+ self.table_name = 'fidget_details'
152
+ belongs_to :fidget
153
+ end
154
+ end
155
+
156
+ let(:fidget_class) do
157
+ Class.new(ActiveRecord::Base) do
158
+ self.table_name = 'fidgets'
159
+ has_one :fidget_detail
160
+ end
161
+ end
162
+
163
+ let(:widget_fidget_class) do
164
+ Class.new(ActiveRecord::Base) do
165
+ self.table_name = 'widget_fidgets'
166
+ belongs_to :fidget
167
+ belongs_to :widget
168
+ end
169
+ end
170
+
171
+ let(:bulk_id_generator) { proc { SecureRandom.uuid } }
172
+
173
+ let(:key_proc) do
174
+ lambda do |klass|
175
+ case klass.to_s
176
+ when 'Widget', 'Fidget'
177
+ %w(id)
178
+ when 'WidgetFidget'
179
+ %w(widget_id fidget_id)
180
+ when 'FidgetDetail', 'Detail'
181
+ %w(title)
182
+ else
183
+ raise "Key Columns for #{klass} not defined"
184
+ end
185
+
186
+ end
187
+ end
188
+
189
+ before(:each) do
190
+ stub_const('Fidget', fidget_class)
191
+ stub_const('FidgetDetail', fidget_detail_class)
192
+ stub_const('WidgetFidget', widget_fidget_class)
193
+ Widget.reset_column_information
194
+ Fidget.reset_column_information
195
+ WidgetFidget.reset_column_information
196
+ end
197
+
198
+ # rubocop:disable RSpec/MultipleExpectations, RSpec/ExampleLength
199
+ it 'should backfill the associations when upserting primary records' do
200
+ batch = Deimos::ActiveRecordConsume::BatchRecordList.new(
201
+ [
202
+ Deimos::ActiveRecordConsume::BatchRecord.new(
203
+ klass: WidgetFidget,
204
+ attributes: {
205
+ widget: { test_id: 'id1', some_int: 10, detail: { title: 'Widget Title 1' } },
206
+ fidget: { test_id: 'id1', some_int: 10, fidget_detail: { title: 'Fidget Title 1' } },
207
+ note: 'Stuff 1'
208
+ },
209
+ bulk_import_column: 'bulk_import_id',
210
+ bulk_import_id_generator: bulk_id_generator
211
+ ),
212
+ Deimos::ActiveRecordConsume::BatchRecord.new(
213
+ klass: WidgetFidget,
214
+ attributes: {
215
+ widget: { test_id: 'id2', some_int: 20, detail: { title: 'Widget Title 2' } },
216
+ fidget: { test_id: 'id2', some_int: 20, fidget_detail: { title: 'Fidget Title 2' } },
217
+ note: 'Stuff 2'
218
+ },
219
+ bulk_import_column: 'bulk_import_id',
220
+ bulk_import_id_generator: bulk_id_generator
221
+ )
222
+ ]
223
+ )
224
+
225
+ results = described_class.new(WidgetFidget,
226
+ bulk_import_id_generator: bulk_id_generator,
227
+ bulk_import_id_column: 'bulk_import_id',
228
+ key_col_proc: key_proc,
229
+ save_associations_first: true).mass_update(batch)
230
+ expect(results.count).to eq(2)
231
+ expect(Widget.count).to eq(2)
232
+ expect(Detail.count).to eq(2)
233
+ expect(Fidget.count).to eq(2)
234
+ expect(FidgetDetail.count).to eq(2)
235
+
236
+ WidgetFidget.all.each_with_index do |widget_fidget, ind|
237
+ widget = Widget.find_by(id: widget_fidget.widget_id)
238
+ expect(widget.test_id).to eq("id#{ind + 1}")
239
+ expect(widget.some_int).to eq((ind + 1) * 10)
240
+ detail = Detail.find_by(widget_id: widget_fidget.widget_id)
241
+ expect(detail.title).to eq("Widget Title #{ind + 1}")
242
+ fidget = Fidget.find_by(id: widget_fidget.fidget_id)
243
+ expect(fidget.test_id).to eq("id#{ind + 1}")
244
+ expect(fidget.some_int).to eq((ind + 1) * 10)
245
+ fidget_detail = FidgetDetail.find_by(fidget_id: widget_fidget.fidget_id)
246
+ expect(fidget_detail.title).to eq("Fidget Title #{ind + 1}")
247
+ expect(widget_fidget.note).to eq("Stuff #{ind + 1}")
248
+ end
249
+ end
250
+ # rubocop:enable RSpec/MultipleExpectations, RSpec/ExampleLength
251
+
252
+ end
253
+
117
254
  end
118
255
  end
@@ -92,7 +92,8 @@ describe Deimos, 'configuration' do
92
92
  handler: 'ConsumerTest::MyConsumer',
93
93
  use_schema_classes: nil,
94
94
  max_db_batch_size: nil,
95
- bulk_import_id_generator: nil
95
+ bulk_import_id_generator: nil,
96
+ save_associations_first: false
96
97
  }, {
97
98
  topic: 'my_batch_consume_topic',
98
99
  group_id: 'my_batch_group_id',
@@ -111,7 +112,8 @@ describe Deimos, 'configuration' do
111
112
  handler: 'ConsumerTest::MyBatchConsumer',
112
113
  use_schema_classes: nil,
113
114
  max_db_batch_size: nil,
114
- bulk_import_id_generator: nil
115
+ bulk_import_id_generator: nil,
116
+ save_associations_first: false
115
117
  }
116
118
  ],
117
119
  producer: {
@@ -125,7 +127,8 @@ describe Deimos, 'configuration' do
125
127
  compression_threshold: 1,
126
128
  max_queue_size: 10_000,
127
129
  delivery_threshold: 0,
128
- delivery_interval: 0
130
+ delivery_interval: 0,
131
+ persistent_connections: false
129
132
  }
130
133
  )
131
134
  end
@@ -264,7 +267,8 @@ describe Deimos, 'configuration' do
264
267
  handler: 'MyConfigConsumer',
265
268
  use_schema_classes: false,
266
269
  max_db_batch_size: nil,
267
- bulk_import_id_generator: nil
270
+ bulk_import_id_generator: nil,
271
+ save_associations_first: false
268
272
  }
269
273
  ],
270
274
  producer: {
@@ -278,7 +282,8 @@ describe Deimos, 'configuration' do
278
282
  compression_threshold: 2,
279
283
  max_queue_size: 10,
280
284
  delivery_threshold: 1,
281
- delivery_interval: 1
285
+ delivery_interval: 1,
286
+ persistent_connections: true
282
287
  }
283
288
  )
284
289
  end
@@ -295,6 +300,7 @@ describe Deimos, 'configuration' do
295
300
  group_id 'myconsumerid'
296
301
  bulk_import_id_generator(-> { 'consumer' })
297
302
  replace_associations false
303
+ save_associations_first true
298
304
  end
299
305
 
300
306
  consumer do
@@ -312,10 +318,12 @@ describe Deimos, 'configuration' do
312
318
  custom = MyConfigConsumer.config
313
319
  expect(custom[:replace_associations]).to eq(false)
314
320
  expect(custom[:bulk_import_id_generator].call).to eq('consumer')
321
+ expect(custom[:save_associations_first]).to eq(true)
315
322
 
316
323
  default = MyConfigConsumer2.config
317
324
  expect(default[:replace_associations]).to eq(true)
318
325
  expect(default[:bulk_import_id_generator].call).to eq('global')
326
+ expect(default[:save_associations_first]).to eq(false)
319
327
 
320
328
  end
321
329
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deimos-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.24.2
4
+ version: 1.24.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Orner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-05-01 00:00:00.000000000 Z
11
+ date: 2024-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: avro_turf