deimos-ruby 1.8.2.pre.beta1 → 1.8.5

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/Gemfile.lock +84 -79
  4. data/README.md +41 -3
  5. data/deimos-ruby.gemspec +2 -2
  6. data/docs/CONFIGURATION.md +1 -0
  7. data/docs/INTEGRATION_TESTS.md +52 -0
  8. data/docs/PULL_REQUEST_TEMPLATE.md +1 -0
  9. data/docs/UPGRADING.md +128 -0
  10. data/lib/deimos/active_record_consume/message_consumption.rb +9 -0
  11. data/lib/deimos/active_record_consumer.rb +8 -0
  12. data/lib/deimos/backends/db.rb +10 -1
  13. data/lib/deimos/config/configurable.rb +12 -0
  14. data/lib/deimos/config/configuration.rb +4 -0
  15. data/lib/deimos/config/phobos_config.rb +4 -1
  16. data/lib/deimos/kafka_source.rb +3 -2
  17. data/lib/deimos/kafka_topic_info.rb +2 -5
  18. data/lib/deimos/producer.rb +5 -3
  19. data/lib/deimos/schema_backends/avro_schema_coercer.rb +5 -3
  20. data/lib/deimos/schema_backends/avro_schema_registry.rb +1 -1
  21. data/lib/deimos/utils/db_poller.rb +2 -1
  22. data/lib/deimos/utils/db_producer.rb +5 -1
  23. data/lib/deimos/utils/inline_consumer.rb +9 -3
  24. data/lib/deimos/utils/schema_controller_mixin.rb +5 -1
  25. data/lib/deimos/version.rb +1 -1
  26. data/spec/active_record_consumer_spec.rb +13 -0
  27. data/spec/backends/db_spec.rb +6 -0
  28. data/spec/config/configuration_spec.rb +15 -0
  29. data/spec/generators/active_record_generator_spec.rb +1 -1
  30. data/spec/kafka_source_spec.rb +83 -0
  31. data/spec/kafka_topic_info_spec.rb +6 -6
  32. data/spec/producer_spec.rb +49 -0
  33. data/spec/schema_backends/avro_base_shared.rb +26 -1
  34. data/spec/schemas/com/my-namespace/request/CreateTopic.avsc +11 -0
  35. data/spec/schemas/com/my-namespace/response/CreateTopic.avsc +11 -0
  36. data/spec/spec_helper.rb +1 -1
  37. data/spec/utils/db_producer_spec.rb +27 -0
  38. data/spec/utils/inline_consumer_spec.rb +31 -0
  39. data/spec/utils/schema_controller_mixin_spec.rb +16 -0
  40. metadata +21 -13
@@ -28,6 +28,7 @@ Please describe the tests that you ran to verify your changes. Provide instructi
28
28
  - [ ] I have performed a self-review of my own code
29
29
  - [ ] I have commented my code, particularly in hard-to-understand areas
30
30
  - [ ] I have made corresponding changes to the documentation
31
+ - [ ] I have added a line in the CHANGELOG describing this change, under the UNRELEASED heading
31
32
  - [ ] My changes generate no new warnings
32
33
  - [ ] I have added tests that prove my fix is effective or that my feature works
33
34
  - [ ] New and existing unit tests pass locally with my changes
