deimos-ruby 1.19.7 → 1.20.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|