deimos-ruby 2.4.0.pre.beta18 → 2.4.0.pre.beta19
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/CHANGELOG.md +13 -0
- data/README.md +4 -0
- data/deimos-ruby.gemspec +1 -0
- data/lib/deimos/active_record_consume/batch_consumption.rb +19 -1
- data/lib/deimos/active_record_consumer.rb +1 -0
- data/lib/deimos/active_record_producer.rb +11 -5
- data/lib/deimos/metrics/minimal_datadog.rb +10 -3
- data/lib/deimos/schema_backends/avro_schema_registry.rb +0 -1
- data/lib/deimos/version.rb +1 -1
- data/spec/active_record_batch_consumer_association_spec.rb +1 -1
- data/spec/active_record_batch_consumer_spec.rb +85 -0
- data/spec/deimos_spec.rb +28 -0
- data/spec/schema_backends/base_spec.rb +12 -0
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2cebceac8c154d892ac2b52333d39bedd44fab2031bc072599c3f87c76d4a720
|
|
4
|
+
data.tar.gz: e615c5604fdbece27c19b53026035d64e70d37ac0f6d8e83681e6cf4b223a9a6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b9950799d72162f17afa70bbff778cb1a96edfe0c4279aae7381c1cde77dd64835c1dc45fd35bab356f0b219c94a9db93154e2edf639fda99f1f021856594196
|
|
7
|
+
data.tar.gz: d17195d05a9bd2fc91e6375efbc43e5c766b8b00060f7226b1d0d22253943ad08cc16210d40eb8452364249fb63791ed135b7dd08a7fe0c4141be2564a0411db
|
data/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
11
11
|
- Add `registry_url`, `registry_user` and `registry_password` per-topic configuration.
|
|
12
12
|
- Added `have_sent_including` RSpec matcher to allow for Protobuf messages that use default values to be checked.
|
|
13
13
|
|
|
14
|
+
# 2.3.3 - 2026-02-21
|
|
15
|
+
|
|
16
|
+
- Fix/feature: Send `consumer.lags` and `consumer.lags_delta` metrics via MinimalDatadogListener.
|
|
17
|
+
|
|
18
|
+
# 2.3.2 - 2026-02-20
|
|
19
|
+
|
|
20
|
+
- Feature: Add overridable process_message? for batch consumption.
|
|
21
|
+
- Feature: Add overridable post_process_batch for batch consumption.
|
|
22
|
+
|
|
23
|
+
# 2.3.1 - 2026-01-22
|
|
24
|
+
|
|
25
|
+
- Feature: Allow strings to be used in the `record_class` declaration.
|
|
26
|
+
|
|
14
27
|
# 2.3.0 - 2026-01-13
|
|
15
28
|
|
|
16
29
|
- Feature: Support broker setting per topic in producer configs.
|
data/README.md
CHANGED
|
@@ -403,6 +403,8 @@ class MyProducer < Deimos::ActiveRecordProducer
|
|
|
403
403
|
# using the default functionality and don't need to override it)
|
|
404
404
|
# by setting `refetch` to false. This will avoid extra database fetches.
|
|
405
405
|
record_class Widget, refetch: false
|
|
406
|
+
|
|
407
|
+
# You can use a string here instead to avoid eager loading: record_class 'Widget'
|
|
406
408
|
|
|
407
409
|
# Optionally override this if you want the message to be
|
|
408
410
|
# sent even if fields that aren't in the schema are changed.
|
|
@@ -490,6 +492,7 @@ A sample consumer would look as follows:
|
|
|
490
492
|
```ruby
|
|
491
493
|
class MyConsumer < Deimos::ActiveRecordConsumer
|
|
492
494
|
record_class Widget
|
|
495
|
+
# or use a string: record_class 'Widget'
|
|
493
496
|
|
|
494
497
|
# Optional override of the way to fetch records based on payload and
|
|
495
498
|
# key. Default is to use the key to search the primary key of the table.
|
|
@@ -567,6 +570,7 @@ A sample batch consumer would look as follows:
|
|
|
567
570
|
```ruby
|
|
568
571
|
class MyConsumer < Deimos::ActiveRecordConsumer
|
|
569
572
|
record_class Widget
|
|
573
|
+
# or use a string: record_class 'Widget'
|
|
570
574
|
|
|
571
575
|
# Controls whether the batch is compacted before consuming.
|
|
572
576
|
# If true, only the last message for each unique key in a batch will be
|
data/deimos-ruby.gemspec
CHANGED
|
@@ -33,6 +33,7 @@ Gem::Specification.new do |spec|
|
|
|
33
33
|
spec.add_development_dependency('guard-rubocop', '~> 1')
|
|
34
34
|
spec.add_development_dependency('karafka-testing', '~> 2.0')
|
|
35
35
|
spec.add_development_dependency('pg', '~> 1.1')
|
|
36
|
+
spec.add_development_dependency('pry', '~> 0.14.1')
|
|
36
37
|
spec.add_development_dependency('rails', '~> 8.0')
|
|
37
38
|
spec.add_development_dependency('rake', '~> 13')
|
|
38
39
|
spec.add_development_dependency('rspec', '~> 3')
|
|
@@ -25,7 +25,15 @@ module Deimos
|
|
|
25
25
|
# they are split
|
|
26
26
|
# @return [void]
|
|
27
27
|
def consume_batch
|
|
28
|
-
|
|
28
|
+
filtered = messages.select { |p| process_message?(p.payload) }
|
|
29
|
+
skipped_count = messages.size - filtered.size
|
|
30
|
+
if skipped_count.positive?
|
|
31
|
+
Deimos::Logging.log_debug(
|
|
32
|
+
message: 'Skipping processing of messages in batch',
|
|
33
|
+
skipped_count: skipped_count
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
deimos_messages = filtered.map { |p| Deimos::Message.new(p.payload, key: p.key) }
|
|
29
37
|
|
|
30
38
|
tag = topic.name
|
|
31
39
|
Deimos.config.tracer.active_span.set_tag('topic', tag)
|
|
@@ -37,6 +45,8 @@ module Deimos
|
|
|
37
45
|
uncompacted_update(deimos_messages)
|
|
38
46
|
end
|
|
39
47
|
end
|
|
48
|
+
|
|
49
|
+
post_process_batch(deimos_messages)
|
|
40
50
|
end
|
|
41
51
|
|
|
42
52
|
protected
|
|
@@ -94,6 +104,14 @@ module Deimos
|
|
|
94
104
|
true
|
|
95
105
|
end
|
|
96
106
|
|
|
107
|
+
# Perform any post-processing after a batch has been consumed.
|
|
108
|
+
# Called once per batch with the list of Deimos::Message that were processed (after filtering and compaction).
|
|
109
|
+
# @param messages [Array<Deimos::Message>] The batch of messages that were processed
|
|
110
|
+
# @return [void]
|
|
111
|
+
def post_process_batch(_messages)
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
97
115
|
private
|
|
98
116
|
|
|
99
117
|
# Compact a batch of messages, taking only the last message for each
|
|
@@ -13,18 +13,24 @@ module Deimos
|
|
|
13
13
|
class ActiveRecordProducer < Producer
|
|
14
14
|
class << self
|
|
15
15
|
# Indicate the class this producer is working on.
|
|
16
|
-
# @param klass [Class]
|
|
16
|
+
# @param klass [Class,String]
|
|
17
17
|
# @param refetch [Boolean] if true, and we are given a hash instead of
|
|
18
18
|
# a record object, refetch the record to pass into the `generate_payload`
|
|
19
19
|
# method.
|
|
20
20
|
# @return [void]
|
|
21
21
|
def record_class(klass=nil, refetch: true)
|
|
22
|
-
return
|
|
22
|
+
return record_klass if klass.nil?
|
|
23
23
|
|
|
24
24
|
@record_class = klass
|
|
25
25
|
@refetch_record = refetch
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# @return [Class,nil]
|
|
29
|
+
def record_klass
|
|
30
|
+
@record_class = @record_class.constantize if @record_class.is_a?(String)
|
|
31
|
+
@record_class
|
|
32
|
+
end
|
|
33
|
+
|
|
28
34
|
# @param record [ActiveRecord::Base]
|
|
29
35
|
# @param force_send [Boolean]
|
|
30
36
|
# @return [void]
|
|
@@ -38,14 +44,14 @@ module Deimos
|
|
|
38
44
|
def send_events(records, force_send: false)
|
|
39
45
|
return if Deimos.producers_disabled?(self)
|
|
40
46
|
|
|
41
|
-
primary_key =
|
|
47
|
+
primary_key = record_klass&.primary_key
|
|
42
48
|
messages = records.map do |record|
|
|
43
49
|
if record.respond_to?(:attributes)
|
|
44
50
|
attrs = record.attributes.with_indifferent_access
|
|
45
51
|
else
|
|
46
52
|
attrs = record.with_indifferent_access
|
|
47
53
|
if @refetch_record && attrs[primary_key]
|
|
48
|
-
record =
|
|
54
|
+
record = record_klass.find(attrs[primary_key])
|
|
49
55
|
end
|
|
50
56
|
end
|
|
51
57
|
generate_payload(attrs, record).with_indifferent_access
|
|
@@ -96,7 +102,7 @@ module Deimos
|
|
|
96
102
|
# than this value).
|
|
97
103
|
# @return [ActiveRecord::Relation]
|
|
98
104
|
def poll_query(time_from:, time_to:, min_id:, column_name: :updated_at)
|
|
99
|
-
klass =
|
|
105
|
+
klass = record_klass
|
|
100
106
|
table = ActiveRecord::Base.connection.quote_table_name(klass.table_name)
|
|
101
107
|
column = ActiveRecord::Base.connection.quote_column_name(column_name)
|
|
102
108
|
primary = ActiveRecord::Base.connection.quote_column_name(klass.primary_key)
|
|
@@ -6,8 +6,12 @@ require 'deimos/metrics/minimal_datadog_listener'
|
|
|
6
6
|
|
|
7
7
|
module Deimos
|
|
8
8
|
module Metrics
|
|
9
|
-
# A Metrics wrapper class for Datadog, with only minimal metrics being sent. This will
|
|
10
|
-
# send
|
|
9
|
+
# A Metrics wrapper class for Datadog, with only minimal metrics being sent. This will only
|
|
10
|
+
# send the following rdkafka metrics:
|
|
11
|
+
# * consumer.lags
|
|
12
|
+
# * consumer.lags_delta
|
|
13
|
+
#
|
|
14
|
+
# and only the following other metrics:
|
|
11
15
|
# * consumer_group
|
|
12
16
|
# * error_occurred
|
|
13
17
|
# * consumer.messages
|
|
@@ -28,7 +32,10 @@ module Deimos
|
|
|
28
32
|
if config[:karafka_distribution_mode]
|
|
29
33
|
karafka_config.distribution_mode = config[:karafka_distribution_mode]
|
|
30
34
|
end
|
|
31
|
-
karafka_config.rd_kafka_metrics = [
|
|
35
|
+
karafka_config.rd_kafka_metrics = [
|
|
36
|
+
RdKafkaMetric.new(:gauge, :topics, 'consumer.lags', 'consumer_lag_stored'),
|
|
37
|
+
RdKafkaMetric.new(:gauge, :topics, 'consumer.lags_delta', 'consumer_lag_stored_d')
|
|
38
|
+
]
|
|
32
39
|
end
|
|
33
40
|
Karafka.monitor.subscribe(karafka_listener)
|
|
34
41
|
end
|
data/lib/deimos/version.rb
CHANGED
|
@@ -70,7 +70,7 @@ module ActiveRecordBatchConsumerTest # rubocop:disable Metrics/ModuleLength
|
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
before(:each) do
|
|
73
|
-
ActiveRecord::Base.connection.truncate_tables(
|
|
73
|
+
ActiveRecord::Base.connection.truncate_tables(:widgets, :details, :locales)
|
|
74
74
|
Widget.create!(test_id: 'bad_id', some_int: 100) # should not show up
|
|
75
75
|
end
|
|
76
76
|
|
|
@@ -57,6 +57,12 @@ module ActiveRecordBatchConsumerTest
|
|
|
57
57
|
Class.new(described_class) do
|
|
58
58
|
record_class Widget
|
|
59
59
|
compacted false
|
|
60
|
+
|
|
61
|
+
def process_message?(payload)
|
|
62
|
+
return true if payload.nil?
|
|
63
|
+
|
|
64
|
+
payload['test_id'] != 'skipme'
|
|
65
|
+
end
|
|
60
66
|
end
|
|
61
67
|
end
|
|
62
68
|
|
|
@@ -103,6 +109,27 @@ module ActiveRecordBatchConsumerTest
|
|
|
103
109
|
)
|
|
104
110
|
end
|
|
105
111
|
|
|
112
|
+
it 'should not create records for messages that return false from process_message?' do
|
|
113
|
+
publish_batch(
|
|
114
|
+
[
|
|
115
|
+
{ key: 1,
|
|
116
|
+
payload: { test_id: 'abc', some_int: 3 } },
|
|
117
|
+
{ key: 2,
|
|
118
|
+
payload: { test_id: 'skipme', some_int: 4 } },
|
|
119
|
+
{ key: 3,
|
|
120
|
+
payload: { test_id: 'ghi', some_int: 5 } }
|
|
121
|
+
]
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
expect(all_widgets).
|
|
125
|
+
to contain_exactly(
|
|
126
|
+
have_attributes(id: 1, test_id: 'abc', some_int: 3, updated_at: start,
|
|
127
|
+
created_at: start),
|
|
128
|
+
have_attributes(id: 3, test_id: 'ghi', some_int: 5, updated_at: start,
|
|
129
|
+
created_at: start)
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
106
133
|
it 'should handle deleting a record that doesn\'t exist' do
|
|
107
134
|
publish_batch(
|
|
108
135
|
[
|
|
@@ -805,6 +832,64 @@ module ActiveRecordBatchConsumerTest
|
|
|
805
832
|
|
|
806
833
|
end
|
|
807
834
|
|
|
835
|
+
describe 'post_process_batch' do
|
|
836
|
+
before(:each) do
|
|
837
|
+
register_consumer(consumer_class,
|
|
838
|
+
'MySchema',
|
|
839
|
+
key_config: { plain: true })
|
|
840
|
+
MyBatchConsumer.last_post_process_batch = nil
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
let(:consumer_class) do
|
|
844
|
+
Class.new(described_class) do
|
|
845
|
+
class << self
|
|
846
|
+
attr_accessor :last_post_process_batch
|
|
847
|
+
end
|
|
848
|
+
|
|
849
|
+
record_class Widget
|
|
850
|
+
compacted false
|
|
851
|
+
|
|
852
|
+
def post_process_batch(messages)
|
|
853
|
+
self.class.last_post_process_batch = messages
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
def process_message?(payload)
|
|
857
|
+
payload['test_id'] != 'skipme'
|
|
858
|
+
end
|
|
859
|
+
end
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
it 'is called after the batch is processed with the processed messages' do
|
|
863
|
+
publish_batch(
|
|
864
|
+
[
|
|
865
|
+
{ key: 1,
|
|
866
|
+
payload: { test_id: 'abc', some_int: 3 } },
|
|
867
|
+
{ key: 2,
|
|
868
|
+
payload: { test_id: 'def', some_int: 4 } }
|
|
869
|
+
]
|
|
870
|
+
)
|
|
871
|
+
expect(MyBatchConsumer.last_post_process_batch.map(&:payload).map do |p|
|
|
872
|
+
p[:test_id]
|
|
873
|
+
end).to contain_exactly('abc', 'def')
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
it 'is called with filtered messages when process_message? excludes some' do
|
|
877
|
+
publish_batch(
|
|
878
|
+
[
|
|
879
|
+
{ key: 1,
|
|
880
|
+
payload: { test_id: 'abc', some_int: 3 } },
|
|
881
|
+
{ key: 2,
|
|
882
|
+
payload: { test_id: 'skipme', some_int: 4 } },
|
|
883
|
+
{ key: 3,
|
|
884
|
+
payload: { test_id: 'ghi', some_int: 5 } }
|
|
885
|
+
]
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
expect(MyBatchConsumer.last_post_process_batch.size).to eq(2)
|
|
889
|
+
expect(MyBatchConsumer.last_post_process_batch.map(&:key).map(&:to_i)).to contain_exactly(1, 3)
|
|
890
|
+
end
|
|
891
|
+
end
|
|
892
|
+
|
|
808
893
|
end
|
|
809
894
|
end
|
|
810
895
|
# rubocop:enable Lint/ConstantDefinitionInBlock
|
data/spec/deimos_spec.rb
CHANGED
|
@@ -164,6 +164,34 @@ describe Deimos do
|
|
|
164
164
|
end
|
|
165
165
|
end
|
|
166
166
|
|
|
167
|
+
describe 'configure' do
|
|
168
|
+
it 'should reset cached backends when configure is called again' do
|
|
169
|
+
Karafka::App.routes.redraw do
|
|
170
|
+
topic 'configure-test-topic' do
|
|
171
|
+
active false
|
|
172
|
+
schema 'MySchema'
|
|
173
|
+
namespace 'com.my-namespace'
|
|
174
|
+
key_config none: true
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
topic_config = described_class.karafka_configs.find { |c| c.name == 'configure-test-topic' }
|
|
179
|
+
payload_transcoder = topic_config.deserializers[:payload]
|
|
180
|
+
|
|
181
|
+
# Access the backend to cache it
|
|
182
|
+
first_backend = payload_transcoder.backend
|
|
183
|
+
|
|
184
|
+
# Call configure again (simulating a second configure call after topics are defined)
|
|
185
|
+
described_class.configure do |config|
|
|
186
|
+
config.schema.backend = :avro_validation
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# The cached backend should have been reset, so a new one is created
|
|
190
|
+
second_backend = payload_transcoder.backend
|
|
191
|
+
expect(second_backend).not_to equal(first_backend)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
167
195
|
describe '#schema_backend_class with mock_backends' do
|
|
168
196
|
after(:each) do
|
|
169
197
|
described_class.mock_backends = false
|
|
@@ -26,6 +26,18 @@ describe Deimos::SchemaBackends::Base do
|
|
|
26
26
|
backend.decode(payload, schema: 'schema2')
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
it 'should use explicit schema as subject when topic is nil' do
|
|
30
|
+
expect(backend).to receive(:validate).with(payload, schema: 'schema2')
|
|
31
|
+
expect(backend).to receive(:encode_payload).with(payload, schema: 'schema2', subject: 'schema2-value')
|
|
32
|
+
backend.encode(payload, schema: 'schema2')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'should use explicit schema as subject for key when topic is nil' do
|
|
36
|
+
expect(backend).to receive(:validate).with(payload, schema: 'schema2')
|
|
37
|
+
expect(backend).to receive(:encode_payload).with(payload, schema: 'schema2', subject: 'schema2-key')
|
|
38
|
+
backend.encode(payload, schema: 'schema2', is_key: true)
|
|
39
|
+
end
|
|
40
|
+
|
|
29
41
|
it 'should return nil if passed nil' do
|
|
30
42
|
expect(backend.decode(nil, schema: 'schema2')).to be_nil
|
|
31
43
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: deimos-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.4.0.pre.
|
|
4
|
+
version: 2.4.0.pre.beta19
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Orner
|
|
@@ -225,6 +225,20 @@ dependencies:
|
|
|
225
225
|
- - "~>"
|
|
226
226
|
- !ruby/object:Gem::Version
|
|
227
227
|
version: '1.1'
|
|
228
|
+
- !ruby/object:Gem::Dependency
|
|
229
|
+
name: pry
|
|
230
|
+
requirement: !ruby/object:Gem::Requirement
|
|
231
|
+
requirements:
|
|
232
|
+
- - "~>"
|
|
233
|
+
- !ruby/object:Gem::Version
|
|
234
|
+
version: 0.14.1
|
|
235
|
+
type: :development
|
|
236
|
+
prerelease: false
|
|
237
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
238
|
+
requirements:
|
|
239
|
+
- - "~>"
|
|
240
|
+
- !ruby/object:Gem::Version
|
|
241
|
+
version: 0.14.1
|
|
228
242
|
- !ruby/object:Gem::Dependency
|
|
229
243
|
name: rails
|
|
230
244
|
requirement: !ruby/object:Gem::Requirement
|