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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -0
- data/Gemfile.lock +84 -79
- data/README.md +41 -3
- data/deimos-ruby.gemspec +2 -2
- data/docs/CONFIGURATION.md +1 -0
- data/docs/INTEGRATION_TESTS.md +52 -0
- data/docs/PULL_REQUEST_TEMPLATE.md +1 -0
- data/docs/UPGRADING.md +128 -0
- data/lib/deimos/active_record_consume/message_consumption.rb +9 -0
- data/lib/deimos/active_record_consumer.rb +8 -0
- data/lib/deimos/backends/db.rb +10 -1
- data/lib/deimos/config/configurable.rb +12 -0
- data/lib/deimos/config/configuration.rb +4 -0
- data/lib/deimos/config/phobos_config.rb +4 -1
- data/lib/deimos/kafka_source.rb +3 -2
- data/lib/deimos/kafka_topic_info.rb +2 -5
- data/lib/deimos/producer.rb +5 -3
- data/lib/deimos/schema_backends/avro_schema_coercer.rb +5 -3
- data/lib/deimos/schema_backends/avro_schema_registry.rb +1 -1
- data/lib/deimos/utils/db_poller.rb +2 -1
- data/lib/deimos/utils/db_producer.rb +5 -1
- data/lib/deimos/utils/inline_consumer.rb +9 -3
- data/lib/deimos/utils/schema_controller_mixin.rb +5 -1
- data/lib/deimos/version.rb +1 -1
- data/spec/active_record_consumer_spec.rb +13 -0
- data/spec/backends/db_spec.rb +6 -0
- data/spec/config/configuration_spec.rb +15 -0
- data/spec/generators/active_record_generator_spec.rb +1 -1
- data/spec/kafka_source_spec.rb +83 -0
- data/spec/kafka_topic_info_spec.rb +6 -6
- data/spec/producer_spec.rb +49 -0
- data/spec/schema_backends/avro_base_shared.rb +26 -1
- data/spec/schemas/com/my-namespace/request/CreateTopic.avsc +11 -0
- data/spec/schemas/com/my-namespace/response/CreateTopic.avsc +11 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/utils/db_producer_spec.rb +27 -0
- data/spec/utils/inline_consumer_spec.rb +31 -0
- data/spec/utils/schema_controller_mixin_spec.rb +16 -0
- 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
|
data/docs/UPGRADING.md
ADDED
@@ -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
|
data/lib/deimos/backends/db.rb
CHANGED
@@ -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
|
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|
|
data/lib/deimos/kafka_source.rb
CHANGED
@@ -88,8 +88,9 @@ module Deimos
|
|
88
88
|
array_of_attributes,
|
89
89
|
options={})
|
90
90
|
results = super
|
91
|
-
|
92
|
-
|
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
|
-
|
89
|
-
|
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
|
data/lib/deimos/producer.rb
CHANGED
@@ -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
|
48
|
-
|
49
|
-
|
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.
|
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,
|
25
|
+
@consumer.seek(topic, 0, offset)
|
23
26
|
end
|
24
27
|
rescue StandardError => e
|
25
|
-
|
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>]
|
data/lib/deimos/version.rb
CHANGED
@@ -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
|