deimos-ruby 1.19.7 → 1.20.0
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 +5 -0
- data/docs/CONFIGURATION.md +2 -1
- data/lib/deimos/config/configuration.rb +4 -1
- data/lib/deimos/utils/db_poller/base.rb +45 -8
- data/lib/deimos/utils/db_poller/state_based.rb +4 -4
- data/lib/deimos/utils/db_poller/time_based.rb +9 -9
- data/lib/deimos/utils/db_poller.rb +12 -6
- data/lib/deimos/version.rb +1 -1
- data/spec/spec_helper.rb +2 -0
- data/spec/utils/db_poller_spec.rb +99 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 51c4ea2e760bba16fc5bc251bdd842f578340f40543d54287f408e76ea4c6dfd
|
4
|
+
data.tar.gz: 4bffdae791e9b8b2aa491c43ff7f4c0ba16a98582f9bee872af648da6afdf846
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 68565725b1ff47b86ba70e4a993befadb95f3a8e99fbef5a1d7f2dd51c25e5749e66c0ac8e82f5dafbb31f2605b3f03743cc774486bf616b1cd10c746df937bf
|
7
|
+
data.tar.gz: d1aa1f426f7c6b6894724ccc7a84661c6e4ca151e11f419cea024d5b562f0f40b80cfc44c3f1410ed125a4eae482261200870a5136af301de85bf2e5f9f5d3d4
|
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.20.0 - 2023-04-17
|
11
|
+
|
12
|
+
- Feature: Updated the DB Poller logic to allow for inherited db poller classes to initialize producers and
|
13
|
+
be able to publish to multiple Kafka topics using the results from a single poll_query method
|
14
|
+
|
10
15
|
# 1.19.7 - 2023-04-17
|
11
16
|
|
12
17
|
- Fix: Update Datadog metrics backend so it doesn't crash on newer versions of dogstatsd-ruby
|
data/docs/CONFIGURATION.md
CHANGED
@@ -114,7 +114,7 @@ end
|
|
114
114
|
```
|
115
115
|
|
116
116
|
Config name|Default|Description
|
117
|
-
|
117
|
+
-----------|-------|-----------
|
118
118
|
producer_class|nil|ActiveRecordProducer class to use for sending messages.
|
119
119
|
mode|:time_based|Whether to use time-based polling or state-based polling.
|
120
120
|
run_every|60|Amount of time in seconds to wait between runs.
|
@@ -127,6 +127,7 @@ state_column|nil|If set, this represents the DB column to use to update publishi
|
|
127
127
|
publish_timestamp_column|nil|If set, this represents the DB column to use to update when publishing is done. State-based only.
|
128
128
|
published_state|nil|If set, the poller will update the `state_column` to this value when publishing succeeds. State-based only.
|
129
129
|
failed_state|nil|If set, the poller will update the `state_column` to this value when publishing fails. State-based only.
|
130
|
+
poller_class|nil|Inherited poller class name to use for publishing to multiple kafka topics from a single poller.
|
130
131
|
|
131
132
|
## Kafka Configuration
|
132
133
|
|
@@ -453,7 +453,7 @@ module Deimos
|
|
453
453
|
# Mode to use for querying - :time_based (via updated_at) or :state_based.
|
454
454
|
setting :mode, :time_based
|
455
455
|
# Producer class to use for the poller.
|
456
|
-
setting :producer_class
|
456
|
+
setting :producer_class, nil
|
457
457
|
# How often to run the poller, in seconds. If the poll takes longer than this
|
458
458
|
# time, it will run again immediately and the timeout
|
459
459
|
# will be pushed to the next e.g. 1 minute.
|
@@ -481,6 +481,9 @@ module Deimos
|
|
481
481
|
setting :published_state
|
482
482
|
# Value to set the state_column to if publishing fails - state-based only.
|
483
483
|
setting :failed_state
|
484
|
+
|
485
|
+
# Inherited poller class name to use for publishing to multiple kafka topics from a single poller
|
486
|
+
setting :poller_class, nil
|
484
487
|
end
|
485
488
|
|
486
489
|
deprecate 'kafka_logger', 'kafka.logger'
|
@@ -21,18 +21,30 @@ module Deimos
|
|
21
21
|
# @return [Hash]
|
22
22
|
attr_reader :config
|
23
23
|
|
24
|
+
# Method to define producers if a single poller needs to publish to multiple topics.
|
25
|
+
# Producer classes should be constantized
|
26
|
+
# @return [Array<Producer>]
|
27
|
+
def self.producers
|
28
|
+
[]
|
29
|
+
end
|
30
|
+
|
24
31
|
# @param config [FigTree::ConfigStruct]
|
25
32
|
def initialize(config)
|
26
33
|
@config = config
|
27
34
|
@id = SecureRandom.hex
|
28
35
|
begin
|
29
|
-
@
|
36
|
+
if @config.poller_class.nil? && @config.producer_class.nil?
|
37
|
+
raise 'No producers have been set for this DB poller!'
|
38
|
+
end
|
39
|
+
|
40
|
+
@resource_class = self.class.producers.any? ? self.class : @config.producer_class.constantize
|
41
|
+
|
42
|
+
producer_classes.each do |producer_class|
|
43
|
+
validate_producer_class(producer_class)
|
44
|
+
end
|
30
45
|
rescue NameError
|
31
46
|
raise "Class #{@config.producer_class} not found!"
|
32
47
|
end
|
33
|
-
unless @producer < Deimos::ActiveRecordProducer
|
34
|
-
raise "Class #{@producer.class.name} is not an ActiveRecordProducer!"
|
35
|
-
end
|
36
48
|
end
|
37
49
|
|
38
50
|
# Start the poll:
|
@@ -65,12 +77,12 @@ module Deimos
|
|
65
77
|
# Grab the PollInfo or create if it doesn't exist.
|
66
78
|
# @return [void]
|
67
79
|
def retrieve_poll_info
|
68
|
-
@info = Deimos::PollInfo.find_by_producer(@
|
80
|
+
@info = Deimos::PollInfo.find_by_producer(@resource_class.to_s) || create_poll_info
|
69
81
|
end
|
70
82
|
|
71
83
|
# @return [Deimos::PollInfo]
|
72
84
|
def create_poll_info
|
73
|
-
Deimos::PollInfo.create!(producer: @
|
85
|
+
Deimos::PollInfo.create!(producer: @resource_class.to_s, last_sent: Time.new(0))
|
74
86
|
end
|
75
87
|
|
76
88
|
# Indicate whether this current loop should process updates. Most loops
|
@@ -101,7 +113,7 @@ module Deimos
|
|
101
113
|
begin
|
102
114
|
span = Deimos.config.tracer&.start(
|
103
115
|
'deimos-db-poller',
|
104
|
-
resource: @
|
116
|
+
resource: @resource_class.name.gsub('::', '-')
|
105
117
|
)
|
106
118
|
process_batch(batch)
|
107
119
|
Deimos.config.tracer&.finish(span)
|
@@ -128,10 +140,35 @@ module Deimos
|
|
128
140
|
true
|
129
141
|
end
|
130
142
|
|
143
|
+
# Publish batch using the configured producers
|
131
144
|
# @param batch [Array<ActiveRecord::Base>]
|
132
145
|
# @return [void]
|
133
146
|
def process_batch(batch)
|
134
|
-
|
147
|
+
producer_classes.each do |producer|
|
148
|
+
producer.send_events(batch)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Configure log identifier and messages to be used in subclasses
|
153
|
+
# @return [String]
|
154
|
+
def log_identifier
|
155
|
+
"#{@resource_class.name}: #{producer_classes.map(&:topic)}"
|
156
|
+
end
|
157
|
+
|
158
|
+
# Return array of configured producers depending on poller class
|
159
|
+
# @return [Array<ActiveRecordProducer>]
|
160
|
+
def producer_classes
|
161
|
+
return self.class.producers if self.class.producers.any?
|
162
|
+
|
163
|
+
[@config.producer_class.constantize]
|
164
|
+
end
|
165
|
+
|
166
|
+
# Validate if a producer class is an ActiveRecordProducer or not
|
167
|
+
# @return [void]
|
168
|
+
def validate_producer_class(producer_class)
|
169
|
+
unless producer_class < Deimos::ActiveRecordProducer
|
170
|
+
raise "Class #{producer_class.class.name} is not an ActiveRecordProducer!"
|
171
|
+
end
|
135
172
|
end
|
136
173
|
end
|
137
174
|
end
|
@@ -10,13 +10,13 @@ module Deimos
|
|
10
10
|
# Send messages for updated data.
|
11
11
|
# @return [void]
|
12
12
|
def process_updates
|
13
|
-
Deimos.config.logger.info("Polling #{
|
13
|
+
Deimos.config.logger.info("Polling #{log_identifier}")
|
14
14
|
status = PollStatus.new(0, 0, 0)
|
15
15
|
|
16
16
|
# poll_query gets all the relevant data from the database, as defined
|
17
17
|
# by the producer itself.
|
18
18
|
loop do
|
19
|
-
Deimos.config.logger.debug("Polling #{
|
19
|
+
Deimos.config.logger.debug("Polling #{log_identifier}, batch #{status.current_batch}")
|
20
20
|
batch = fetch_results.to_a
|
21
21
|
if batch.empty?
|
22
22
|
@info.touch(:last_sent)
|
@@ -26,12 +26,12 @@ module Deimos
|
|
26
26
|
success = process_batch_with_span(batch, status)
|
27
27
|
finalize_batch(batch, success)
|
28
28
|
end
|
29
|
-
Deimos.config.logger.info("Poll #{
|
29
|
+
Deimos.config.logger.info("Poll #{log_identifier} complete (#{status.report}")
|
30
30
|
end
|
31
31
|
|
32
32
|
# @return [ActiveRecord::Relation]
|
33
33
|
def fetch_results
|
34
|
-
@
|
34
|
+
@resource_class.poll_query.limit(BATCH_SIZE).order(@config.timestamp_column)
|
35
35
|
end
|
36
36
|
|
37
37
|
# @param batch [Array<ActiveRecord::Base>]
|
@@ -11,7 +11,7 @@ module Deimos
|
|
11
11
|
# :nodoc:
|
12
12
|
def create_poll_info
|
13
13
|
new_time = @config.start_from_beginning ? Time.new(0) : Time.zone.now
|
14
|
-
Deimos::PollInfo.create!(producer: @
|
14
|
+
Deimos::PollInfo.create!(producer: @resource_class.to_s,
|
15
15
|
last_sent: new_time,
|
16
16
|
last_sent_id: 0)
|
17
17
|
end
|
@@ -28,13 +28,13 @@ module Deimos
|
|
28
28
|
def process_updates
|
29
29
|
time_from = @config.full_table ? Time.new(0) : @info.last_sent.in_time_zone
|
30
30
|
time_to = Time.zone.now - @config.delay_time
|
31
|
-
Deimos.config.logger.info("Polling #{
|
31
|
+
Deimos.config.logger.info("Polling #{log_identifier} from #{time_from} to #{time_to}")
|
32
32
|
status = PollStatus.new(0, 0, 0)
|
33
33
|
|
34
34
|
# poll_query gets all the relevant data from the database, as defined
|
35
35
|
# by the producer itself.
|
36
36
|
loop do
|
37
|
-
Deimos.config.logger.debug("Polling #{
|
37
|
+
Deimos.config.logger.debug("Polling #{log_identifier}, batch #{status.current_batch}")
|
38
38
|
batch = fetch_results(time_from, time_to).to_a
|
39
39
|
if batch.empty?
|
40
40
|
@info.touch(:last_sent)
|
@@ -44,20 +44,20 @@ module Deimos
|
|
44
44
|
process_and_touch_info(batch, status)
|
45
45
|
time_from = last_updated(batch.last)
|
46
46
|
end
|
47
|
-
Deimos.config.logger.info("Poll #{
|
47
|
+
Deimos.config.logger.info("Poll #{log_identifier} complete at #{time_to} (#{status.report})")
|
48
48
|
end
|
49
49
|
|
50
50
|
# @param time_from [ActiveSupport::TimeWithZone]
|
51
51
|
# @param time_to [ActiveSupport::TimeWithZone]
|
52
52
|
# @return [ActiveRecord::Relation]
|
53
53
|
def fetch_results(time_from, time_to)
|
54
|
-
id =
|
54
|
+
id = self.producer_classes.first.config[:record_class].primary_key
|
55
55
|
quoted_timestamp = ActiveRecord::Base.connection.quote_column_name(@config.timestamp_column)
|
56
56
|
quoted_id = ActiveRecord::Base.connection.quote_column_name(id)
|
57
|
-
@
|
58
|
-
|
59
|
-
|
60
|
-
|
57
|
+
@resource_class.poll_query(time_from: time_from,
|
58
|
+
time_to: time_to,
|
59
|
+
column_name: @config.timestamp_column,
|
60
|
+
min_id: @info.last_sent_id).
|
61
61
|
limit(BATCH_SIZE).
|
62
62
|
order("#{quoted_timestamp}, #{quoted_id}")
|
63
63
|
end
|
@@ -12,7 +12,7 @@ module Deimos
|
|
12
12
|
end
|
13
13
|
|
14
14
|
pollers = Deimos.config.db_poller_objects.map do |poller_config|
|
15
|
-
self.class_for_config(poller_config
|
15
|
+
self.class_for_config(poller_config).new(poller_config)
|
16
16
|
end
|
17
17
|
executor = Sigurd::Executor.new(pollers,
|
18
18
|
sleep_seconds: 5,
|
@@ -21,15 +21,21 @@ module Deimos
|
|
21
21
|
signal_handler.run!
|
22
22
|
end
|
23
23
|
|
24
|
-
# @param config_name [
|
24
|
+
# @param config_name [DBPollerConfig]
|
25
25
|
# @return [Class<Deimos::Utils::DbPoller>]
|
26
26
|
def self.class_for_config(config_name)
|
27
|
-
|
28
|
-
|
29
|
-
Deimos::Utils::DbPoller::StateBased
|
27
|
+
if config_name.poller_class.present?
|
28
|
+
config_name.poller_class.constantize
|
30
29
|
else
|
31
|
-
|
30
|
+
case config_name.mode
|
31
|
+
when :state_based
|
32
|
+
Deimos::Utils::DbPoller::StateBased
|
33
|
+
else
|
34
|
+
Deimos::Utils::DbPoller::TimeBased
|
35
|
+
end
|
32
36
|
end
|
37
|
+
rescue NameError
|
38
|
+
raise "Class #{config_name.poller_class} not found!"
|
33
39
|
end
|
34
40
|
|
35
41
|
PollStatus = Struct.new(:batches_processed, :batches_errored, :messages_processed) do
|
data/lib/deimos/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -68,7 +68,7 @@ each_db_config(Deimos::Utils::DbPoller::Base) do
|
|
68
68
|
include_context 'with widgets'
|
69
69
|
|
70
70
|
let(:poller) do
|
71
|
-
poller = Deimos::Utils::DbPoller.class_for_config(config
|
71
|
+
poller = Deimos::Utils::DbPoller.class_for_config(config).new(config)
|
72
72
|
allow(poller).to receive(:sleep)
|
73
73
|
poller
|
74
74
|
end
|
@@ -387,5 +387,103 @@ each_db_config(Deimos::Utils::DbPoller::Base) do
|
|
387
387
|
end
|
388
388
|
end
|
389
389
|
end
|
390
|
+
|
391
|
+
describe 'multi_producer_pollers' do
|
392
|
+
include_context 'with widgets'
|
393
|
+
|
394
|
+
let(:poller) do
|
395
|
+
poller = Deimos::Utils::DbPoller.class_for_config(config).new(config)
|
396
|
+
allow(poller).to receive(:sleep)
|
397
|
+
poller
|
398
|
+
end
|
399
|
+
|
400
|
+
let(:config) { Deimos.config.db_poller_objects.first.dup }
|
401
|
+
|
402
|
+
before(:each) do
|
403
|
+
Widget.delete_all
|
404
|
+
producer_class = Class.new(Deimos::ActiveRecordProducer) do
|
405
|
+
schema 'MySchemaWithId'
|
406
|
+
namespace 'com.my-namespace'
|
407
|
+
topic 'my-topic-with-id'
|
408
|
+
key_config none: true
|
409
|
+
record_class Widget
|
410
|
+
|
411
|
+
# :nodoc:
|
412
|
+
def self.generate_payload(attrs, widget)
|
413
|
+
super.merge(message_id: widget.generated_id)
|
414
|
+
end
|
415
|
+
end
|
416
|
+
stub_const('ProducerOne', producer_class)
|
417
|
+
|
418
|
+
producer_class = Class.new(Deimos::ActiveRecordProducer) do
|
419
|
+
schema 'MySchemaWithId'
|
420
|
+
namespace 'com.my-namespace'
|
421
|
+
topic 'my-topic-with-id'
|
422
|
+
key_config none: true
|
423
|
+
record_class Widget
|
424
|
+
|
425
|
+
# :nodoc:
|
426
|
+
def self.generate_payload(attrs, widget)
|
427
|
+
super.merge(message_id: widget.generated_id)
|
428
|
+
end
|
429
|
+
end
|
430
|
+
stub_const('ProducerTwo', producer_class)
|
431
|
+
|
432
|
+
poller_class = Class.new(Deimos::Utils::DbPoller::StateBased) do
|
433
|
+
def self.producers
|
434
|
+
[ProducerOne, ProducerTwo]
|
435
|
+
end
|
436
|
+
|
437
|
+
def self.poll_query(*)
|
438
|
+
Widget.where(publish_status: nil)
|
439
|
+
end
|
440
|
+
end
|
441
|
+
stub_const('Deimos::Utils::DbPoller::MultiProducerPoller', poller_class)
|
442
|
+
end
|
443
|
+
|
444
|
+
it 'should publish to two different kafka topics from two producers' do
|
445
|
+
Deimos.configure do
|
446
|
+
db_poller do
|
447
|
+
poller_class 'Deimos::Utils::DbPoller::MultiProducerPoller'
|
448
|
+
mode :state_based
|
449
|
+
state_column :publish_status
|
450
|
+
publish_timestamp_column :published_at
|
451
|
+
published_state 'PUBLISHED'
|
452
|
+
failed_state 'PUBLISH_FAILED'
|
453
|
+
run_every 1.minute
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
widgets = (1..3).map do |i|
|
458
|
+
Widget.create!(test_id: 'some_id', some_int: i,
|
459
|
+
updated_at: time_value(mins: -61, secs: 30 + i),
|
460
|
+
publish_status: nil, published_at: nil)
|
461
|
+
end
|
462
|
+
poller.retrieve_poll_info
|
463
|
+
allow(Deimos::Utils::DbPoller::MultiProducerPoller).to receive(:poll_query).and_call_original
|
464
|
+
allow(ProducerOne).to receive(:send_events)
|
465
|
+
allow(ProducerTwo).to receive(:send_events)
|
466
|
+
expect(Deimos::Utils::DbPoller::MultiProducerPoller).to receive(:poll_query).at_least(:once)
|
467
|
+
poller.process_updates
|
468
|
+
|
469
|
+
expect(ProducerOne).to have_received(:send_events).with(widgets)
|
470
|
+
expect(ProducerTwo).to have_received(:send_events).with(widgets)
|
471
|
+
expect(widgets.map(&:reload).map(&:publish_status)).to eq(%w(PUBLISHED PUBLISHED PUBLISHED))
|
472
|
+
end
|
473
|
+
|
474
|
+
it 'should raise an error if producer_class and poller_class are both not configured' do
|
475
|
+
Deimos.configure do
|
476
|
+
db_poller do
|
477
|
+
mode :state_based
|
478
|
+
state_column :publish_status
|
479
|
+
publish_timestamp_column :published_at
|
480
|
+
published_state 'PUBLISHED'
|
481
|
+
failed_state 'PUBLISH_FAILED'
|
482
|
+
run_every 1.minute
|
483
|
+
end
|
484
|
+
end
|
485
|
+
expect { described_class.new(config) }.to raise_error('No producers have been set for this DB poller!')
|
486
|
+
end
|
487
|
+
end
|
390
488
|
end
|
391
489
|
# rubocop:enable Layout/LineLength
|