@@ -0,0 +1,128 @@
1
+ # Upgrading Deimos
2
+
3
+ ## Upgrading from < 1.5.0 to >= 1.5.0
4
+
5
+ If you are using Confluent's schema registry to Avro-encode your
6
+ messages, you will need to manually include the `avro_turf` gem
7
+ in your Gemfile now.
8
+
9
+ This update changes how to interact with Deimos's schema classes.
10
+ Although these are meant to be internal, they are still "public"
11
+ and can be used by calling code.
12
+
13
+ Before 1.5.0:
14
+
15
+ ```ruby
16
+ encoder = Deimos::AvroDataEncoder.new(schema: 'MySchema',
17
+ namespace: 'com.my-namespace')
18
+ encoder.encode(my_payload)
19
+
20
+ decoder = Deimos::AvroDataDecoder.new(schema: 'MySchema',
21
+ namespace: 'com.my-namespace')
22
+ decoder.decode(my_payload)
23
+ ```
24
+
25
+ After 1.5.0:
26
+ ```ruby
27
+ backend = Deimos.schema_backend(schema: 'MySchema', namespace: 'com.my-namespace')
28
+ backend.encode(my_payload)
29
+ backend.decode(my_payload)
30
+ ```
31
+
32
+ The two classes are different and if you are using them to e.g.
33
+ inspect Avro schema fields, please look at the source code for the following:
34
+ * `Deimos::SchemaBackends::Base`
35
+ * `Deimos::SchemaBackends::AvroBase`
36
+ * `Deimos::SchemaBackends::AvroSchemaRegistry`
37
+
38
+ Deprecated `Deimos::TestHelpers.sent_messages` in favor of
39
+ `Deimos::Backends::Test.sent_messages`.
40
+
41
+ ## Upgrading from < 1.4.0 to >= 1.4.0
42
+
43
+ Previously, configuration was handled as follows:
44
+ * Kafka configuration, including listeners, lived in `phobos.yml`
45
+ * Additional Deimos configuration would live in an initializer, e.g. `kafka.rb`
46
+ * Producer and consumer configuration lived in each individual producer and consumer
47
+
48
+ As of 1.4.0, all configuration is centralized in one initializer
49
+ file, using default configuration.
50
+
51
+ Before 1.4.0:
52
+ ```yaml
53
+ # config/phobos.yml
54
+ logger:
55
+ file: log/phobos.log
56
+ level: debug
57
+ ruby_kafka:
58
+ level: debug
59
+
60
+ kafka:
61
+ client_id: phobos
62
+ connect_timeout: 15
63
+ socket_timeout: 15
64
+
65
+ producer:
66
+ ack_timeout: 5
67
+ required_acks: :all
68
+ ...
69
+
70
+ listeners:
71
+ - handler: ConsumerTest::MyConsumer
72
+ topic: my_consume_topic
73
+ group_id: my_group_id
74
+ - handler: ConsumerTest::MyBatchConsumer
75
+ topic: my_batch_consume_topic
76
+ group_id: my_batch_group_id
77
+ delivery: inline_batch
78
+ ```
79
+
80
+ ```ruby
81
+ # kafka.rb
82
+ Deimos.configure do |config|
83
+ config.reraise_consumer_errors = true
84
+ config.logger = Rails.logger
85
+ ...
86
+ end
87
+
88
+ # my_consumer.rb
89
+ class ConsumerTest::MyConsumer < Deimos::Producer
90
+ namespace 'com.my-namespace'
91
+ schema 'MySchema'
92
+ topic 'MyTopic'
93
+ key_config field: :id
94
+ end
95
+ ```
96
+
97
+ After 1.4.0:
98
+ ```ruby
99
+ kafka.rb
100
+ Deimos.configure do
101
+ logger Rails.logger
102
+ kafka do
103
+ client_id 'phobos'
104
+ connect_timeout 15
105
+ socket_timeout 15
106
+ end
107
+ producers.ack_timeout 5
108
+ producers.required_acks :all
109
+ ...
110
+ consumer do
111
+ class_name 'ConsumerTest::MyConsumer'
112
+ topic 'my_consume_topic'
113
+ group_id 'my_group_id'
114
+ namespace 'com.my-namespace'
115
+ schema 'MySchema'
116
+ topic 'MyTopic'
117
+ key_config field: :id
118
+ end
119
+ ...
120
+ end
121
+ ```
122
+
123
+ Note that the old configuration way *will* work if you set
124
+ `config.phobos_config_file = "config/phobos.yml"`. You will
125
+ get a number of deprecation notices, however. You can also still
126
+ set the topic, namespace, etc. on the producer/consumer class,
127
+ but it's much more convenient to centralize these configs
128
+ in one place to see what your app does.
@@ -26,6 +26,15 @@ module Deimos
26
26
 
27
27
  # :nodoc:
28
28
  def consume(payload, metadata)
29
+ unless self.process_message?(payload)
30
+ Deimos.config.logger.debug(
31
+ message: 'Skipping processing of message',
32
+ payload: payload,
33
+ metadata: metadata
34
+ )
35
+ return
36
+ end
37
+
29
38
  key = metadata.with_indifferent_access[:key]
