deimos-kafka 1.0.0.pre.beta15
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +74 -0
- data/.gitignore +41 -0
- data/.gitmodules +0 -0
- data/.rspec +1 -0
- data/.rubocop.yml +321 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +9 -0
- data/CODE_OF_CONDUCT.md +77 -0
- data/Dockerfile +23 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +165 -0
- data/Guardfile +22 -0
- data/LICENSE.md +195 -0
- data/README.md +742 -0
- data/Rakefile +13 -0
- data/bin/deimos +4 -0
- data/deimos-kafka.gemspec +42 -0
- data/docker-compose.yml +71 -0
- data/docs/DATABASE_BACKEND.md +147 -0
- data/docs/PULL_REQUEST_TEMPLATE.md +34 -0
- data/lib/deimos.rb +134 -0
- data/lib/deimos/active_record_consumer.rb +81 -0
- data/lib/deimos/active_record_producer.rb +64 -0
- data/lib/deimos/avro_data_coder.rb +89 -0
- data/lib/deimos/avro_data_decoder.rb +36 -0
- data/lib/deimos/avro_data_encoder.rb +51 -0
- data/lib/deimos/backends/db.rb +27 -0
- data/lib/deimos/backends/kafka.rb +27 -0
- data/lib/deimos/backends/kafka_async.rb +27 -0
- data/lib/deimos/configuration.rb +88 -0
- data/lib/deimos/consumer.rb +164 -0
- data/lib/deimos/instrumentation.rb +71 -0
- data/lib/deimos/kafka_message.rb +27 -0
- data/lib/deimos/kafka_source.rb +126 -0
- data/lib/deimos/kafka_topic_info.rb +79 -0
- data/lib/deimos/message.rb +74 -0
- data/lib/deimos/metrics/datadog.rb +47 -0
- data/lib/deimos/metrics/mock.rb +39 -0
- data/lib/deimos/metrics/provider.rb +38 -0
- data/lib/deimos/monkey_patches/phobos_cli.rb +35 -0
- data/lib/deimos/monkey_patches/phobos_producer.rb +51 -0
- data/lib/deimos/monkey_patches/ruby_kafka_heartbeat.rb +85 -0
- data/lib/deimos/monkey_patches/schema_store.rb +19 -0
- data/lib/deimos/producer.rb +218 -0
- data/lib/deimos/publish_backend.rb +30 -0
- data/lib/deimos/railtie.rb +8 -0
- data/lib/deimos/schema_coercer.rb +108 -0
- data/lib/deimos/shared_config.rb +59 -0
- data/lib/deimos/test_helpers.rb +356 -0
- data/lib/deimos/tracing/datadog.rb +35 -0
- data/lib/deimos/tracing/mock.rb +40 -0
- data/lib/deimos/tracing/provider.rb +31 -0
- data/lib/deimos/utils/db_producer.rb +95 -0
- data/lib/deimos/utils/executor.rb +117 -0
- data/lib/deimos/utils/inline_consumer.rb +144 -0
- data/lib/deimos/utils/lag_reporter.rb +182 -0
- data/lib/deimos/utils/platform_schema_validation.rb +0 -0
- data/lib/deimos/utils/signal_handler.rb +68 -0
- data/lib/deimos/version.rb +5 -0
- data/lib/generators/deimos/db_backend/templates/migration +24 -0
- data/lib/generators/deimos/db_backend/templates/rails3_migration +30 -0
- data/lib/generators/deimos/db_backend_generator.rb +48 -0
- data/lib/tasks/deimos.rake +17 -0
- data/spec/active_record_consumer_spec.rb +81 -0
- data/spec/active_record_producer_spec.rb +107 -0
- data/spec/avro_data_decoder_spec.rb +18 -0
- data/spec/avro_data_encoder_spec.rb +37 -0
- data/spec/backends/db_spec.rb +35 -0
- data/spec/backends/kafka_async_spec.rb +11 -0
- data/spec/backends/kafka_spec.rb +11 -0
- data/spec/consumer_spec.rb +169 -0
- data/spec/deimos_spec.rb +117 -0
- data/spec/kafka_source_spec.rb +168 -0
- data/spec/kafka_topic_info_spec.rb +88 -0
- data/spec/phobos.bad_db.yml +73 -0
- data/spec/phobos.yml +73 -0
- data/spec/producer_spec.rb +397 -0
- data/spec/publish_backend_spec.rb +10 -0
- data/spec/schemas/com/my-namespace/MySchema-key.avsc +13 -0
- data/spec/schemas/com/my-namespace/MySchema.avsc +18 -0
- data/spec/schemas/com/my-namespace/MySchemaWithBooleans.avsc +18 -0
- data/spec/schemas/com/my-namespace/MySchemaWithDateTimes.avsc +33 -0
- data/spec/schemas/com/my-namespace/MySchemaWithId.avsc +28 -0
- data/spec/schemas/com/my-namespace/MySchemaWithUniqueId.avsc +32 -0
- data/spec/schemas/com/my-namespace/Widget.avsc +27 -0
- data/spec/schemas/com/my-namespace/WidgetTheSecond.avsc +27 -0
- data/spec/spec_helper.rb +207 -0
- data/spec/updateable_schema_store_spec.rb +36 -0
- data/spec/utils/db_producer_spec.rb +208 -0
- data/spec/utils/executor_spec.rb +42 -0
- data/spec/utils/lag_reporter_spec.rb +69 -0
- data/spec/utils/platform_schema_validation_spec.rb +0 -0
- data/spec/utils/signal_handler_spec.rb +16 -0
- data/support/deimos-solo.png +0 -0
- data/support/deimos-with-name-next.png +0 -0
- data/support/deimos-with-name.png +0 -0
- data/support/flipp-logo.png +0 -0
- metadata +452 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deimos
|
4
|
+
# Abstract class for all publish backends.
|
5
|
+
class PublishBackend
|
6
|
+
class << self
|
7
|
+
# @param producer_class [Class < Deimos::Producer]
|
8
|
+
# @param messages [Array<Deimos::Message>]
|
9
|
+
def publish(producer_class:, messages:)
|
10
|
+
Deimos.config.logger.info(
|
11
|
+
message: 'Publishing messages',
|
12
|
+
topic: producer_class.topic,
|
13
|
+
payloads: messages.map do |message|
|
14
|
+
{
|
15
|
+
payload: message.payload,
|
16
|
+
key: message.key
|
17
|
+
}
|
18
|
+
end
|
19
|
+
)
|
20
|
+
execute(producer_class: producer_class, messages: messages)
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param producer_class [Class < Deimos::Producer]
|
24
|
+
# @param messages [Array<Deimos::Message>]
|
25
|
+
def execute(producer_class:, messages:)
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deimos
|
4
|
+
# Class to coerce values in a payload to match a schema.
|
5
|
+
class SchemaCoercer
|
6
|
+
# @param schema [Avro::Schema]
|
7
|
+
def initialize(schema)
|
8
|
+
@schema = schema
|
9
|
+
end
|
10
|
+
|
11
|
+
# @param payload [Hash]
|
12
|
+
# @return [HashWithIndifferentAccess]
|
13
|
+
def coerce(payload)
|
14
|
+
result = {}
|
15
|
+
@schema.fields.each do |field|
|
16
|
+
name = field.name
|
17
|
+
next unless payload.key?(name)
|
18
|
+
|
19
|
+
val = payload[name]
|
20
|
+
result[name] = _coerce_type(field.type, val)
|
21
|
+
end
|
22
|
+
result.with_indifferent_access
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# @param val [String]
|
28
|
+
# @return [Boolean]
|
29
|
+
def _is_integer_string?(val)
|
30
|
+
return false unless val.is_a?(String)
|
31
|
+
|
32
|
+
begin
|
33
|
+
true if Integer(val)
|
34
|
+
rescue StandardError
|
35
|
+
false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param val [String]
|
40
|
+
# @return [Boolean]
|
41
|
+
def _is_float_string?(val)
|
42
|
+
return false unless val.is_a?(String)
|
43
|
+
|
44
|
+
begin
|
45
|
+
true if Float(val)
|
46
|
+
rescue StandardError
|
47
|
+
false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# @param val [Object]
|
52
|
+
# @return [Boolean]
|
53
|
+
def _is_to_s_defined?(val)
|
54
|
+
return false if val.nil?
|
55
|
+
|
56
|
+
Object.instance_method(:to_s).bind(val).call != val.to_s
|
57
|
+
end
|
58
|
+
|
59
|
+
# @param type [Symbol]
|
60
|
+
# @param val [Object]
|
61
|
+
# @return [Object]
|
62
|
+
def _coerce_type(type, val)
|
63
|
+
int_classes = [Time, DateTime, ActiveSupport::TimeWithZone]
|
64
|
+
field_type = type.type.to_sym
|
65
|
+
if field_type == :union
|
66
|
+
union_types = type.schemas.map { |s| s.type.to_sym }
|
67
|
+
return nil if val.nil? && union_types.include?(:null)
|
68
|
+
|
69
|
+
field_type = union_types.find { |t| t != :null }
|
70
|
+
end
|
71
|
+
|
72
|
+
case field_type
|
73
|
+
when :int, :long
|
74
|
+
if val.is_a?(Integer) ||
|
75
|
+
_is_integer_string?(val) ||
|
76
|
+
int_classes.any? { |klass| val.is_a?(klass) }
|
77
|
+
val.to_i
|
78
|
+
else
|
79
|
+
val # this will fail
|
80
|
+
end
|
81
|
+
|
82
|
+
when :float, :double
|
83
|
+
if val.is_a?(Numeric) || _is_float_string?(val)
|
84
|
+
val.to_f
|
85
|
+
else
|
86
|
+
val # this will fail
|
87
|
+
end
|
88
|
+
|
89
|
+
when :string
|
90
|
+
if val.respond_to?(:to_str)
|
91
|
+
val.to_s
|
92
|
+
elsif _is_to_s_defined?(val)
|
93
|
+
val.to_s
|
94
|
+
else
|
95
|
+
val # this will fail
|
96
|
+
end
|
97
|
+
when :boolean
|
98
|
+
if val.nil? || val == false
|
99
|
+
false
|
100
|
+
else
|
101
|
+
true
|
102
|
+
end
|
103
|
+
else
|
104
|
+
val
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
|
5
|
+
module Deimos
|
6
|
+
# Module that producers and consumers can share which sets up configuration.
|
7
|
+
module SharedConfig
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
# need to use this instead of class_methods to be backwards-compatible
|
11
|
+
# with Rails 3
|
12
|
+
module ClassMethods
|
13
|
+
# @return [Hash]
|
14
|
+
def config
|
15
|
+
return @config if @config
|
16
|
+
|
17
|
+
@config = {
|
18
|
+
encode_key: true
|
19
|
+
}
|
20
|
+
klass = self.superclass
|
21
|
+
while klass.respond_to?(:config)
|
22
|
+
klass_config = klass.config
|
23
|
+
if klass_config
|
24
|
+
# default is true for this so don't include it in the merge
|
25
|
+
klass_config.delete(:encode_key) if klass_config[:encode_key]
|
26
|
+
@config.merge!(klass_config) if klass.config
|
27
|
+
end
|
28
|
+
klass = klass.superclass
|
29
|
+
end
|
30
|
+
@config
|
31
|
+
end
|
32
|
+
|
33
|
+
# Set the schema.
|
34
|
+
# @param schema [String]
|
35
|
+
def schema(schema)
|
36
|
+
config[:schema] = schema
|
37
|
+
end
|
38
|
+
|
39
|
+
# Set the namespace.
|
40
|
+
# @param namespace [String]
|
41
|
+
def namespace(namespace)
|
42
|
+
config[:namespace] = namespace
|
43
|
+
end
|
44
|
+
|
45
|
+
# Set key configuration.
|
46
|
+
# @param field [Symbol] the name of a field to use in the value schema as
|
47
|
+
# a generated key schema
|
48
|
+
# @param schema [String|Symbol] the name of a schema to use for the key
|
49
|
+
# @param plain [Boolean] if true, do not encode keys at all
|
50
|
+
# @param none [Boolean] if true, do not use keys at all
|
51
|
+
def key_config(plain: nil, field: nil, schema: nil, none: nil)
|
52
|
+
config[:no_keys] = none
|
53
|
+
config[:encode_key] = !plain && !none
|
54
|
+
config[:key_field] = field&.to_s
|
55
|
+
config[:key_schema] = schema
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,356 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
require 'active_support/core_ext'
|
5
|
+
require 'avro_turf'
|
6
|
+
require 'deimos/tracing/mock'
|
7
|
+
require 'deimos/metrics/mock'
|
8
|
+
|
9
|
+
module Deimos
|
10
|
+
# Include this module in your RSpec spec_helper
|
11
|
+
# to stub out external dependencies
|
12
|
+
# and add methods to use to test encoding/decoding.
|
13
|
+
module TestHelpers
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
class << self
|
17
|
+
# @return [Array<Hash>]
|
18
|
+
def sent_messages
|
19
|
+
@sent_messages ||= []
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
included do
|
24
|
+
# @param encoder_schema [String]
|
25
|
+
# @param namespace [String]
|
26
|
+
# @return [Deimos::AvroDataEncoder]
|
27
|
+
def create_encoder(encoder_schema, namespace)
|
28
|
+
encoder = Deimos::AvroDataEncoder.new(schema: encoder_schema,
|
29
|
+
namespace: namespace)
|
30
|
+
|
31
|
+
# we added and_wrap_original to RSpec 2 but the regular block
|
32
|
+
# syntax wasn't working for some reason - block wasn't being passed
|
33
|
+
# to the method
|
34
|
+
block = proc do |m, *args|
|
35
|
+
m.call(*args)
|
36
|
+
args[0]
|
37
|
+
end
|
38
|
+
allow(encoder).to receive(:encode_local).and_wrap_original(&block)
|
39
|
+
allow(encoder).to receive(:encode) do |payload, schema: nil, topic: nil|
|
40
|
+
encoder.encode_local(payload, schema: schema)
|
41
|
+
end
|
42
|
+
|
43
|
+
block = proc do |m, *args|
|
44
|
+
m.call(*args)&.values&.first
|
45
|
+
end
|
46
|
+
allow(encoder).to receive(:encode_key).and_wrap_original(&block)
|
47
|
+
encoder
|
48
|
+
end
|
49
|
+
|
50
|
+
# @param decoder_schema [String]
|
51
|
+
# @param namespace [String]
|
52
|
+
# @return [Deimos::AvroDataDecoder]
|
53
|
+
def create_decoder(decoder_schema, namespace)
|
54
|
+
decoder = Deimos::AvroDataDecoder.new(schema: decoder_schema,
|
55
|
+
namespace: namespace)
|
56
|
+
allow(decoder).to receive(:decode_local) { |payload| payload }
|
57
|
+
allow(decoder).to receive(:decode) do |payload, schema: nil|
|
58
|
+
schema ||= decoder.schema
|
59
|
+
if schema && decoder.namespace
|
60
|
+
# Validate against local schema.
|
61
|
+
encoder = Deimos::AvroDataEncoder.new(schema: schema,
|
62
|
+
namespace: decoder.namespace)
|
63
|
+
encoder.schema_store = decoder.schema_store
|
64
|
+
encoder.encode_local(payload)
|
65
|
+
end
|
66
|
+
payload
|
67
|
+
end
|
68
|
+
allow(decoder).to receive(:decode_key) do |payload, _key_id|
|
69
|
+
payload.values.first
|
70
|
+
end
|
71
|
+
decoder
|
72
|
+
end
|
73
|
+
|
74
|
+
RSpec.configure do |config|
|
75
|
+
|
76
|
+
config.before(:suite) do
|
77
|
+
Deimos.configure do |fr_config|
|
78
|
+
fr_config.logger = Logger.new(STDOUT)
|
79
|
+
fr_config.seed_broker ||= 'test_broker'
|
80
|
+
fr_config.tracer = Deimos::Tracing::Mock.new
|
81
|
+
fr_config.metrics = Deimos::Metrics::Mock.new
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
before(:each) do
|
87
|
+
client = double('client').as_null_object
|
88
|
+
allow(client).to receive(:time) do |*_args, &block|
|
89
|
+
block.call
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Stub all already-loaded producers and consumers fir unit testing purposes.
|
95
|
+
def stub_producers_and_consumers!
|
96
|
+
Deimos::TestHelpers.sent_messages.clear
|
97
|
+
|
98
|
+
allow(Deimos::Producer).to receive(:produce_batch) do |_, batch|
|
99
|
+
Deimos::TestHelpers.sent_messages.concat(batch.map(&:to_h))
|
100
|
+
end
|
101
|
+
|
102
|
+
Deimos::Producer.descendants.each do |klass|
|
103
|
+
next if klass == Deimos::ActiveRecordProducer # "abstract" class
|
104
|
+
|
105
|
+
stub_producer(klass)
|
106
|
+
end
|
107
|
+
|
108
|
+
Deimos::Consumer.descendants.each do |klass|
|
109
|
+
next if klass == Deimos::ActiveRecordConsumer # "abstract" class
|
110
|
+
|
111
|
+
stub_consumer(klass)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Stub a given producer class.
|
116
|
+
# @param klass [Class < Deimos::Producer]
|
117
|
+
def stub_producer(klass)
|
118
|
+
allow(klass).to receive(:encoder) do
|
119
|
+
create_encoder(klass.config[:schema], klass.config[:namespace])
|
120
|
+
end
|
121
|
+
allow(klass).to receive(:key_encoder) do
|
122
|
+
create_encoder(klass.config[:key_schema], klass.config[:namespace])
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Stub a given consumer class.
|
127
|
+
# @param klass [Class < Deimos::Consumer]
|
128
|
+
def stub_consumer(klass)
|
129
|
+
allow(klass).to receive(:decoder) do
|
130
|
+
create_decoder(klass.config[:schema], klass.config[:namespace])
|
131
|
+
end
|
132
|
+
klass.class_eval do
|
133
|
+
alias_method(:old_consume, :consume) unless self.instance_methods.include?(:old_consume)
|
134
|
+
end
|
135
|
+
allow_any_instance_of(klass).to receive(:consume) do |instance, payload, metadata|
|
136
|
+
metadata[:key] = klass.new.decode_key(metadata[:key])
|
137
|
+
instance.old_consume(payload, metadata)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# get the difference of 2 hashes.
|
142
|
+
# @param hash1 [Hash]
|
143
|
+
# @param hash2 [Hash]
|
144
|
+
def _hash_diff(hash1, hash2)
|
145
|
+
if hash1.nil? || !hash1.is_a?(Hash)
|
146
|
+
hash2
|
147
|
+
elsif hash2.nil? || !hash2.is_a?(Hash)
|
148
|
+
hash1
|
149
|
+
else
|
150
|
+
hash1.dup.
|
151
|
+
delete_if { |k, v| hash2[k] == v }.
|
152
|
+
merge!(hash2.dup.delete_if { |k, _v| hash1.key?(k) })
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# :nodoc:
|
157
|
+
def _frk_failure_message(topic, message, key=nil, partition_key=nil, was_negated=false)
|
158
|
+
messages = Deimos::TestHelpers.sent_messages.
|
159
|
+
select { |m| m[:topic] == topic }.
|
160
|
+
map { |m| m.except(:topic) }
|
161
|
+
message_string = ''
|
162
|
+
diff = nil
|
163
|
+
min_hash_diff = nil
|
164
|
+
if messages.any?
|
165
|
+
message_string = messages.map(&:inspect).join("\n")
|
166
|
+
min_hash_diff = messages.min_by { |m| _hash_diff(m, message).keys.size }
|
167
|
+
diff = RSpec::Expectations.differ.
|
168
|
+
diff_as_object(message, min_hash_diff[:payload])
|
169
|
+
end
|
170
|
+
description = if message.respond_to?(:description)
|
171
|
+
message.description
|
172
|
+
elsif message.nil?
|
173
|
+
'nil'
|
174
|
+
else
|
175
|
+
message
|
176
|
+
end
|
177
|
+
str = "Expected #{topic} #{'not ' if was_negated}to have sent #{description}"
|
178
|
+
str += " with key #{key}" if key
|
179
|
+
str += " with partition key #{partition_key}" if partition_key
|
180
|
+
str += "\nClosest message received: #{min_hash_diff}" if min_hash_diff
|
181
|
+
str += "\nDiff: #{diff}" if diff
|
182
|
+
str + "\nAll Messages received:\n#{message_string}"
|
183
|
+
end
|
184
|
+
|
185
|
+
RSpec::Matchers.define :have_sent do |msg, key=nil, partition_key=nil|
|
186
|
+
message = if msg.respond_to?(:with_indifferent_access)
|
187
|
+
msg.with_indifferent_access
|
188
|
+
else
|
189
|
+
msg
|
190
|
+
end
|
191
|
+
match do |topic|
|
192
|
+
Deimos::TestHelpers.sent_messages.any? do |m|
|
193
|
+
hash_matcher = RSpec::Matchers::BuiltIn::Match.new(message)
|
194
|
+
hash_matcher.send(:match,
|
195
|
+
message,
|
196
|
+
m[:payload]&.with_indifferent_access) &&
|
197
|
+
topic == m[:topic] &&
|
198
|
+
(key.present? ? key == m[:key] : true) &&
|
199
|
+
(partition_key.present? ? partition_key == m[:partition_key] : true)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
if respond_to?(:failure_message)
|
204
|
+
failure_message do |topic|
|
205
|
+
_frk_failure_message(topic, message, key, partition_key)
|
206
|
+
end
|
207
|
+
failure_message_when_negated do |topic|
|
208
|
+
_frk_failure_message(topic, message, key, partition_key, true)
|
209
|
+
end
|
210
|
+
else
|
211
|
+
failure_message_for_should do |topic|
|
212
|
+
_frk_failure_message(topic, message, key, partition_key)
|
213
|
+
end
|
214
|
+
failure_message_for_should_not do |topic|
|
215
|
+
_frk_failure_message(topic, message, key, partition_key, true)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Clear all sent messages - e.g. if we want to check that
|
221
|
+
# particular messages were sent or not sent after a point in time.
|
222
|
+
def clear_kafka_messages!
|
223
|
+
Deimos::TestHelpers.sent_messages.clear
|
224
|
+
end
|
225
|
+
|
226
|
+
# test that a message was sent on the given topic.
|
227
|
+
# DEPRECATED - use the "have_sent" matcher instead.
|
228
|
+
# @param message [Hash]
|
229
|
+
# @param topic [String]
|
230
|
+
# @param key [String|Integer]
|
231
|
+
# @return [Boolean]
|
232
|
+
def was_message_sent?(message, topic, key=nil)
|
233
|
+
Deimos::TestHelpers.sent_messages.any? do |m|
|
234
|
+
message == m[:payload] && m[:topic] == topic &&
|
235
|
+
(key.present? ? m[:key] == key : true)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# Test that a given handler will consume a given payload correctly, i.e.
|
240
|
+
# that the Avro schema is correct. If
|
241
|
+
# a block is given, that block will be executed when `consume` is called.
|
242
|
+
# Otherwise it will just confirm that `consume` is called at all.
|
243
|
+
# @param handler_class_or_topic [Class|String] Class which inherits from
|
244
|
+
# Deimos::Consumer or the topic as a string
|
245
|
+
# @param payload [Hash] the payload to consume
|
246
|
+
# @param call_original [Boolean] if true, allow the consume handler
|
247
|
+
# to continue as normal. Not compatible with a block.
|
248
|
+
# @param ignore_expectation [Boolean] Set to true to not place any
|
249
|
+
# expectations on the consumer. Primarily used internally to Deimos.
|
250
|
+
# @param key [Object] the key to use.
|
251
|
+
# @param partition_key [Object] the partition key to use.
|
252
|
+
def test_consume_message(handler_class_or_topic,
|
253
|
+
payload,
|
254
|
+
call_original: false,
|
255
|
+
key: nil,
|
256
|
+
partition_key: nil,
|
257
|
+
skip_expectation: false,
|
258
|
+
&block)
|
259
|
+
raise 'Cannot have both call_original and be given a block!' if call_original && block_given?
|
260
|
+
|
261
|
+
payload&.stringify_keys!
|
262
|
+
handler_class = if handler_class_or_topic.is_a?(String)
|
263
|
+
_get_handler_class_from_topic(handler_class_or_topic)
|
264
|
+
else
|
265
|
+
handler_class_or_topic
|
266
|
+
end
|
267
|
+
handler = handler_class.new
|
268
|
+
allow(handler_class).to receive(:new).and_return(handler)
|
269
|
+
listener = double('listener',
|
270
|
+
handler_class: handler_class,
|
271
|
+
encoding: nil)
|
272
|
+
key ||= _key_from_consumer(handler_class)
|
273
|
+
message = double('message',
|
274
|
+
'key' => key,
|
275
|
+
'partition_key' => partition_key,
|
276
|
+
'partition' => 1,
|
277
|
+
'offset' => 1,
|
278
|
+
'value' => payload)
|
279
|
+
|
280
|
+
unless skip_expectation
|
281
|
+
expectation = expect(handler).to receive(:consume).
|
282
|
+
with(payload, anything, &block)
|
283
|
+
expectation.and_call_original if call_original
|
284
|
+
end
|
285
|
+
Phobos::Actions::ProcessMessage.new(
|
286
|
+
listener: listener,
|
287
|
+
message: message,
|
288
|
+
listener_metadata: { topic: 'my-topic' }
|
289
|
+
).send(:process_message, payload)
|
290
|
+
end
|
291
|
+
|
292
|
+
# Check to see that a given message will fail due to Avro errors.
|
293
|
+
# @param handler_class [Class]
|
294
|
+
# @param payload [Hash]
|
295
|
+
def test_consume_invalid_message(handler_class, payload)
|
296
|
+
handler = handler_class.new
|
297
|
+
allow(handler_class).to receive(:new).and_return(handler)
|
298
|
+
listener = double('listener',
|
299
|
+
handler_class: handler_class,
|
300
|
+
encoding: nil)
|
301
|
+
message = double('message',
|
302
|
+
key: _key_from_consumer(handler_class),
|
303
|
+
partition_key: nil,
|
304
|
+
partition: 1,
|
305
|
+
offset: 1,
|
306
|
+
value: payload)
|
307
|
+
|
308
|
+
expect {
|
309
|
+
Phobos::Actions::ProcessMessage.new(
|
310
|
+
listener: listener,
|
311
|
+
message: message,
|
312
|
+
listener_metadata: { topic: 'my-topic' }
|
313
|
+
).send(:process_message, payload)
|
314
|
+
}.to raise_error(Avro::SchemaValidator::ValidationError)
|
315
|
+
end
|
316
|
+
|
317
|
+
# @param schema1 [String|Hash] a file path, JSON string, or
|
318
|
+
# hash representing a schema.
|
319
|
+
# @param schema2 [String|Hash] a file path, JSON string, or
|
320
|
+
# hash representing a schema.
|
321
|
+
# @return [Boolean] true if the schemas are compatible, false otherwise.
|
322
|
+
def self.schemas_compatible?(schema1, schema2)
|
323
|
+
json1, json2 = [schema1, schema2].map do |schema|
|
324
|
+
if schema.is_a?(String)
|
325
|
+
schema = File.read(schema) unless schema.strip.starts_with?('{') # file path
|
326
|
+
MultiJson.load(schema)
|
327
|
+
else
|
328
|
+
schema
|
329
|
+
end
|
330
|
+
end
|
331
|
+
avro_schema1 = Avro::Schema.real_parse(json1, {})
|
332
|
+
avro_schema2 = Avro::Schema.real_parse(json2, {})
|
333
|
+
Avro::SchemaCompatibility.mutual_read?(avro_schema1, avro_schema2)
|
334
|
+
end
|
335
|
+
|
336
|
+
private
|
337
|
+
|
338
|
+
def _key_from_consumer(consumer)
|
339
|
+
if consumer.config[:key_field] || consumer.config[:key_schema]
|
340
|
+
{ 'test' => 1 }
|
341
|
+
else
|
342
|
+
1
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
# @param topic [String]
|
347
|
+
# @return [Class]
|
348
|
+
def _get_handler_class_from_topic(topic)
|
349
|
+
listeners = Phobos.config['listeners']
|
350
|
+
handler = listeners.find { |l| l.topic == topic }
|
351
|
+
raise "No consumer found in Phobos configuration for topic #{topic}!" if handler.nil?
|
352
|
+
|
353
|
+
handler.handler.constantize
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|