deimos-ruby 1.8.2.pre.beta1 → 1.8.5

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