30
39
  klass = self.class.config[:record_class]
31
40
  record = fetch_record(klass, (payload || {}).with_indifferent_access, key)
@@ -55,5 +55,13 @@ module Deimos
55
55
  def record_attributes(payload, _key=nil)
56
56
  @converter.convert(payload)
57
57
  end
58
+
59
+ # Override this message to conditionally save records
60
+ # @param payload [Hash] The kafka message as a hash
61
+ # @return [Boolean] if true, record is created/update.
62
+ # If false, record processing is skipped but message offset is still committed.
63
+ def process_message?(_payload)
64
+ true
65
+ end
58
66
  end
59
67
  end
@@ -14,7 +14,7 @@ module Deimos
14
14
  message = Deimos::KafkaMessage.new(
15
15
  message: m.encoded_payload ? m.encoded_payload.to_s.b : nil,
16
16
  topic: m.topic,
17
- partition_key: m.partition_key || m.key
17
+ partition_key: partition_key_for(m)
18
18
  )
19
19
  message.key = m.encoded_key.to_s.b unless producer_class.config[:no_keys]
20
20
  message
@@ -26,6 +26,15 @@ module Deimos
26
26
  by: records.size
27
27
  )
28
28
  end
29
+
30
+ # @param message [Deimos::Message]
31
+ # @return [String] the partition key to use for this message
32
+ def partition_key_for(message)
33
+ return message.partition_key if message.partition_key.present?
34
+ return message.key unless message.key.is_a?(Hash)
35
+
36
+ message.key.to_yaml
37
+ end
29
38
  end
30
39
  end
31
40
  end
@@ -15,6 +15,11 @@ module Deimos
15
15
  # enabled true
16
16
  # ca_cert_file 'my_file'
17
17
  # end
18
+ # config.kafka do
19
+ # ssl do
20
+ # enabled true
21
+ # end
22
+ # end
18
23
  # end
19
24
  # - Allows for arrays of configurations:
20
25
  # Deimos.configure do |config|
@@ -245,6 +250,13 @@ module Deimos
245
250
 
246
251
  # Configure the settings with values.
247
252
  def configure(&block)
253
+ if defined?(Rake) && defined?(Rake.application)
254
+ tasks = Rake.application.top_level_tasks
255
+ if tasks.any? { |t| %w(assets webpacker yarn).include?(t.split(':').first) }
256
+ puts 'Skipping Deimos configuration since we are in JS/CSS compilation'
257
+ return
258
+ end
259
+ end
248
260
  config.run_callbacks(:configure) do
249
261
  config.instance_eval(&block)
250
262
  end
@@ -319,6 +319,10 @@ module Deimos
319
319
  # Key configuration (see docs).
320
320
  # @return [Hash]
321
321
  setting :key_config
322
+ # Set to true to ignore the consumer in the Phobos config and not actually start up a
323
+ # listener.
324
+ # @return [Boolean]
325
+ setting :disabled, false
322
326
 
323
327
  # These are the phobos "listener" configs. See CONFIGURATION.md for more
324
328
  # info.
@@ -63,8 +63,10 @@ module Deimos
63
63
  }
64
64
 
65
65
  p_config[:listeners] = self.consumer_objects.map do |consumer|
66
+ next nil if consumer.disabled
67
+
66
68
  hash = consumer.to_h.reject do |k, _|
67
- %i(class_name schema namespace key_config backoff).include?(k)
69
+ %i(class_name schema namespace key_config backoff disabled).include?(k)
68
70
  end
69
71
  hash = hash.map { |k, v| [k, v.is_a?(Symbol) ? v.to_s : v] }.to_h
70
72
  hash[:handler] = consumer.class_name
@@ -73,6 +75,7 @@ module Deimos
73
75
  end
74
76
  hash
75
77
  end
78
+ p_config[:listeners].compact!
76
79
 
77
80
  if self.kafka.ssl.enabled
78
81
  %w(ca_cert client_cert client_cert_key).each do |key|
@@ -88,8 +88,9 @@ module Deimos
88
88
  array_of_attributes,
89
89
  options={})
90
90
  results = super
