deimos-ruby 2.1.8 → 2.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +31 -0
- data/CHANGELOG.md +8 -0
- data/README.md +6 -0
- data/deimos-ruby.gemspec +4 -4
- data/lib/deimos/active_record_consume/batch_consumption.rb +1 -1
- data/lib/deimos/active_record_consume/message_consumption.rb +1 -1
- data/lib/deimos/active_record_consumer.rb +8 -0
- data/lib/deimos/kafka_source.rb +17 -12
- data/lib/deimos/version.rb +1 -1
- data/spec/generators/active_record_generator_spec.rb +1 -1
- data/spec/kafka_source_spec.rb +44 -1
- data/spec/producer_spec.rb +4 -2
- metadata +12 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4298f86cb3eb5cb847e29361a4f6a33f5db7492832c730155956c1099f26006b
|
4
|
+
data.tar.gz: fe50a1133a94f9263758ba29198bbdccb83e35ccb45aee27107374f0141c26ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d80a71f983e026f703b616d7206320922a22c471fb16d9c364b3fb24a826156eee8ab022666138e4b287f7a153e2abf4079ff58b758e98805902fc3cf34d2a49
|
7
|
+
data.tar.gz: 51d3ba78308dfc330a01a0cc53c3105c70afe1c3f2ba812ec70902661be7bf5f381681135ff4909be9b3b153432553aa8a8bb8f11d605bd63308163a407f1e92
|
@@ -0,0 +1,31 @@
|
|
1
|
+
name: Release Gem
|
2
|
+
on:
|
3
|
+
push:
|
4
|
+
branches:
|
5
|
+
- main
|
6
|
+
tags:
|
7
|
+
- 'v*.*.*' # Matches semantic versioning tags like v1.0.0
|
8
|
+
workflow_dispatch: # Allows manual triggering of the workflow
|
9
|
+
|
10
|
+
jobs:
|
11
|
+
push:
|
12
|
+
name: Push gem to RubyGems.org
|
13
|
+
runs-on: ubuntu-latest
|
14
|
+
|
15
|
+
permissions:
|
16
|
+
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
17
|
+
contents: write # IMPORTANT: this permission is required for `rake release` to push the release tag
|
18
|
+
|
19
|
+
steps:
|
20
|
+
# Set up
|
21
|
+
- uses: actions/checkout@v4
|
22
|
+
with:
|
23
|
+
persist-credentials: false
|
24
|
+
- name: Set up Ruby
|
25
|
+
uses: ruby/setup-ruby@v1
|
26
|
+
with:
|
27
|
+
bundler-cache: true
|
28
|
+
ruby-version: ruby
|
29
|
+
|
30
|
+
# Release
|
31
|
+
- uses: rubygems/release-gem@v1
|
data/CHANGELOG.md
CHANGED
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
7
7
|
|
8
8
|
## UNRELEASED
|
9
9
|
|
10
|
+
# 2.1.10 - 2025-09-19
|
11
|
+
|
12
|
+
- Feature: Added `delete_record?` method to ActiveRecordConsumer to allow inspection of the payload to decide whether to delete, instead of hardcoding it to only delete on a null payload.
|
13
|
+
|
14
|
+
# 2.1.9 - 2025-08-13
|
15
|
+
|
16
|
+
- Fix: When a model uses multiple producers, `send_kafka_event_on_update` no longer aggregates `watched_attributes` across all producers and publishes to all of them if any field changed. It now evaluates each producer’s `watched_attributes` independently and publishes only to the producers whose fields changed (per‑producer isolation).
|
17
|
+
|
10
18
|
# 2.1.8 - 2025-08-11
|
11
19
|
|
12
20
|
- Feature: Producers can now customize the deletion payload, so different producers using the same model can send different delete messages.
|
data/README.md
CHANGED
@@ -498,6 +498,12 @@ class MyConsumer < Deimos::ActiveRecordConsumer
|
|
498
498
|
def destroy_record(record)
|
499
499
|
super
|
500
500
|
end
|
501
|
+
|
502
|
+
# Optional override of the logic determining whether to delete the record. Default is to
|
503
|
+
# delete it if the payload is nil.
|
504
|
+
def delete_record?(record)
|
505
|
+
super
|
506
|
+
end
|
501
507
|
|
502
508
|
# Optional override to change the attributes of the record before they
|
503
509
|
# are saved.
|
data/deimos-ruby.gemspec
CHANGED
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.add_development_dependency('activerecord-import')
|
27
27
|
spec.add_development_dependency('activerecord-trilogy-adapter')
|
28
28
|
spec.add_development_dependency('avro', '~> 1.9')
|
29
|
-
spec.add_development_dependency('database_cleaner', '~> 1
|
29
|
+
spec.add_development_dependency('database_cleaner', '~> 2.1')
|
30
30
|
spec.add_development_dependency('ddtrace', '>= 0.11')
|
31
31
|
spec.add_development_dependency('dogstatsd-ruby', '>= 4.2')
|
32
32
|
spec.add_development_dependency('guard', '~> 2')
|
@@ -35,15 +35,15 @@ Gem::Specification.new do |spec|
|
|
35
35
|
spec.add_development_dependency('karafka-testing', '~> 2.0')
|
36
36
|
spec.add_development_dependency('trilogy', '>= 0.1')
|
37
37
|
spec.add_development_dependency('pg', '~> 1.1')
|
38
|
-
spec.add_development_dependency('rails', '~>
|
38
|
+
spec.add_development_dependency('rails', '~> 8.0')
|
39
39
|
spec.add_development_dependency('rake', '~> 13')
|
40
40
|
spec.add_development_dependency('rspec', '~> 3')
|
41
41
|
spec.add_development_dependency('rspec_junit_formatter', '~>0.3')
|
42
|
-
spec.add_development_dependency('rspec-rails', '~>
|
42
|
+
spec.add_development_dependency('rspec-rails', '~> 8.0')
|
43
43
|
spec.add_development_dependency('rspec-snapshot', '~> 2.0')
|
44
44
|
spec.add_development_dependency('rubocop', '0.89.0')
|
45
45
|
spec.add_development_dependency('rubocop-rspec', '1.42.0')
|
46
46
|
spec.add_development_dependency('sord', '>= 5.0')
|
47
|
-
spec.add_development_dependency('sqlite3', '~>
|
47
|
+
spec.add_development_dependency('sqlite3', '~> 2.7')
|
48
48
|
spec.add_development_dependency('steep', '~> 1.0')
|
49
49
|
end
|
@@ -125,7 +125,7 @@ module Deimos
|
|
125
125
|
def update_database(messages)
|
126
126
|
# Find all upserted records (i.e. that have a payload) and all
|
127
127
|
# deleted record (no payload)
|
128
|
-
removed, upserted = messages.partition(
|
128
|
+
removed, upserted = messages.partition { |m| delete_record?(m) }
|
129
129
|
|
130
130
|
max_db_batch_size = self.class.config[:max_db_batch_size]
|
131
131
|
if upserted.any?
|
@@ -95,6 +95,14 @@ module Deimos
|
|
95
95
|
self.converter.convert(payload)
|
96
96
|
end
|
97
97
|
|
98
|
+
# Indicates whether to delete the given message. Defaults to checking if the message
|
99
|
+
# payload is nil.
|
100
|
+
# @param message [Deimos::Message]
|
101
|
+
# @return [Boolean]
|
102
|
+
def delete_record?(message)
|
103
|
+
message.payload.nil?
|
104
|
+
end
|
105
|
+
|
98
106
|
# Override this message to conditionally save records
|
99
107
|
# @param _payload [Hash,Deimos::SchemaClass::Record] The kafka message
|
100
108
|
# @return [Boolean] if true, record is created/update.
|
data/lib/deimos/kafka_source.rb
CHANGED
@@ -27,18 +27,21 @@ module Deimos
|
|
27
27
|
def send_kafka_event_on_update
|
28
28
|
return unless self.class.kafka_config[:update]
|
29
29
|
|
30
|
-
producers = self.class.kafka_producers
|
31
|
-
fields = producers.flat_map { |p| p.watched_attributes(self) }.uniq
|
32
|
-
fields -= ['updated_at']
|
33
|
-
# Only send an event if a field we care about was changed.
|
34
|
-
any_changes = fields.any? do |field|
|
35
|
-
field_change = self.previous_changes[field]
|
36
|
-
field_change.present? && field_change[0] != field_change[1]
|
37
|
-
end
|
38
|
-
return unless any_changes
|
39
30
|
self.truncate_columns if Deimos.config.producers.truncate_columns
|
40
31
|
|
41
|
-
producers
|
32
|
+
producers = self.class.kafka_producers
|
33
|
+
producers.each do |producer|
|
34
|
+
fields = producer.watched_attributes(self)
|
35
|
+
fields -= ['updated_at']
|
36
|
+
# Only send an event if a field we care about was changed.
|
37
|
+
any_changes = fields.any? do |field|
|
38
|
+
field_change = self.previous_changes[field]
|
39
|
+
field_change.present? && field_change[0] != field_change[1]
|
40
|
+
end
|
41
|
+
next unless any_changes
|
42
|
+
|
43
|
+
producer.send_event(self)
|
44
|
+
end
|
42
45
|
end
|
43
46
|
|
44
47
|
# Send a deletion (null payload) event to Kafka.
|
@@ -82,7 +85,7 @@ module Deimos
|
|
82
85
|
# @!visibility private
|
83
86
|
def import_without_validations_or_callbacks(column_names,
|
84
87
|
array_of_attributes,
|
85
|
-
options={})
|
88
|
+
options = {})
|
86
89
|
results = super
|
87
90
|
if !self.kafka_config[:import] || array_of_attributes.empty?
|
88
91
|
return results
|
@@ -112,7 +115,8 @@ module Deimos
|
|
112
115
|
last_id = if self.connection.adapter_name.downcase =~ /sqlite/
|
113
116
|
self.connection.select_value('select last_insert_rowid()') -
|
114
117
|
hashes_without_id.size + 1
|
115
|
-
else
|
118
|
+
else
|
119
|
+
# mysql
|
116
120
|
self.connection.select_value('select LAST_INSERT_ID()')
|
117
121
|
end
|
118
122
|
hashes_without_id.each_with_index do |attrs, i|
|
@@ -130,6 +134,7 @@ module Deimos
|
|
130
134
|
self.class.columns.each do |col|
|
131
135
|
next unless col.type == :string
|
132
136
|
next if self[col.name].blank?
|
137
|
+
|
133
138
|
if self[col.name].to_s.length > col.limit
|
134
139
|
self[col.name] = self[col.name][0..col.limit - 1]
|
135
140
|
end
|
data/lib/deimos/version.rb
CHANGED
@@ -16,7 +16,7 @@ RSpec.describe Deimos::Generators::ActiveRecordGenerator do
|
|
16
16
|
files = Dir['db/migrate/*.rb']
|
17
17
|
expect(files.length).to eq(1)
|
18
18
|
results = <<~MIGRATION
|
19
|
-
class CreateGeneratedTable < ActiveRecord::Migration[
|
19
|
+
class CreateGeneratedTable < ActiveRecord::Migration[8.0]
|
20
20
|
def up
|
21
21
|
if table_exists?(:generated_table)
|
22
22
|
warn "generated_table already exists, exiting"
|
data/spec/kafka_source_spec.rb
CHANGED
@@ -17,10 +17,16 @@ module KafkaSourceSpec
|
|
17
17
|
|
18
18
|
# Dummy producer which mimicks the behavior of a real producer
|
19
19
|
class WidgetProducer < Deimos::ActiveRecordProducer
|
20
|
+
def self.watched_attributes(_record)
|
21
|
+
%w(name widget_id)
|
22
|
+
end
|
20
23
|
end
|
21
24
|
|
22
25
|
# Dummy producer which mimicks the behavior of a real producer
|
23
26
|
class WidgetProducerTheSecond < Deimos::ActiveRecordProducer
|
27
|
+
def self.watched_attributes(_record)
|
28
|
+
%w(description widget_id)
|
29
|
+
end
|
24
30
|
end
|
25
31
|
|
26
32
|
# Dummy class we can include the mixin in. Has a backing table created
|
@@ -122,7 +128,6 @@ module KafkaSourceSpec
|
|
122
128
|
def generate_payload(attributes, record)
|
123
129
|
payload = super(attributes, record)
|
124
130
|
payload.merge('id' => record.model_id)
|
125
|
-
|
126
131
|
end
|
127
132
|
|
128
133
|
def generate_deletion_payload(record)
|
@@ -182,6 +187,7 @@ module KafkaSourceSpec
|
|
182
187
|
before(:each) do
|
183
188
|
Deimos.config.producers.truncate_columns = false
|
184
189
|
end
|
190
|
+
|
185
191
|
it 'should not truncate values' do
|
186
192
|
widget = Widget.create!(widget_id: 1, name: 'a' * 500)
|
187
193
|
expect('my-topic').to have_sent({
|
@@ -206,6 +212,7 @@ module KafkaSourceSpec
|
|
206
212
|
before(:each) do
|
207
213
|
Deimos.config.producers.truncate_columns = true
|
208
214
|
end
|
215
|
+
|
209
216
|
it 'should truncate values' do
|
210
217
|
widget = Widget.create!(widget_id: 1, name: 'a' * 500)
|
211
218
|
expect('my-topic').to have_sent({
|
@@ -466,5 +473,41 @@ module KafkaSourceSpec
|
|
466
473
|
}.to raise_error(Deimos::MissingImplementationError)
|
467
474
|
end
|
468
475
|
end
|
476
|
+
|
477
|
+
describe 'Isolated watched‑attribute per producer when send_kafka_event_on_update' do
|
478
|
+
it 'should only send events to producers that watch the name field' do
|
479
|
+
widget = Widget.create!(widget_id: 1, name: 'initial', description: 'initial desc')
|
480
|
+
clear_kafka_messages!
|
481
|
+
|
482
|
+
widget.update_attribute(:name, 'updated name')
|
483
|
+
|
484
|
+
expect('my-topic').to have_sent(hash_including(name: 'updated name'))
|
485
|
+
expect('my-topic-the-second').not_to have_sent(anything)
|
486
|
+
end
|
487
|
+
|
488
|
+
it 'should only send events to producers that watch the description field' do
|
489
|
+
widget = Widget.create!(widget_id: 1, name: 'test', description: 'initial desc')
|
490
|
+
clear_kafka_messages!
|
491
|
+
|
492
|
+
widget.update_attribute(:description, 'updated description')
|
493
|
+
|
494
|
+
expect('my-topic-the-second').to have_sent(anything)
|
495
|
+
expect('my-topic').not_to have_sent(anything)
|
496
|
+
end
|
497
|
+
|
498
|
+
it 'should send events to all producers when a commonly watched field changes' do
|
499
|
+
allow(WidgetProducer).to receive(:watched_attributes).and_return(%w(name widget_id))
|
500
|
+
allow(WidgetProducerTheSecond).to receive(:watched_attributes).and_return(%w(description widget_id))
|
501
|
+
|
502
|
+
widget = Widget.create!(widget_id: 1, name: 'test', description: 'test desc')
|
503
|
+
clear_kafka_messages!
|
504
|
+
|
505
|
+
widget.update_attribute(:widget_id, 999)
|
506
|
+
|
507
|
+
expect('my-topic').to have_sent(hash_including(widget_id: 999))
|
508
|
+
expect('my-topic-the-second').to have_sent(hash_including(widget_id: 999))
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
469
512
|
end
|
470
513
|
end
|
data/spec/producer_spec.rb
CHANGED
@@ -273,9 +273,12 @@ module ProducerTest
|
|
273
273
|
end
|
274
274
|
|
275
275
|
describe 'payload logging' do
|
276
|
+
before(:each) do
|
277
|
+
allow(Karafka.logger).to receive(:info)
|
278
|
+
allow(Karafka.logger).to receive(:tagged).and_yield(Karafka.logger)
|
279
|
+
end
|
276
280
|
context 'with default / full' do
|
277
281
|
it 'should log full payload' do
|
278
|
-
allow(Karafka.logger).to receive(:info)
|
279
282
|
MyProducerWithID.publish_list(
|
280
283
|
[
|
281
284
|
{ 'test_id' => 'foo', 'some_int' => 123, :payload_key => 'key' },
|
@@ -301,7 +304,6 @@ module ProducerTest
|
|
301
304
|
context 'with count' do
|
302
305
|
it 'should log only count' do
|
303
306
|
Deimos.karafka_config_for(topic: 'my-topic-with-id').payload_log :count
|
304
|
-
allow(Karafka.logger).to receive(:info)
|
305
307
|
MyProducerWithID.publish_list(
|
306
308
|
[
|
307
309
|
{ 'test_id' => 'foo', 'some_int' => 123, :payload_key => 'key' },
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: deimos-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.1.
|
4
|
+
version: 2.1.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Orner
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: avro_turf
|
@@ -126,14 +125,14 @@ dependencies:
|
|
126
125
|
requirements:
|
127
126
|
- - "~>"
|
128
127
|
- !ruby/object:Gem::Version
|
129
|
-
version: '1
|
128
|
+
version: '2.1'
|
130
129
|
type: :development
|
131
130
|
prerelease: false
|
132
131
|
version_requirements: !ruby/object:Gem::Requirement
|
133
132
|
requirements:
|
134
133
|
- - "~>"
|
135
134
|
- !ruby/object:Gem::Version
|
136
|
-
version: '1
|
135
|
+
version: '2.1'
|
137
136
|
- !ruby/object:Gem::Dependency
|
138
137
|
name: ddtrace
|
139
138
|
requirement: !ruby/object:Gem::Requirement
|
@@ -252,14 +251,14 @@ dependencies:
|
|
252
251
|
requirements:
|
253
252
|
- - "~>"
|
254
253
|
- !ruby/object:Gem::Version
|
255
|
-
version: '
|
254
|
+
version: '8.0'
|
256
255
|
type: :development
|
257
256
|
prerelease: false
|
258
257
|
version_requirements: !ruby/object:Gem::Requirement
|
259
258
|
requirements:
|
260
259
|
- - "~>"
|
261
260
|
- !ruby/object:Gem::Version
|
262
|
-
version: '
|
261
|
+
version: '8.0'
|
263
262
|
- !ruby/object:Gem::Dependency
|
264
263
|
name: rake
|
265
264
|
requirement: !ruby/object:Gem::Requirement
|
@@ -308,14 +307,14 @@ dependencies:
|
|
308
307
|
requirements:
|
309
308
|
- - "~>"
|
310
309
|
- !ruby/object:Gem::Version
|
311
|
-
version: '
|
310
|
+
version: '8.0'
|
312
311
|
type: :development
|
313
312
|
prerelease: false
|
314
313
|
version_requirements: !ruby/object:Gem::Requirement
|
315
314
|
requirements:
|
316
315
|
- - "~>"
|
317
316
|
- !ruby/object:Gem::Version
|
318
|
-
version: '
|
317
|
+
version: '8.0'
|
319
318
|
- !ruby/object:Gem::Dependency
|
320
319
|
name: rspec-snapshot
|
321
320
|
requirement: !ruby/object:Gem::Requirement
|
@@ -378,14 +377,14 @@ dependencies:
|
|
378
377
|
requirements:
|
379
378
|
- - "~>"
|
380
379
|
- !ruby/object:Gem::Version
|
381
|
-
version: '
|
380
|
+
version: '2.7'
|
382
381
|
type: :development
|
383
382
|
prerelease: false
|
384
383
|
version_requirements: !ruby/object:Gem::Requirement
|
385
384
|
requirements:
|
386
385
|
- - "~>"
|
387
386
|
- !ruby/object:Gem::Version
|
388
|
-
version: '
|
387
|
+
version: '2.7'
|
389
388
|
- !ruby/object:Gem::Dependency
|
390
389
|
name: steep
|
391
390
|
requirement: !ruby/object:Gem::Requirement
|
@@ -400,7 +399,6 @@ dependencies:
|
|
400
399
|
- - "~>"
|
401
400
|
- !ruby/object:Gem::Version
|
402
401
|
version: '1.0'
|
403
|
-
description:
|
404
402
|
email:
|
405
403
|
- daniel.orner@wishabi.com
|
406
404
|
executables:
|
@@ -410,6 +408,7 @@ extensions: []
|
|
410
408
|
extra_rdoc_files: []
|
411
409
|
files:
|
412
410
|
- ".github/workflows/ci.yml"
|
411
|
+
- ".github/workflows/release.yml"
|
413
412
|
- ".gitignore"
|
414
413
|
- ".gitmodules"
|
415
414
|
- ".rspec"
|
@@ -637,7 +636,6 @@ homepage: ''
|
|
637
636
|
licenses:
|
638
637
|
- Apache-2.0
|
639
638
|
metadata: {}
|
640
|
-
post_install_message:
|
641
639
|
rdoc_options: []
|
642
640
|
require_paths:
|
643
641
|
- lib
|
@@ -652,8 +650,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
652
650
|
- !ruby/object:Gem::Version
|
653
651
|
version: '0'
|
654
652
|
requirements: []
|
655
|
-
rubygems_version: 3.
|
656
|
-
signing_key:
|
653
|
+
rubygems_version: 3.6.9
|
657
654
|
specification_version: 4
|
658
655
|
summary: Kafka libraries for Ruby.
|
659
656
|
test_files:
|