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 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