91
- return unless self.kafka_config[:import]
92
- return if array_of_attributes.empty?
91
+ if !self.kafka_config[:import] || array_of_attributes.empty?
92
+ return results
93
+ end
93
94
 
94
95
  # This will contain an array of hashes, where each hash is the actual
95
96
  # attribute hash that created the object.
@@ -85,11 +85,8 @@ module Deimos
85
85
  locked_at: Time.zone.now,
86
86
  error: true,
87
87
  retries: record.retries + 1 }
88
- if ActiveRecord::VERSION::MAJOR >= 4
89
- record.update!(attr_hash)
90
- else
91
- record.update_attributes!(attr_hash)
92
- end
88
+ record.attributes = attr_hash
89
+ record.save!
93
90
  end
94
91
 
95
92
  # Update the locked_at timestamp to indicate that the producer is still
@@ -104,6 +104,8 @@ module Deimos
104
104
  Deimos.config.producers.disabled ||
105
105
  Deimos.producers_disabled?(self)
106
106
 
107
+ raise 'Topic not specified. Please specify the topic.' if topic.blank?
108
+
107
109
  backend_class = determine_backend_class(sync, force_send)
108
110
  Deimos.instrument(
109
111
  'encode_messages',
@@ -183,7 +185,7 @@ module Deimos
183
185
  nil
184
186
  else
185
187
  encoder.encode(message.payload,
186
- topic: "#{config[:topic]}-value")
188
+ topic: "#{Deimos.config.producers.topic_prefix}#{config[:topic]}-value")
187
189
  end
188
190
  end
189
191
 
@@ -201,9 +203,9 @@ module Deimos
201
203
  end
202
204
 
203
205
  if config[:key_field]
204
- encoder.encode_key(config[:key_field], key, topic: "#{config[:topic]}-key")
206
+ encoder.encode_key(config[:key_field], key, topic: "#{Deimos.config.producers.topic_prefix}#{config[:topic]}-key")
205
207
  elsif config[:key_schema]
206
- key_encoder.encode(key, topic: "#{config[:topic]}-key")
208
+ key_encoder.encode(key, topic: "#{Deimos.config.producers.topic_prefix}#{config[:topic]}-key")
207
209
  else
208
210
  key
209
211
  end
@@ -44,9 +44,11 @@ module Deimos
44
44
 
45
45
  case field_type
46
46
  when :int, :long
47
- if val.is_a?(Integer) ||
48
- _is_integer_string?(val) ||
49
- int_classes.any? { |klass| val.is_a?(klass) }
47
+ if %w(timestamp-millis timestamp-micros).include?(type.logical_type)
48
+ val
49
+ elsif val.is_a?(Integer) ||
50
+ _is_integer_string?(val) ||
51
+ int_classes.any? { |klass| val.is_a?(klass) }
50
52
  val.to_i
51
53
  else
52
54
  val # this will fail
@@ -15,7 +15,7 @@ module Deimos
15
15
 
16
16
  # @override
17
17
  def encode_payload(payload, schema: nil, topic: nil)
18
- avro_turf_messaging.encode(payload, schema_name: schema, subject: topic)
18
+ avro_turf_messaging.encode(payload, schema_name: schema, subject: topic || schema)
19
19
  end
20
20
 
21
21
  private
@@ -142,7 +142,8 @@ module Deimos
142
142
  last_id = record.public_send(id_method)
143
143
  last_updated_at = last_updated(record)
144
144
  @producer.send_events(batch)
145
- @info.update_attributes!(last_sent: last_updated_at, last_sent_id: last_id)
145
+ @info.attributes = { last_sent: last_updated_at, last_sent_id: last_id }
146
+ @info.save!
146
147
  end
147
148
  end
148
149
  end
@@ -190,11 +190,14 @@ module Deimos
190
190
  end
191
191
  end
192
192
 
193
+ # Produce messages in batches, reducing the size 1/10 if the batch is too
194
+ # large. Does not retry batches of messages that have already been sent.
193
195
  # @param batch [Array<Hash>]
194
196
  def produce_messages(batch)
195
197
  batch_size = batch.size
198
+ current_index = 0
196
199
  begin
197
- batch.in_groups_of(batch_size, false).each do |group|
200
+ batch[current_index..-1].in_groups_of(batch_size, false).each do |group|
198
201
  @logger.debug("Publishing #{group.size} messages to #{@current_topic}")
