deimos-ruby 1.0.0 → 1.1.0.pre.beta1
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 +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile.lock +3 -3
- data/README.md +79 -3
- data/deimos-ruby.gemspec +1 -1
- data/lib/deimos/base_consumer.rb +93 -0
- data/lib/deimos/batch_consumer.rb +147 -0
- data/lib/deimos/consumer.rb +5 -70
- data/lib/deimos/test_helpers.rb +119 -4
- data/lib/deimos/version.rb +1 -1
- data/lib/deimos.rb +26 -1
- data/spec/batch_consumer_spec.rb +247 -0
- data/spec/consumer_spec.rb +1 -1
- data/spec/deimos_spec.rb +108 -44
- data/spec/handlers/my_batch_consumer.rb +5 -0
- data/spec/handlers/my_consumer.rb +5 -0
- data/spec/phobos.yml +4 -0
- data/spec/spec_helper.rb +2 -0
- metadata +14 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b9cb2755df1f6588b130a9171d427a54301e7e5258d6f414d8e9950393b0a361
|
4
|
+
data.tar.gz: dc475fcb4e450a693501b6c3a148c93dec90e0c40212e40de991e2aa7b8afb4f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8b7594655e6051986ddc55efc7a5df32fafd3e445324b8d2592eeb406cbf607cf401b6c6ab9c4d7c76d6d8cfa3c8890d47400231dec15f330dbffb0341644a59
|
7
|
+
data.tar.gz: bc909ec5cb051ee58e6f7840a1d191e9881f72e3442d9eba35be14e28fada2ff070f318fd5d73d46eca3bdc05f7ed05e0b0ac4d8a98f8ff4f29377854dd129ca
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
deimos-ruby (1.0.
|
4
|
+
deimos-ruby (1.1.0.pre.beta1)
|
5
5
|
avro-patches (~> 0.3)
|
6
6
|
avro_turf (~> 0.8)
|
7
|
-
phobos (~> 1.8)
|
7
|
+
phobos (~> 1.8.2.pre.beta2)
|
8
8
|
ruby-kafka (~> 0.7)
|
9
9
|
|
10
10
|
GEM
|
@@ -87,7 +87,7 @@ GEM
|
|
87
87
|
parser (2.6.3.0)
|
88
88
|
ast (~> 2.4.0)
|
89
89
|
pg (1.1.3)
|
90
|
-
phobos (1.8.
|
90
|
+
phobos (1.8.2.pre.beta2)
|
91
91
|
activesupport (>= 3.0.0)
|
92
92
|
concurrent-ruby (>= 1.0.2)
|
93
93
|
concurrent-ruby-ext (>= 1.0.2)
|
data/README.md
CHANGED
@@ -21,6 +21,7 @@ Built on Phobos and hence Ruby-Kafka.
|
|
21
21
|
* [Kafka Message Keys](#kafka-message-keys)
|
22
22
|
* [Consumers](#consumers)
|
23
23
|
* [Rails Integration](#rails-integration)
|
24
|
+
* [Database Backend](#database-backend)
|
24
25
|
* [Running Consumers](#running-consumers)
|
25
26
|
* [Metrics](#metrics)
|
26
27
|
* [Testing](#testing)
|
@@ -335,8 +336,53 @@ class MyConsumer < Deimos::Consumer
|
|
335
336
|
def consume(payload, metadata)
|
336
337
|
# Same method as Phobos consumers.
|
337
338
|
# payload is an Avro-decoded hash.
|
338
|
-
#
|
339
|
-
# key
|
339
|
+
# metadata is a hash that contains information like :key and :topic.
|
340
|
+
# In general, your key should be included in the payload itself. However,
|
341
|
+
# if you need to access it separately from the payload, you can use
|
342
|
+
# metadata[:key]
|
343
|
+
end
|
344
|
+
end
|
345
|
+
```
|
346
|
+
|
347
|
+
### Batch Consumption
|
348
|
+
|
349
|
+
Instead of consuming messages one at a time, consumers can receive a batch of
|
350
|
+
messages as an array and then process them together. This can improve
|
351
|
+
consumer throughput, depending on the use case. Batch consumers behave like
|
352
|
+
other consumers in regards to key and payload decoding, etc.
|
353
|
+
|
354
|
+
To enable batch consumption, create a listener in `phobos.yml` and ensure that
|
355
|
+
the `delivery` property is set to `inline_batch`. For example:
|
356
|
+
|
357
|
+
```yaml
|
358
|
+
listeners:
|
359
|
+
- handler: Consumers::MyBatchConsumer
|
360
|
+
topic: my_batched_topic
|
361
|
+
group_id: my_group_id
|
362
|
+
delivery: inline_batch
|
363
|
+
```
|
364
|
+
|
365
|
+
Batch consumers must inherit from the Deimos::BatchConsumer class as in
|
366
|
+
this sample:
|
367
|
+
|
368
|
+
```ruby
|
369
|
+
class MyBatchConsumer < Deimos::BatchConsumer
|
370
|
+
|
371
|
+
# See the Consumer sample in the previous section
|
372
|
+
schema 'MySchema'
|
373
|
+
namespace 'com.my-namespace'
|
374
|
+
key_config field: :my_id
|
375
|
+
|
376
|
+
def consume_batch(payloads, metadata)
|
377
|
+
# payloads is an array of Avro-decoded hashes.
|
378
|
+
# metadata is a hash that contains information like :keys and :topic.
|
379
|
+
# Keys are automatically decoded and available as an array with
|
380
|
+
# the same cardinality as the payloads. If you need to iterate
|
381
|
+
# over payloads and keys together, you can use something like this:
|
382
|
+
|
383
|
+
payloads.zip(metadata[:keys]) do |_payload, _key|
|
384
|
+
# Do something
|
385
|
+
end
|
340
386
|
end
|
341
387
|
end
|
342
388
|
```
|
@@ -454,7 +500,7 @@ class Widget < ActiveRecord::Base
|
|
454
500
|
end
|
455
501
|
```
|
456
502
|
|
457
|
-
|
503
|
+
# Database Backend
|
458
504
|
|
459
505
|
Deimos provides a way to allow Kafka messages to be created inside a
|
460
506
|
database transaction, and send them asynchronously. This ensures that your
|
@@ -479,6 +525,7 @@ of immediately sending to Kafka. Now, you just need to call
|
|
479
525
|
|
480
526
|
Deimos.start_db_backend!
|
481
527
|
|
528
|
+
You can do this inside a thread or fork block.
|
482
529
|
If using Rails, you can use a Rake task to do this:
|
483
530
|
|
484
531
|
rails deimos:db_producer
|
@@ -495,6 +542,16 @@ If you want to force a message to send immediately, just call the `publish_list`
|
|
495
542
|
method with `force_send: true`. You can also pass `force_send` into any of the
|
496
543
|
other methods that publish events, like `send_event` in `ActiveRecordProducer`.
|
497
544
|
|
545
|
+
A couple of gotchas when using this feature:
|
546
|
+
* This may result in high throughput depending on your scale. If you're
|
547
|
+
using Rails < 5.1, you should add a migration to change the `id` column
|
548
|
+
to `BIGINT`. Rails >= 5.1 sets it to BIGINT by default.
|
549
|
+
* This table is high throughput but should generally be empty. Make sure
|
550
|
+
you optimize/vacuum this table regularly to reclaim the disk space.
|
551
|
+
* Currently, threads allow you to scale the *number* of topics but not
|
552
|
+
a single large topic with lots of messages. There is an [issue](https://github.com/flipp-oss/deimos/issues/23)
|
553
|
+
opened that would help with this case.
|
554
|
+
|
498
555
|
For more information on how the database backend works and why it was
|
499
556
|
implemented, please see [Database Backends](docs/DATABASE_BACKEND.md).
|
500
557
|
|
@@ -562,7 +619,17 @@ The following metrics are reported:
|
|
562
619
|
* `status:success`
|
563
620
|
* `status:error`
|
564
621
|
* `time:consume` (histogram)
|
622
|
+
* Amount of time spent executing handler for each message
|
623
|
+
* Batch Consumers - report counts by number of batches
|
624
|
+
* `status:batch_received`
|
625
|
+
* `status:batch_success`
|
626
|
+
* `status:batch_error`
|
627
|
+
* `time:consume_batch` (histogram)
|
628
|
+
* Amount of time spent executing handler for entire batch
|
565
629
|
* `time:time_delayed` (histogram)
|
630
|
+
* Indicates the amount of time between the `timestamp` property of each
|
631
|
+
payload (if present) and the time that the consumer started processing
|
632
|
+
the message.
|
566
633
|
* `publish` - a count of the number of messages received. Tagged
|
567
634
|
with `topic:{topic_name}`
|
568
635
|
* `publish_error` - a count of the number of messages which failed
|
@@ -655,6 +722,15 @@ test_consume_message(MyConsumer,
|
|
655
722
|
test_consume_invalid_message(MyConsumer,
|
656
723
|
{ 'some-invalid-payload' => 'some-value' })
|
657
724
|
|
725
|
+
# For batch consumers, there are similar methods such as:
|
726
|
+
test_consume_batch(MyBatchConsumer,
|
727
|
+
[{ 'some-payload' => 'some-value' },
|
728
|
+
{ 'some-payload' => 'some-other-value' }]) do |payloads, metadata|
|
729
|
+
# Expectations here
|
730
|
+
end
|
731
|
+
|
732
|
+
## Producing
|
733
|
+
|
658
734
|
# A matcher which allows you to test that a message was sent on the given
|
659
735
|
# topic, without having to know which class produced it.
|
660
736
|
expect(topic_name).to have_sent(payload, key=nil)
|
data/deimos-ruby.gemspec
CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.add_runtime_dependency('avro-patches', '~> 0.3')
|
22
22
|
spec.add_runtime_dependency('avro_turf', '~> 0.8')
|
23
|
-
spec.add_runtime_dependency('phobos', '~> 1.8')
|
23
|
+
spec.add_runtime_dependency('phobos', '~> 1.8.2.pre.beta2')
|
24
24
|
spec.add_runtime_dependency('ruby-kafka', '~> 0.7')
|
25
25
|
|
26
26
|
spec.add_development_dependency('activerecord', '~> 5.2')
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deimos
|
4
|
+
# Shared methods for Kafka Consumers
|
5
|
+
class BaseConsumer
|
6
|
+
include SharedConfig
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# @return [AvroDataEncoder]
|
10
|
+
def decoder
|
11
|
+
@decoder ||= AvroDataDecoder.new(schema: config[:schema],
|
12
|
+
namespace: config[:namespace])
|
13
|
+
end
|
14
|
+
|
15
|
+
# @return [AvroDataEncoder]
|
16
|
+
def key_decoder
|
17
|
+
@key_decoder ||= AvroDataDecoder.new(schema: config[:key_schema],
|
18
|
+
namespace: config[:namespace])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Helper method to decode an Avro-encoded key.
|
23
|
+
# @param key [String]
|
24
|
+
# @return [Object] the decoded key.
|
25
|
+
def decode_key(key)
|
26
|
+
return nil if key.nil?
|
27
|
+
|
28
|
+
config = self.class.config
|
29
|
+
if config[:encode_key] && config[:key_field].nil? &&
|
30
|
+
config[:key_schema].nil?
|
31
|
+
raise 'No key config given - if you are not decoding keys, please use '\
|
32
|
+
'`key_config plain: true`'
|
33
|
+
end
|
34
|
+
|
35
|
+
if config[:key_field]
|
36
|
+
self.class.decoder.decode_key(key, config[:key_field])
|
37
|
+
elsif config[:key_schema]
|
38
|
+
self.class.key_decoder.decode(key, schema: config[:key_schema])
|
39
|
+
else # no encoding
|
40
|
+
key
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
# @param payload [Hash|String]
|
47
|
+
# @param metadata [Hash]
|
48
|
+
def _with_error_span(payload, metadata)
|
49
|
+
@span = Deimos.config.tracer&.start(
|
50
|
+
'deimos-consumer',
|
51
|
+
resource: self.class.name.gsub('::', '-')
|
52
|
+
)
|
53
|
+
yield
|
54
|
+
rescue StandardError => e
|
55
|
+
_handle_error(e, payload, metadata)
|
56
|
+
ensure
|
57
|
+
Deimos.config.tracer&.finish(@span)
|
58
|
+
end
|
59
|
+
|
60
|
+
def _report_time_delayed(payload, metadata)
|
61
|
+
return if payload.nil? || payload['timestamp'].blank?
|
62
|
+
|
63
|
+
begin
|
64
|
+
time_delayed = Time.now.in_time_zone - payload['timestamp'].to_datetime
|
65
|
+
rescue ArgumentError
|
66
|
+
Deimos.config.logger.info(
|
67
|
+
message: "Error parsing timestamp! #{payload['timestamp']}"
|
68
|
+
)
|
69
|
+
return
|
70
|
+
end
|
71
|
+
Deimos.config.metrics&.histogram('handler', time_delayed, tags: %W(
|
72
|
+
time:time_delayed
|
73
|
+
topic:#{metadata[:topic]}
|
74
|
+
))
|
75
|
+
end
|
76
|
+
|
77
|
+
# @param exception [Throwable]
|
78
|
+
# @param _payload [Hash]
|
79
|
+
# @param _metadata [Hash]
|
80
|
+
def _handle_error(exception, _payload, _metadata)
|
81
|
+
Deimos.config.tracer&.set_error(@span, exception)
|
82
|
+
|
83
|
+
raise if Deimos.config.reraise_consumer_errors
|
84
|
+
end
|
85
|
+
|
86
|
+
# @param _time_taken [Float]
|
87
|
+
# @param _payload [Hash]
|
88
|
+
# @param _metadata [Hash]
|
89
|
+
def _handle_success(_time_taken, _payload, _metadata)
|
90
|
+
raise NotImplementedError
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'deimos/avro_data_decoder'
|
4
|
+
require 'deimos/base_consumer'
|
5
|
+
require 'phobos/batch_handler'
|
6
|
+
|
7
|
+
module Deimos
|
8
|
+
# Class to consume batches of messages in a topic
|
9
|
+
# Note: According to the docs, instances of your handler will be created
|
10
|
+
# for every incoming batch of messages. This class should be lightweight.
|
11
|
+
class BatchConsumer < BaseConsumer
|
12
|
+
include Phobos::BatchHandler
|
13
|
+
|
14
|
+
# :nodoc:
|
15
|
+
def around_consume_batch(payloads, metadata)
|
16
|
+
_received_batch(payloads, metadata)
|
17
|
+
benchmark = Benchmark.measure do
|
18
|
+
_with_error_span(payloads, metadata) { yield }
|
19
|
+
end
|
20
|
+
_handle_success(benchmark.real, payloads, metadata)
|
21
|
+
end
|
22
|
+
|
23
|
+
# :nodoc:
|
24
|
+
def before_consume_batch(batch, metadata)
|
25
|
+
_with_error_span(batch, metadata) do
|
26
|
+
metadata[:keys] = batch.map do |message|
|
27
|
+
decode_key(message.key)
|
28
|
+
end
|
29
|
+
|
30
|
+
batch.map do |message|
|
31
|
+
self.class.decoder.decode(message.payload) if message.payload.present?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Consume a batch of incoming messages.
|
37
|
+
# @param _payloads [Array<Phobos::BatchMessage>]
|
38
|
+
# @param _metadata [Hash]
|
39
|
+
def consume_batch(_payloads, _metadata)
|
40
|
+
raise NotImplementedError
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
def _received_batch(payloads, metadata)
|
46
|
+
Deimos.config.logger.info(
|
47
|
+
message: 'Got Kafka batch event',
|
48
|
+
message_ids: _payload_identifiers(payloads, metadata),
|
49
|
+
metadata: metadata.except(:keys)
|
50
|
+
)
|
51
|
+
Deimos.config.logger.debug(
|
52
|
+
message: 'Kafka batch event payloads',
|
53
|
+
payloads: payloads
|
54
|
+
)
|
55
|
+
Deimos.config.metrics&.increment(
|
56
|
+
'handler',
|
57
|
+
tags: %W(
|
58
|
+
status:batch_received
|
59
|
+
topic:#{metadata[:topic]}
|
60
|
+
))
|
61
|
+
Deimos.config.metrics&.increment(
|
62
|
+
'handler',
|
63
|
+
by: metadata['batch_size'],
|
64
|
+
tags: %W(
|
65
|
+
status:received
|
66
|
+
topic:#{metadata[:topic]}
|
67
|
+
))
|
68
|
+
if payloads.present?
|
69
|
+
payloads.each { |payload| _report_time_delayed(payload, metadata) }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# @param exception [Throwable]
|
74
|
+
# @param payloads [Array<Hash>]
|
75
|
+
# @param metadata [Hash]
|
76
|
+
def _handle_error(exception, payloads, metadata)
|
77
|
+
Deimos.config.metrics&.increment(
|
78
|
+
'handler',
|
79
|
+
tags: %W(
|
80
|
+
status:batch_error
|
81
|
+
topic:#{metadata[:topic]}
|
82
|
+
))
|
83
|
+
Deimos.config.logger.warn(
|
84
|
+
message: 'Error consuming message batch',
|
85
|
+
handler: self.class.name,
|
86
|
+
metadata: metadata.except(:keys),
|
87
|
+
message_ids: _payload_identifiers(payloads, metadata),
|
88
|
+
error_message: exception.message,
|
89
|
+
error: exception.backtrace
|
90
|
+
)
|
91
|
+
super
|
92
|
+
end
|
93
|
+
|
94
|
+
# @param time_taken [Float]
|
95
|
+
# @param payloads [Array<Hash>]
|
96
|
+
# @param metadata [Hash]
|
97
|
+
def _handle_success(time_taken, payloads, metadata)
|
98
|
+
Deimos.config.metrics&.histogram('handler', time_taken, tags: %W(
|
99
|
+
time:consume_batch
|
100
|
+
topic:#{metadata[:topic]}
|
101
|
+
))
|
102
|
+
Deimos.config.metrics&.increment(
|
103
|
+
'handler',
|
104
|
+
tags: %W(
|
105
|
+
status:batch_success
|
106
|
+
topic:#{metadata[:topic]}
|
107
|
+
))
|
108
|
+
Deimos.config.metrics&.increment(
|
109
|
+
'handler',
|
110
|
+
by: metadata['batch_size'],
|
111
|
+
tags: %W(
|
112
|
+
status:success
|
113
|
+
topic:#{metadata[:topic]}
|
114
|
+
))
|
115
|
+
Deimos.config.logger.info(
|
116
|
+
message: 'Finished processing Kafka batch event',
|
117
|
+
message_ids: _payload_identifiers(payloads, metadata),
|
118
|
+
time_elapsed: time_taken,
|
119
|
+
metadata: metadata.except(:keys)
|
120
|
+
)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Get payload identifiers (key and message_id if present) for logging.
|
124
|
+
# @param payloads [Array<Hash>]
|
125
|
+
# @param metadata [Hash]
|
126
|
+
# @return [Hash] the identifiers.
|
127
|
+
def _payload_identifiers(payloads, metadata)
|
128
|
+
message_ids = payloads&.map do |payload|
|
129
|
+
if payload.is_a?(Hash) && payload.key?('message_id')
|
130
|
+
payload['message_id']
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Payloads may be nil if preprocessing failed
|
135
|
+
messages = payloads || metadata[:keys] || []
|
136
|
+
|
137
|
+
messages.zip(metadata[:keys] || [], message_ids || []).map do |_, k, m_id|
|
138
|
+
ids = {}
|
139
|
+
|
140
|
+
ids[:key] = k if k.present?
|
141
|
+
ids[:message_id] = m_id if m_id.present?
|
142
|
+
|
143
|
+
ids
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
data/lib/deimos/consumer.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'deimos/avro_data_decoder'
|
4
|
+
require 'deimos/base_consumer'
|
4
5
|
require 'deimos/shared_config'
|
5
6
|
require 'phobos/handler'
|
6
7
|
require 'active_support/all'
|
@@ -11,23 +12,8 @@ require 'ddtrace'
|
|
11
12
|
# for every incoming message. This class should be lightweight.
|
12
13
|
module Deimos
|
13
14
|
# Parent consumer class.
|
14
|
-
class Consumer
|
15
|
+
class Consumer < BaseConsumer
|
15
16
|
include Phobos::Handler
|
16
|
-
include SharedConfig
|
17
|
-
|
18
|
-
class << self
|
19
|
-
# @return [AvroDataEncoder]
|
20
|
-
def decoder
|
21
|
-
@decoder ||= AvroDataDecoder.new(schema: config[:schema],
|
22
|
-
namespace: config[:namespace])
|
23
|
-
end
|
24
|
-
|
25
|
-
# @return [AvroDataEncoder]
|
26
|
-
def key_decoder
|
27
|
-
@key_decoder ||= AvroDataDecoder.new(schema: config[:key_schema],
|
28
|
-
namespace: config[:namespace])
|
29
|
-
end
|
30
|
-
end
|
31
17
|
|
32
18
|
# :nodoc:
|
33
19
|
def around_consume(payload, metadata)
|
@@ -41,34 +27,11 @@ module Deimos
|
|
41
27
|
# :nodoc:
|
42
28
|
def before_consume(payload, metadata)
|
43
29
|
_with_error_span(payload, metadata) do
|
44
|
-
|
45
|
-
metadata[:key] = decode_key(metadata[:key])
|
46
|
-
end
|
30
|
+
metadata[:key] = decode_key(metadata[:key])
|
47
31
|
self.class.decoder.decode(payload) if payload.present?
|
48
32
|
end
|
49
33
|
end
|
50
34
|
|
51
|
-
# Helper method to decode an Avro-encoded key.
|
52
|
-
# @param key [String]
|
53
|
-
# @return [Object] the decoded key.
|
54
|
-
def decode_key(key)
|
55
|
-
return nil if key.nil?
|
56
|
-
|
57
|
-
config = self.class.config
|
58
|
-
if config[:encode_key] && config[:key_field].nil? &&
|
59
|
-
config[:key_schema].nil?
|
60
|
-
raise 'No key config given - if you are not decoding keys, please use `key_config plain: true`'
|
61
|
-
end
|
62
|
-
|
63
|
-
if config[:key_field]
|
64
|
-
self.class.decoder.decode_key(key, config[:key_field])
|
65
|
-
elsif config[:key_schema]
|
66
|
-
self.class.key_decoder.decode(key, schema: config[:key_schema])
|
67
|
-
else # no encoding
|
68
|
-
key
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
35
|
# Consume incoming messages.
|
73
36
|
# @param _payload [String]
|
74
37
|
# @param _metadata [Hash]
|
@@ -78,20 +41,6 @@ module Deimos
|
|
78
41
|
|
79
42
|
private
|
80
43
|
|
81
|
-
# @param payload [Hash|String]
|
82
|
-
# @param metadata [Hash]
|
83
|
-
def _with_error_span(payload, metadata)
|
84
|
-
@span = Deimos.config.tracer&.start(
|
85
|
-
'deimos-consumer',
|
86
|
-
resource: self.class.name.gsub('::', '-')
|
87
|
-
)
|
88
|
-
yield
|
89
|
-
rescue StandardError => e
|
90
|
-
_handle_error(e, payload, metadata)
|
91
|
-
ensure
|
92
|
-
Deimos.config.tracer&.finish(@span)
|
93
|
-
end
|
94
|
-
|
95
44
|
def _received_message(payload, metadata)
|
96
45
|
Deimos.config.logger.info(
|
97
46
|
message: 'Got Kafka event',
|
@@ -102,13 +51,13 @@ module Deimos
|
|
102
51
|
status:received
|
103
52
|
topic:#{metadata[:topic]}
|
104
53
|
))
|
54
|
+
_report_time_delayed(payload, metadata)
|
105
55
|
end
|
106
56
|
|
107
57
|
# @param exception [Throwable]
|
108
58
|
# @param payload [Hash]
|
109
59
|
# @param metadata [Hash]
|
110
60
|
def _handle_error(exception, payload, metadata)
|
111
|
-
Deimos.config.tracer&.set_error(@span, exception)
|
112
61
|
Deimos.config.metrics&.increment(
|
113
62
|
'handler',
|
114
63
|
tags: %W(
|
@@ -124,7 +73,7 @@ module Deimos
|
|
124
73
|
error_message: exception.message,
|
125
74
|
error: exception.backtrace
|
126
75
|
)
|
127
|
-
|
76
|
+
super
|
128
77
|
end
|
129
78
|
|
130
79
|
# @param time_taken [Float]
|
@@ -145,20 +94,6 @@ module Deimos
|
|
145
94
|
time_elapsed: time_taken,
|
146
95
|
metadata: metadata
|
147
96
|
)
|
148
|
-
return if payload.nil? || payload['timestamp'].blank?
|
149
|
-
|
150
|
-
begin
|
151
|
-
time_delayed = Time.now.in_time_zone - payload['timestamp'].to_datetime
|
152
|
-
rescue ArgumentError
|
153
|
-
Deimos.config.logger.info(
|
154
|
-
message: "Error parsing timestamp! #{payload['timestamp']}"
|
155
|
-
)
|
156
|
-
return
|
157
|
-
end
|
158
|
-
Deimos.config.metrics&.histogram('handler', time_delayed, tags: %W(
|
159
|
-
time:time_delayed
|
160
|
-
topic:#{metadata[:topic]}
|
161
|
-
))
|
162
97
|
end
|
163
98
|
end
|
164
99
|
end
|
data/lib/deimos/test_helpers.rb
CHANGED
@@ -91,7 +91,7 @@ module Deimos
|
|
91
91
|
end
|
92
92
|
end
|
93
93
|
|
94
|
-
# Stub all already-loaded producers and consumers
|
94
|
+
# Stub all already-loaded producers and consumers for unit testing purposes.
|
95
95
|
def stub_producers_and_consumers!
|
96
96
|
Deimos::TestHelpers.sent_messages.clear
|
97
97
|
|
@@ -106,10 +106,17 @@ module Deimos
|
|
106
106
|
end
|
107
107
|
|
108
108
|
Deimos::Consumer.descendants.each do |klass|
|
109
|
+
# TODO: remove this when ActiveRecordConsumer uses batching
|
109
110
|
next if klass == Deimos::ActiveRecordConsumer # "abstract" class
|
110
111
|
|
111
112
|
stub_consumer(klass)
|
112
113
|
end
|
114
|
+
|
115
|
+
Deimos::BatchConsumer.descendants.each do |klass|
|
116
|
+
next if klass == Deimos::ActiveRecordConsumer # "abstract" class
|
117
|
+
|
118
|
+
stub_batch_consumer(klass)
|
119
|
+
end
|
113
120
|
end
|
114
121
|
|
115
122
|
# Stub a given producer class.
|
@@ -126,9 +133,7 @@ module Deimos
|
|
126
133
|
# Stub a given consumer class.
|
127
134
|
# @param klass [Class < Deimos::Consumer]
|
128
135
|
def stub_consumer(klass)
|
129
|
-
|
130
|
-
create_decoder(klass.config[:schema], klass.config[:namespace])
|
131
|
-
end
|
136
|
+
_stub_base_consumer(klass)
|
132
137
|
klass.class_eval do
|
133
138
|
alias_method(:old_consume, :consume) unless self.instance_methods.include?(:old_consume)
|
134
139
|
end
|
@@ -138,6 +143,12 @@ module Deimos
|
|
138
143
|
end
|
139
144
|
end
|
140
145
|
|
146
|
+
# Stub a given batch consumer class.
|
147
|
+
# @param klass [Class < Deimos::BatchConsumer]
|
148
|
+
def stub_batch_consumer(klass)
|
149
|
+
_stub_base_consumer(klass)
|
150
|
+
end
|
151
|
+
|
141
152
|
# get the difference of 2 hashes.
|
142
153
|
# @param hash1 [Hash]
|
143
154
|
# @param hash2 [Hash]
|
@@ -314,6 +325,102 @@ module Deimos
|
|
314
325
|
}.to raise_error(Avro::SchemaValidator::ValidationError)
|
315
326
|
end
|
316
327
|
|
328
|
+
# Test that a given handler will consume a given batch payload correctly,
|
329
|
+
# i.e. that the Avro schema is correct. If
|
330
|
+
# a block is given, that block will be executed when `consume` is called.
|
331
|
+
# Otherwise it will just confirm that `consume` is called at all.
|
332
|
+
# @param handler_class_or_topic [Class|String] Class which inherits from
|
333
|
+
# Deimos::Consumer or the topic as a string
|
334
|
+
# @param payloads [Array<Hash>] the payload to consume
|
335
|
+
def test_consume_batch(handler_class_or_topic,
|
336
|
+
payloads,
|
337
|
+
keys: [],
|
338
|
+
partition_keys: [],
|
339
|
+
call_original: false,
|
340
|
+
skip_expectation: false,
|
341
|
+
&block)
|
342
|
+
if call_original && block_given?
|
343
|
+
raise 'Cannot have both call_original and be given a block!'
|
344
|
+
end
|
345
|
+
|
346
|
+
topic_name = 'my-topic'
|
347
|
+
handler_class = if handler_class_or_topic.is_a?(String)
|
348
|
+
_get_handler_class_from_topic(handler_class_or_topic)
|
349
|
+
else
|
350
|
+
handler_class_or_topic
|
351
|
+
end
|
352
|
+
handler = handler_class.new
|
353
|
+
allow(handler_class).to receive(:new).and_return(handler)
|
354
|
+
listener = double('listener',
|
355
|
+
handler_class: handler_class,
|
356
|
+
encoding: nil)
|
357
|
+
batch_messages = payloads.zip(keys, partition_keys).map do |payload, key, partition_key|
|
358
|
+
key ||= _key_from_consumer(handler_class)
|
359
|
+
|
360
|
+
double('message',
|
361
|
+
'key' => key,
|
362
|
+
'partition_key' => partition_key,
|
363
|
+
'partition' => 1,
|
364
|
+
'offset' => 1,
|
365
|
+
'value' => payload)
|
366
|
+
end
|
367
|
+
batch = double('fetched_batch',
|
368
|
+
'messages' => batch_messages,
|
369
|
+
'topic' => topic_name,
|
370
|
+
'partition' => 1,
|
371
|
+
'offset_lag' => 0)
|
372
|
+
unless skip_expectation
|
373
|
+
expectation = expect(handler).to receive(:consume_batch).
|
374
|
+
with(payloads, anything, &block)
|
375
|
+
expectation.and_call_original if call_original
|
376
|
+
end
|
377
|
+
action = Phobos::Actions::ProcessBatchInline.new(
|
378
|
+
listener: listener,
|
379
|
+
batch: batch,
|
380
|
+
metadata: { topic: topic_name }
|
381
|
+
)
|
382
|
+
allow(action).to receive(:backoff_interval).and_return(0)
|
383
|
+
allow(action).to receive(:handle_error) { |e| raise e }
|
384
|
+
action.send(:execute)
|
385
|
+
end
|
386
|
+
|
387
|
+
# Check to see that a given message will fail due to Avro errors.
|
388
|
+
# @param handler_class [Class]
|
389
|
+
# @param payloads [Array<Hash>]
|
390
|
+
def test_consume_batch_invalid_message(handler_class, payloads)
|
391
|
+
topic_name = 'my-topic'
|
392
|
+
handler = handler_class.new
|
393
|
+
allow(handler_class).to receive(:new).and_return(handler)
|
394
|
+
listener = double('listener',
|
395
|
+
handler_class: handler_class,
|
396
|
+
encoding: nil)
|
397
|
+
batch_messages = payloads.map do |payload|
|
398
|
+
key ||= _key_from_consumer(handler_class)
|
399
|
+
|
400
|
+
double('message',
|
401
|
+
'key' => key,
|
402
|
+
'partition' => 1,
|
403
|
+
'offset' => 1,
|
404
|
+
'value' => payload)
|
405
|
+
end
|
406
|
+
batch = double('fetched_batch',
|
407
|
+
'messages' => batch_messages,
|
408
|
+
'topic' => topic_name,
|
409
|
+
'partition' => 1,
|
410
|
+
'offset_lag' => 0)
|
411
|
+
|
412
|
+
action = Phobos::Actions::ProcessBatchInline.new(
|
413
|
+
listener: listener,
|
414
|
+
batch: batch,
|
415
|
+
metadata: { topic: topic_name }
|
416
|
+
)
|
417
|
+
allow(action).to receive(:backoff_interval).and_return(0)
|
418
|
+
allow(action).to receive(:handle_error) { |e| raise e }
|
419
|
+
|
420
|
+
expect { action.send(:execute) }.
|
421
|
+
to raise_error(Avro::SchemaValidator::ValidationError)
|
422
|
+
end
|
423
|
+
|
317
424
|
# @param schema1 [String|Hash] a file path, JSON string, or
|
318
425
|
# hash representing a schema.
|
319
426
|
# @param schema2 [String|Hash] a file path, JSON string, or
|
@@ -352,5 +459,13 @@ module Deimos
|
|
352
459
|
|
353
460
|
handler.handler.constantize
|
354
461
|
end
|
462
|
+
|
463
|
+
# Stub shared methods between consumers/batch consumers
|
464
|
+
# @param [Class < Deimos::BaseConsumer] klass Consumer class to stub
|
465
|
+
def _stub_base_consumer(klass)
|
466
|
+
allow(klass).to receive(:decoder) do
|
467
|
+
create_decoder(klass.config[:schema], klass.config[:namespace])
|
468
|
+
end
|
469
|
+
end
|
355
470
|
end
|
356
471
|
end
|
data/lib/deimos/version.rb
CHANGED
data/lib/deimos.rb
CHANGED
@@ -10,6 +10,7 @@ require 'deimos/producer'
|
|
10
10
|
require 'deimos/active_record_producer'
|
11
11
|
require 'deimos/active_record_consumer'
|
12
12
|
require 'deimos/consumer'
|
13
|
+
require 'deimos/batch_consumer'
|
13
14
|
require 'deimos/configuration'
|
14
15
|
require 'deimos/instrumentation'
|
15
16
|
require 'deimos/utils/lag_reporter'
|
@@ -58,6 +59,8 @@ module Deimos
|
|
58
59
|
configure_loggers(phobos_config)
|
59
60
|
|
60
61
|
Phobos.configure(phobos_config)
|
62
|
+
|
63
|
+
validate_consumers
|
61
64
|
end
|
62
65
|
|
63
66
|
validate_db_backend if self.config.publish_backend == :db
|
@@ -78,7 +81,7 @@ module Deimos
|
|
78
81
|
# Start the DB producers to send Kafka messages.
|
79
82
|
# @param thread_count [Integer] the number of threads to start.
|
80
83
|
def start_db_backend!(thread_count: 1)
|
81
|
-
if self.config.publish_backend != :db
|
84
|
+
if self.config.publish_backend != :db
|
82
85
|
raise('Publish backend is not set to :db, exiting')
|
83
86
|
end
|
84
87
|
|
@@ -119,6 +122,28 @@ module Deimos
|
|
119
122
|
def ssl_var_contents(filename)
|
120
123
|
File.exist?(filename) ? File.read(filename) : filename
|
121
124
|
end
|
125
|
+
|
126
|
+
# Validate that consumers are configured correctly, including their
|
127
|
+
# delivery mode.
|
128
|
+
def validate_consumers
|
129
|
+
Phobos.config.listeners.each do |listener|
|
130
|
+
handler_class = listener.handler.constantize
|
131
|
+
delivery = listener.delivery
|
132
|
+
|
133
|
+
# Validate that Deimos consumers use proper delivery configs
|
134
|
+
if handler_class < Deimos::BatchConsumer
|
135
|
+
unless delivery == 'inline_batch'
|
136
|
+
raise "BatchConsumer #{listener.handler} must have delivery set to"\
|
137
|
+
' `inline_batch`'
|
138
|
+
end
|
139
|
+
elsif handler_class < Deimos::Consumer
|
140
|
+
if delivery.present? && !%w(message batch).include?(delivery)
|
141
|
+
raise "Non-batch Consumer #{listener.handler} must have delivery"\
|
142
|
+
' set to `message` or `batch`'
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
122
147
|
end
|
123
148
|
end
|
124
149
|
|
@@ -0,0 +1,247 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'deimos/batch_consumer'
|
4
|
+
|
5
|
+
# :nodoc:
|
6
|
+
module ConsumerTest
|
7
|
+
describe Deimos::BatchConsumer do
|
8
|
+
|
9
|
+
prepend_before(:each) do
|
10
|
+
# :nodoc:
|
11
|
+
consumer_class = Class.new(Deimos::BatchConsumer) do
|
12
|
+
schema 'MySchema'
|
13
|
+
namespace 'com.my-namespace'
|
14
|
+
key_config field: 'test_id'
|
15
|
+
|
16
|
+
# :nodoc:
|
17
|
+
def consume_batch(_payloads, _metadata)
|
18
|
+
raise 'This should not be called unless call_original is set'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
stub_const('ConsumerTest::MyBatchConsumer', consumer_class)
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:batch) do
|
25
|
+
[
|
26
|
+
{ 'test_id' => 'foo', 'some_int' => 123 },
|
27
|
+
{ 'test_id' => 'bar', 'some_int' => 456 }
|
28
|
+
]
|
29
|
+
end
|
30
|
+
|
31
|
+
let(:invalid_payloads) do
|
32
|
+
batch.concat([{ 'invalid' => 'key' }])
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should consume a batch of messages' do
|
36
|
+
test_consume_batch(MyBatchConsumer, batch) do |received, _metadata|
|
37
|
+
expect(received).to eq(batch)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should consume a message on a topic' do
|
42
|
+
test_consume_batch('my_batch_consume_topic', batch) do |received, _metadata|
|
43
|
+
expect(received).to eq(batch)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should fail on an invalid message in the batch' do
|
48
|
+
test_consume_batch_invalid_message(MyBatchConsumer, batch.concat(invalid_payloads))
|
49
|
+
end
|
50
|
+
|
51
|
+
describe 'when reraising errors is disabled' do
|
52
|
+
before(:each) do
|
53
|
+
Deimos.configure { |config| config.reraise_consumer_errors = false }
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should not fail when before_consume_batch fails' do
|
57
|
+
expect {
|
58
|
+
test_consume_batch(
|
59
|
+
MyBatchConsumer,
|
60
|
+
batch,
|
61
|
+
skip_expectation: true
|
62
|
+
) { raise 'OH NOES' }
|
63
|
+
}.not_to raise_error
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should not fail when consume_batch fails' do
|
67
|
+
expect {
|
68
|
+
test_consume_batch(
|
69
|
+
MyBatchConsumer,
|
70
|
+
invalid_payloads,
|
71
|
+
skip_expectation: true
|
72
|
+
)
|
73
|
+
}.not_to raise_error
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe 'decoding' do
|
78
|
+
let(:keys) do
|
79
|
+
batch.map { |v| v.slice('test_id') }
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should decode payloads for all messages in the batch' do
|
83
|
+
expect_any_instance_of(Deimos::AvroDataDecoder).
|
84
|
+
to receive(:decode).with(batch[0])
|
85
|
+
expect_any_instance_of(Deimos::AvroDataDecoder).
|
86
|
+
to receive(:decode).with(batch[1])
|
87
|
+
|
88
|
+
test_consume_batch('my_batch_consume_topic', batch) do |received, _metadata|
|
89
|
+
# Mock decoder simply returns the payload
|
90
|
+
expect(received).to eq(batch)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'should decode keys for all messages in the batch' do
|
95
|
+
expect_any_instance_of(ConsumerTest::MyBatchConsumer).
|
96
|
+
to receive(:decode_key).with(keys[0]).and_call_original
|
97
|
+
expect_any_instance_of(ConsumerTest::MyBatchConsumer).
|
98
|
+
to receive(:decode_key).with(keys[1]).and_call_original
|
99
|
+
|
100
|
+
test_consume_batch('my_batch_consume_topic', batch, keys: keys) do |_received, metadata|
|
101
|
+
# Mock decode_key extracts the value of the first field as the key
|
102
|
+
expect(metadata[:keys]).to eq(%w(foo bar))
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'should decode plain keys for all messages in the batch' do
|
107
|
+
consumer_class = Class.new(Deimos::BatchConsumer) do
|
108
|
+
schema 'MySchema'
|
109
|
+
namespace 'com.my-namespace'
|
110
|
+
key_config plain: true
|
111
|
+
end
|
112
|
+
stub_const('ConsumerTest::MyBatchConsumer', consumer_class)
|
113
|
+
stub_batch_consumer(consumer_class)
|
114
|
+
|
115
|
+
test_consume_batch('my_batch_consume_topic', batch, keys: [1, 2]) do |_received, metadata|
|
116
|
+
expect(metadata[:keys]).to eq([1, 2])
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe 'timestamps' do
|
122
|
+
before(:each) do
|
123
|
+
# :nodoc:
|
124
|
+
consumer_class = Class.new(Deimos::BatchConsumer) do
|
125
|
+
schema 'MySchemaWithDateTimes'
|
126
|
+
namespace 'com.my-namespace'
|
127
|
+
key_config plain: true
|
128
|
+
|
129
|
+
# :nodoc:
|
130
|
+
def consume_batch(_payloads, _metadata)
|
131
|
+
raise 'This should not be called unless call_original is set'
|
132
|
+
end
|
133
|
+
end
|
134
|
+
stub_const('ConsumerTest::MyBatchConsumer', consumer_class)
|
135
|
+
stub_batch_consumer(consumer_class)
|
136
|
+
allow(Deimos.config.metrics).to receive(:histogram)
|
137
|
+
end
|
138
|
+
|
139
|
+
let(:batch_with_time) do
|
140
|
+
[
|
141
|
+
{
|
142
|
+
'test_id' => 'foo',
|
143
|
+
'some_int' => 123,
|
144
|
+
'updated_at' => Time.now.to_i,
|
145
|
+
'timestamp' => 2.minutes.ago.to_s
|
146
|
+
},
|
147
|
+
{
|
148
|
+
'test_id' => 'bar',
|
149
|
+
'some_int' => 456,
|
150
|
+
'updated_at' => Time.now.to_i,
|
151
|
+
'timestamp' => 3.minutes.ago.to_s
|
152
|
+
}
|
153
|
+
]
|
154
|
+
end
|
155
|
+
|
156
|
+
let(:invalid_times) do
|
157
|
+
[
|
158
|
+
{
|
159
|
+
'test_id' => 'baz',
|
160
|
+
'some_int' => 123,
|
161
|
+
'updated_at' => Time.now.to_i,
|
162
|
+
'timestamp' => 'yesterday morning'
|
163
|
+
},
|
164
|
+
{
|
165
|
+
'test_id' => 'ok',
|
166
|
+
'some_int' => 456,
|
167
|
+
'updated_at' => Time.now.to_i,
|
168
|
+
'timestamp' => ''
|
169
|
+
},
|
170
|
+
{
|
171
|
+
'test_id' => 'hello',
|
172
|
+
'some_int' => 456,
|
173
|
+
'updated_at' => Time.now.to_i,
|
174
|
+
'timestamp' => '1234567890'
|
175
|
+
}
|
176
|
+
]
|
177
|
+
end
|
178
|
+
|
179
|
+
it 'should consume a batch' do
|
180
|
+
expect(Deimos.config.metrics).
|
181
|
+
to receive(:histogram).with('handler',
|
182
|
+
a_kind_of(Numeric),
|
183
|
+
tags: %w(time:time_delayed topic:my-topic)).twice
|
184
|
+
|
185
|
+
test_consume_batch('my_batch_consume_topic', batch_with_time) do |received, _metadata|
|
186
|
+
expect(received).to eq(batch_with_time)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'should fail nicely and ignore timestamps with the wrong format' do
|
191
|
+
batch = invalid_times.concat(batch_with_time)
|
192
|
+
|
193
|
+
expect(Deimos.config.metrics).
|
194
|
+
to receive(:histogram).with('handler',
|
195
|
+
a_kind_of(Numeric),
|
196
|
+
tags: %w(time:time_delayed topic:my-topic)).twice
|
197
|
+
|
198
|
+
test_consume_batch('my_batch_consume_topic', batch) do |received, _metadata|
|
199
|
+
expect(received).to eq(batch)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
describe 'logging' do
|
205
|
+
before(:each) do
|
206
|
+
# :nodoc:
|
207
|
+
consumer_class = Class.new(Deimos::BatchConsumer) do
|
208
|
+
schema 'MySchemaWithUniqueId'
|
209
|
+
namespace 'com.my-namespace'
|
210
|
+
key_config plain: true
|
211
|
+
|
212
|
+
# :nodoc:
|
213
|
+
def consume_batch(_payloads, _metadata)
|
214
|
+
raise 'This should not be called unless call_original is set'
|
215
|
+
end
|
216
|
+
end
|
217
|
+
stub_const('ConsumerTest::MyBatchConsumer', consumer_class)
|
218
|
+
stub_batch_consumer(consumer_class)
|
219
|
+
allow(Deimos.config.metrics).to receive(:histogram)
|
220
|
+
end
|
221
|
+
|
222
|
+
it 'should log message identifiers' do
|
223
|
+
batch_with_message_id = [
|
224
|
+
{ 'id' => 1, 'test_id' => 'foo', 'some_int' => 5,
|
225
|
+
'timestamp' => 3.minutes.ago.to_s, 'message_id' => 'one' },
|
226
|
+
{ 'id' => 2, 'test_id' => 'bar', 'some_int' => 6,
|
227
|
+
'timestamp' => 2.minutes.ago.to_s, 'message_id' => 'two' }
|
228
|
+
]
|
229
|
+
|
230
|
+
allow(Deimos.config.logger).
|
231
|
+
to receive(:info)
|
232
|
+
|
233
|
+
expect(Deimos.config.logger).
|
234
|
+
to receive(:info).
|
235
|
+
with(hash_including(
|
236
|
+
message_ids: [
|
237
|
+
{ key: 1, message_id: 'one' },
|
238
|
+
{ key: 2, message_id: 'two' }
|
239
|
+
]
|
240
|
+
)).
|
241
|
+
twice
|
242
|
+
|
243
|
+
test_consume_batch('my_batch_consume_topic', batch_with_message_id, keys: [1, 2])
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
data/spec/consumer_spec.rb
CHANGED
@@ -96,7 +96,7 @@ module ConsumerTest
|
|
96
96
|
expect { MyConsumer.new.decode_key(123) }.to raise_error(NoMethodError)
|
97
97
|
end
|
98
98
|
|
99
|
-
it 'should not
|
99
|
+
it 'should not decode if plain is set' do
|
100
100
|
consumer_class = Class.new(Deimos::Consumer) do
|
101
101
|
schema 'MySchema'
|
102
102
|
namespace 'com.my-namespace'
|
data/spec/deimos_spec.rb
CHANGED
@@ -2,58 +2,66 @@
|
|
2
2
|
|
3
3
|
describe Deimos do
|
4
4
|
|
5
|
+
let(:phobos_configuration) do
|
6
|
+
{ 'logger' =>
|
7
|
+
{ 'file' => 'log/phobos.log',
|
8
|
+
'stdout_json' => false,
|
9
|
+
'level' => 'debug',
|
10
|
+
'ruby_kafka' =>
|
11
|
+
{ 'level' => 'debug' } },
|
12
|
+
'kafka' =>
|
13
|
+
{ 'client_id' => 'phobos',
|
14
|
+
'connect_timeout' => 15,
|
15
|
+
'socket_timeout' => 15,
|
16
|
+
'seed_brokers' => 'my_seed_broker.com',
|
17
|
+
'ssl_ca_cert' => 'my_ssl_ca_cert',
|
18
|
+
'ssl_client_cert' => 'my_ssl_client_cert',
|
19
|
+
'ssl_client_cert_key' => 'my_ssl_client_cert_key' },
|
20
|
+
'producer' =>
|
21
|
+
{ 'ack_timeout' => 5,
|
22
|
+
'required_acks' => :all,
|
23
|
+
'max_retries' => 2,
|
24
|
+
'retry_backoff' => 1,
|
25
|
+
'max_buffer_size' => 10_000,
|
26
|
+
'max_buffer_bytesize' => 10_000_000,
|
27
|
+
'compression_codec' => nil,
|
28
|
+
'compression_threshold' => 1,
|
29
|
+
'max_queue_size' => 10_000,
|
30
|
+
'delivery_threshold' => 0,
|
31
|
+
'delivery_interval' => 0 },
|
32
|
+
'consumer' =>
|
33
|
+
{ 'session_timeout' => 300,
|
34
|
+
'offset_commit_interval' => 10,
|
35
|
+
'offset_commit_threshold' => 0,
|
36
|
+
'heartbeat_interval' => 10 },
|
37
|
+
'backoff' =>
|
38
|
+
{ 'min_ms' => 1000,
|
39
|
+
'max_ms' => 60_000 },
|
40
|
+
'listeners' => [
|
41
|
+
{ 'handler' => 'ConsumerTest::MyConsumer',
|
42
|
+
'topic' => 'my_consume_topic',
|
43
|
+
'group_id' => 'my_group_id',
|
44
|
+
'max_bytes_per_partition' => 524_288 },
|
45
|
+
{ 'handler' => 'ConsumerTest::MyBatchConsumer',
|
46
|
+
'topic' => 'my_batch_consume_topic',
|
47
|
+
'group_id' => 'my_batch_group_id',
|
48
|
+
'delivery' => 'inline_batch' }
|
49
|
+
],
|
50
|
+
'custom_logger' => nil,
|
51
|
+
'custom_kafka_logger' => nil }
|
52
|
+
end
|
53
|
+
|
54
|
+
let(:config_path) { File.join(File.dirname(__FILE__), 'phobos.yml') }
|
55
|
+
|
5
56
|
it 'should have a version number' do
|
6
57
|
expect(Deimos::VERSION).not_to be_nil
|
7
58
|
end
|
8
59
|
|
9
60
|
specify 'configure' do
|
10
|
-
phobos_configuration = { 'logger' =>
|
11
|
-
{ 'file' => 'log/phobos.log',
|
12
|
-
'stdout_json' => false,
|
13
|
-
'level' => 'debug',
|
14
|
-
'ruby_kafka' =>
|
15
|
-
{ 'level' => 'debug' } },
|
16
|
-
'kafka' =>
|
17
|
-
{ 'client_id' => 'phobos',
|
18
|
-
'connect_timeout' => 15,
|
19
|
-
'socket_timeout' => 15,
|
20
|
-
'seed_brokers' => 'my_seed_broker.com',
|
21
|
-
'ssl_ca_cert' => 'my_ssl_ca_cert',
|
22
|
-
'ssl_client_cert' => 'my_ssl_client_cert',
|
23
|
-
'ssl_client_cert_key' => 'my_ssl_client_cert_key' },
|
24
|
-
'producer' =>
|
25
|
-
{ 'ack_timeout' => 5,
|
26
|
-
'required_acks' => :all,
|
27
|
-
'max_retries' => 2,
|
28
|
-
'retry_backoff' => 1,
|
29
|
-
'max_buffer_size' => 10_000,
|
30
|
-
'max_buffer_bytesize' => 10_000_000,
|
31
|
-
'compression_codec' => nil,
|
32
|
-
'compression_threshold' => 1,
|
33
|
-
'max_queue_size' => 10_000,
|
34
|
-
'delivery_threshold' => 0,
|
35
|
-
'delivery_interval' => 0 },
|
36
|
-
'consumer' =>
|
37
|
-
{ 'session_timeout' => 300,
|
38
|
-
'offset_commit_interval' => 10,
|
39
|
-
'offset_commit_threshold' => 0,
|
40
|
-
'heartbeat_interval' => 10 },
|
41
|
-
'backoff' =>
|
42
|
-
{ 'min_ms' => 1000,
|
43
|
-
'max_ms' => 60_000 },
|
44
|
-
'listeners' => [
|
45
|
-
{ 'handler' => 'ConsumerTest::MyConsumer',
|
46
|
-
'topic' => 'my_consume_topic',
|
47
|
-
'group_id' => 'my_group_id',
|
48
|
-
'max_bytes_per_partition' => 524_288 }
|
49
|
-
],
|
50
|
-
'custom_logger' => nil,
|
51
|
-
'custom_kafka_logger' => nil }
|
52
|
-
|
53
61
|
expect(Phobos).to receive(:configure).with(phobos_configuration)
|
54
62
|
allow(described_class).to receive(:ssl_var_contents) { |key| key }
|
55
63
|
described_class.configure do |config|
|
56
|
-
config.phobos_config_file =
|
64
|
+
config.phobos_config_file = config_path
|
57
65
|
config.seed_broker = 'my_seed_broker.com'
|
58
66
|
config.ssl_enabled = true
|
59
67
|
config.ssl_ca_cert = 'my_ssl_ca_cert'
|
@@ -118,5 +126,61 @@ describe Deimos do
|
|
118
126
|
to raise_error('Thread count is not given or set to zero, exiting')
|
119
127
|
end
|
120
128
|
|
129
|
+
describe 'delivery configuration' do
|
130
|
+
before(:each) do
|
131
|
+
described_class.config = nil
|
132
|
+
allow(YAML).to receive(:load).and_return(phobos_configuration)
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'should not raise an error with properly configured handlers' do
|
136
|
+
# Add explicit consumers
|
137
|
+
phobos_configuration['listeners'] << { 'handler' => 'ConsumerTest::MyConsumer',
|
138
|
+
'delivery' => 'message' }
|
139
|
+
phobos_configuration['listeners'] << { 'handler' => 'ConsumerTest::MyConsumer',
|
140
|
+
'delivery' => 'batch' }
|
141
|
+
|
142
|
+
expect {
|
143
|
+
described_class.configure { |c| c.phobos_config_file = config_path }
|
144
|
+
}.not_to raise_error
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'should raise an error if BatchConsumers do not have inline_batch delivery' do
|
148
|
+
phobos_configuration['listeners'] = [{ 'handler' => 'ConsumerTest::MyBatchConsumer',
|
149
|
+
'delivery' => 'message' }]
|
150
|
+
|
151
|
+
expect {
|
152
|
+
described_class.configure { |c| c.phobos_config_file = config_path }
|
153
|
+
}.to raise_error('BatchConsumer ConsumerTest::MyBatchConsumer must have delivery set to `inline_batch`')
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'should raise an error if Consumers do not have message or batch delivery' do
|
157
|
+
phobos_configuration['listeners'] = [{ 'handler' => 'ConsumerTest::MyConsumer',
|
158
|
+
'delivery' => 'inline_batch' }]
|
159
|
+
|
160
|
+
expect {
|
161
|
+
described_class.configure { |c| c.phobos_config_file = config_path }
|
162
|
+
}.to raise_error('Non-batch Consumer ConsumerTest::MyConsumer must have delivery set to `message` or `batch`')
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'should treat nil as `batch`' do
|
166
|
+
phobos_configuration['listeners'] = [{ 'handler' => 'ConsumerTest::MyConsumer' }]
|
167
|
+
|
168
|
+
expect {
|
169
|
+
described_class.configure { |c| c.phobos_config_file = config_path }
|
170
|
+
}.not_to raise_error
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'should ignore non-Deimos listeners' do
|
174
|
+
consumer_class = Class.new { include Phobos::Handler }
|
175
|
+
stub_const('ConsumerTest::MyOtherConsumer', consumer_class)
|
176
|
+
phobos_configuration['listeners'] = [{ 'handler' => 'ConsumerTest::MyOtherConsumer',
|
177
|
+
'topic' => 'my_consume_topic',
|
178
|
+
'group_id' => 'my_group_id' }]
|
179
|
+
|
180
|
+
expect {
|
181
|
+
described_class.configure { |c| c.phobos_config_file = config_path }
|
182
|
+
}.not_to raise_error
|
183
|
+
end
|
184
|
+
end
|
121
185
|
end
|
122
186
|
end
|
data/spec/phobos.yml
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -8,6 +8,8 @@ require 'deimos/tracing/mock'
|
|
8
8
|
require 'deimos/test_helpers'
|
9
9
|
require 'active_support/testing/time_helpers'
|
10
10
|
require 'activerecord-import'
|
11
|
+
require 'handlers/my_batch_consumer'
|
12
|
+
require 'handlers/my_consumer'
|
11
13
|
|
12
14
|
# Helpers for Executor/DbProducer
|
13
15
|
module TestRunners
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: deimos-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.1.0.pre.beta1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Orner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-09-
|
11
|
+
date: 2019-09-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: avro-patches
|
@@ -44,14 +44,14 @@ dependencies:
|
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: 1.8.2.pre.beta2
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: 1.8.2.pre.beta2
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: ruby-kafka
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -328,6 +328,8 @@ files:
|
|
328
328
|
- lib/deimos/backends/db.rb
|
329
329
|
- lib/deimos/backends/kafka.rb
|
330
330
|
- lib/deimos/backends/kafka_async.rb
|
331
|
+
- lib/deimos/base_consumer.rb
|
332
|
+
- lib/deimos/batch_consumer.rb
|
331
333
|
- lib/deimos/configuration.rb
|
332
334
|
- lib/deimos/consumer.rb
|
333
335
|
- lib/deimos/instrumentation.rb
|
@@ -369,8 +371,11 @@ files:
|
|
369
371
|
- spec/backends/db_spec.rb
|
370
372
|
- spec/backends/kafka_async_spec.rb
|
371
373
|
- spec/backends/kafka_spec.rb
|
374
|
+
- spec/batch_consumer_spec.rb
|
372
375
|
- spec/consumer_spec.rb
|
373
376
|
- spec/deimos_spec.rb
|
377
|
+
- spec/handlers/my_batch_consumer.rb
|
378
|
+
- spec/handlers/my_consumer.rb
|
374
379
|
- spec/kafka_source_spec.rb
|
375
380
|
- spec/kafka_topic_info_spec.rb
|
376
381
|
- spec/phobos.bad_db.yml
|
@@ -411,9 +416,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
411
416
|
version: '0'
|
412
417
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
413
418
|
requirements:
|
414
|
-
- - "
|
419
|
+
- - ">"
|
415
420
|
- !ruby/object:Gem::Version
|
416
|
-
version:
|
421
|
+
version: 1.3.1
|
417
422
|
requirements: []
|
418
423
|
rubygems_version: 3.0.2
|
419
424
|
signing_key:
|
@@ -427,8 +432,11 @@ test_files:
|
|
427
432
|
- spec/backends/db_spec.rb
|
428
433
|
- spec/backends/kafka_async_spec.rb
|
429
434
|
- spec/backends/kafka_spec.rb
|
435
|
+
- spec/batch_consumer_spec.rb
|
430
436
|
- spec/consumer_spec.rb
|
431
437
|
- spec/deimos_spec.rb
|
438
|
+
- spec/handlers/my_batch_consumer.rb
|
439
|
+
- spec/handlers/my_consumer.rb
|
432
440
|
- spec/kafka_source_spec.rb
|
433
441
|
- spec/kafka_topic_info_spec.rb
|
434
442
|
- spec/phobos.bad_db.yml
|