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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 786ff723866bcb2f983f384e7dd8dbf6675ceb8c454a80d58a7c5eed7e730f3e
4
- data.tar.gz: 7bcd9693ce7fe0db27a63285c9ebc47a2299449c95f5526806fe4e2cb5be4f24
3
+ metadata.gz: 51c4ea2e760bba16fc5bc251bdd842f578340f40543d54287f408e76ea4c6dfd
4
+ data.tar.gz: 4bffdae791e9b8b2aa491c43ff7f4c0ba16a98582f9bee872af648da6afdf846
5
5
  SHA512:
6
- metadata.gz: 8fe1b840ebb4255e8df49e0600d5b8a85aad3c40807170b557cb43a236ed7b53af83a0348f8c3e6d4511f87987ade29be1855cf9f3c4a88bf7d254ba556c460e
7
- data.tar.gz: 43b43d127aff1b275453d86bd46ea38097a231419863afee300adb2914ef8a9734d086871d3dc4a96ae8148ddb7ab1423f113ca3fc6ff6d39dc411428a6f8472
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
@@ -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
- @producer = @config.producer_class.constantize
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(@config.producer_class) || create_poll_info
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: @config.producer_class, last_sent: Time.new(0))
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: @producer.class.name.gsub('::', '-')
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
- @producer.send_events(batch)
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 #{@producer.topic}")
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 #{@producer.topic}, batch #{status.current_batch}")
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 #{@producer.topic} complete (#{status.report}")
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
- @producer.poll_query.limit(BATCH_SIZE).order(@config.timestamp_column)
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: @config.producer_class,
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 #{@producer.topic} from #{time_from} to #{time_to}")
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 #{@producer.topic}, batch #{status.current_batch}")
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 #{@producer.topic} complete at #{time_to} (#{status.report})")
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 = @producer.config[:record_class].primary_key
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
- @producer.poll_query(time_from: time_from,
58
- time_to: time_to,
59
- column_name: @config.timestamp_column,
60
- min_id: @info.last_sent_id).
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.mode).new(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 [Symbol]
24
+ # @param config_name [DBPollerConfig]
25
25
  # @return [Class<Deimos::Utils::DbPoller>]
26
26
  def self.class_for_config(config_name)
27
- case config_name
28
- when :state_based
29
- Deimos::Utils::DbPoller::StateBased
27
+ if config_name.poller_class.present?
28
+ config_name.poller_class.constantize
30
29
  else
31
- Deimos::Utils::DbPoller::TimeBased
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Deimos
4
- VERSION = '1.19.7'
4
+ VERSION = '1.20.0'
5
5
  end
data/spec/spec_helper.rb CHANGED
@@ -221,6 +221,8 @@ RSpec.shared_context('with widgets') do
221
221
  t.string(:test_id)
222
222
  t.integer(:some_int)
223
223
  t.boolean(:some_bool)
224
+ t.string(:publish_status)
225
+ t.datetime(:published_at)
224
226
  t.timestamps
225
227
  end
226
228
 
@@ -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.mode).new(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
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: 1.19.7
4
+ version: 1.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Orner