199
202
  producer.publish_list(group)
200
203
  Deimos.config.metrics&.increment(
@@ -202,6 +205,7 @@ module Deimos
202
205
  tags: %W(status:success topic:#{@current_topic}),
203
206
  by: group.size
204
207
  )
208
+ current_index += group.size
205
209
  @logger.info("Sent #{group.size} messages to #{@current_topic}")
206
210
  end
207
211
  rescue Kafka::BufferOverflow, Kafka::MessageSizeTooLarge,
@@ -6,6 +6,7 @@ module Deimos
6
6
  module Utils
7
7
  # Listener that can seek to get the last X messages in a topic.
8
8
  class SeekListener < Phobos::Listener
9
+ MAX_SEEK_RETRIES = 3
9
10
  attr_accessor :num_messages
10
11
 
11
12
  # :nodoc:
@@ -13,16 +14,22 @@ module Deimos
13
14
  @num_messages ||= 10
14
15
  @consumer = create_kafka_consumer
15
16
  @consumer.subscribe(topic, @subscribe_opts)
17
+ attempt = 0
16
18
 
17
19
  begin
20
+ attempt += 1
18
21
  last_offset = @kafka_client.last_offset_for(topic, 0)
19
22
  offset = last_offset - num_messages
20
23
  if offset.positive?
21
24
  Deimos.config.logger.info("Seeking to #{offset}")
22
- @consumer.seek(topic, 1, offset)
25
+ @consumer.seek(topic, 0, offset)
23
26
  end
24
27
  rescue StandardError => e
25
- "Could not seek to offset: #{e.message}"
28
+ if attempt < MAX_SEEK_RETRIES
29
+ sleep(1.seconds * attempt)
30
+ retry
31
+ end
32
+ log_error("Could not seek to offset: #{e.message} after #{MAX_SEEK_RETRIES} retries", listener_metadata)
26
33
  end
27
34
 
28
35
  instrument('listener.start_handler', listener_metadata) do
@@ -50,7 +57,6 @@ module Deimos
50
57
 
51
58
  # :nodoc:
52
59
  def consume(payload, metadata)
53
- puts "Got #{payload}"
54
60
  self.class.total_messages << {
55
61
  key: metadata[:key],
56
62
  payload: payload
@@ -28,14 +28,18 @@ module Deimos
28
28
 
29
29
  # Indicate which schemas should be assigned to actions.
30
30
  # @param actions [Symbol]
31
+ # @param kwactions [String]
31
32
  # @param request [String]
32
33
  # @param response [String]
33
- def schemas(*actions, request: nil, response: nil)
34
+ def schemas(*actions, request: nil, response: nil, **kwactions)
34
35
  actions.each do |action|
35
36
  request ||= action.to_s.titleize
36
37
  response ||= action.to_s.titleize
37
38
  schema_mapping[action.to_s] = { request: request, response: response }
38
39
  end
40
+ kwactions.each do |key, val|
41
+ schema_mapping[key.to_s] = { request: val, response: val }
42
+ end
39
43
  end
40
44
 
41
45
  # @return [Hash<Symbol, String>]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Deimos
4
- VERSION = '1.8.2-beta1'
4
+ VERSION = '1.8.5'
5
5
  end
@@ -137,5 +137,18 @@ module ActiveRecordConsumerTest
137
137
  expect(Widget.find_by_test_id('id1').some_int).to eq(3)
138
138
  expect(Widget.find_by_test_id('id2').some_int).to eq(4)
139
139
  end
140
+
141
+ it 'should not create record of process_message returns false' do
142
+ MyConsumer.any_instance.stub(:process_message?).and_return(false)
143
+ expect(Widget.count).to eq(0)
144
+ test_consume_message(MyConsumer, {
145
+ test_id: 'abc',
146
+ some_int: 3,
147
+ updated_at: 1.day.ago.to_i,
148
+ some_datetime_int: Time.zone.now.to_i,
149
+ timestamp: 2.minutes.ago.to_s
150
+ }, { call_original: true, key: 5 })
151
+ expect(Widget.count).to eq(0)
152
+ end
140
153
  end
141
154
  end