deimos-temp-fork 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (146) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +83 -0
  3. data/.gitignore +41 -0
  4. data/.gitmodules +0 -0
  5. data/.rspec +1 -0
  6. data/.rubocop.yml +333 -0
  7. data/.ruby-gemset +1 -0
  8. data/.ruby-version +1 -0
  9. data/CHANGELOG.md +349 -0
  10. data/CODE_OF_CONDUCT.md +77 -0
  11. data/Dockerfile +23 -0
  12. data/Gemfile +6 -0
  13. data/Gemfile.lock +286 -0
  14. data/Guardfile +22 -0
  15. data/LICENSE.md +195 -0
  16. data/README.md +1099 -0
  17. data/Rakefile +13 -0
  18. data/bin/deimos +4 -0
  19. data/deimos-ruby.gemspec +44 -0
  20. data/docker-compose.yml +71 -0
  21. data/docs/ARCHITECTURE.md +140 -0
  22. data/docs/CONFIGURATION.md +236 -0
  23. data/docs/DATABASE_BACKEND.md +147 -0
  24. data/docs/INTEGRATION_TESTS.md +52 -0
  25. data/docs/PULL_REQUEST_TEMPLATE.md +35 -0
  26. data/docs/UPGRADING.md +128 -0
  27. data/lib/deimos-temp-fork.rb +95 -0
  28. data/lib/deimos/active_record_consume/batch_consumption.rb +164 -0
  29. data/lib/deimos/active_record_consume/batch_slicer.rb +27 -0
  30. data/lib/deimos/active_record_consume/message_consumption.rb +79 -0
  31. data/lib/deimos/active_record_consume/schema_model_converter.rb +52 -0
  32. data/lib/deimos/active_record_consumer.rb +67 -0
  33. data/lib/deimos/active_record_producer.rb +87 -0
  34. data/lib/deimos/backends/base.rb +32 -0
  35. data/lib/deimos/backends/db.rb +41 -0
  36. data/lib/deimos/backends/kafka.rb +33 -0
  37. data/lib/deimos/backends/kafka_async.rb +33 -0
  38. data/lib/deimos/backends/test.rb +20 -0
  39. data/lib/deimos/batch_consumer.rb +7 -0
  40. data/lib/deimos/config/configuration.rb +381 -0
  41. data/lib/deimos/config/phobos_config.rb +137 -0
  42. data/lib/deimos/consume/batch_consumption.rb +150 -0
  43. data/lib/deimos/consume/message_consumption.rb +94 -0
  44. data/lib/deimos/consumer.rb +104 -0
  45. data/lib/deimos/instrumentation.rb +76 -0
  46. data/lib/deimos/kafka_message.rb +60 -0
  47. data/lib/deimos/kafka_source.rb +128 -0
  48. data/lib/deimos/kafka_topic_info.rb +102 -0
  49. data/lib/deimos/message.rb +79 -0
  50. data/lib/deimos/metrics/datadog.rb +47 -0
  51. data/lib/deimos/metrics/mock.rb +39 -0
  52. data/lib/deimos/metrics/provider.rb +36 -0
  53. data/lib/deimos/monkey_patches/phobos_cli.rb +35 -0
  54. data/lib/deimos/monkey_patches/phobos_producer.rb +51 -0
  55. data/lib/deimos/poll_info.rb +9 -0
  56. data/lib/deimos/producer.rb +224 -0
  57. data/lib/deimos/railtie.rb +8 -0
  58. data/lib/deimos/schema_backends/avro_base.rb +140 -0
  59. data/lib/deimos/schema_backends/avro_local.rb +30 -0
  60. data/lib/deimos/schema_backends/avro_schema_coercer.rb +119 -0
  61. data/lib/deimos/schema_backends/avro_schema_registry.rb +34 -0
  62. data/lib/deimos/schema_backends/avro_validation.rb +21 -0
  63. data/lib/deimos/schema_backends/base.rb +150 -0
  64. data/lib/deimos/schema_backends/mock.rb +42 -0
  65. data/lib/deimos/shared_config.rb +63 -0
  66. data/lib/deimos/test_helpers.rb +360 -0
  67. data/lib/deimos/tracing/datadog.rb +35 -0
  68. data/lib/deimos/tracing/mock.rb +40 -0
  69. data/lib/deimos/tracing/provider.rb +29 -0
  70. data/lib/deimos/utils/db_poller.rb +150 -0
  71. data/lib/deimos/utils/db_producer.rb +243 -0
  72. data/lib/deimos/utils/deadlock_retry.rb +68 -0
  73. data/lib/deimos/utils/inline_consumer.rb +150 -0
  74. data/lib/deimos/utils/lag_reporter.rb +175 -0
  75. data/lib/deimos/utils/schema_controller_mixin.rb +115 -0
  76. data/lib/deimos/version.rb +5 -0
  77. data/lib/generators/deimos/active_record/templates/migration.rb.tt +28 -0
  78. data/lib/generators/deimos/active_record/templates/model.rb.tt +5 -0
  79. data/lib/generators/deimos/active_record_generator.rb +79 -0
  80. data/lib/generators/deimos/db_backend/templates/migration +25 -0
  81. data/lib/generators/deimos/db_backend/templates/rails3_migration +31 -0
  82. data/lib/generators/deimos/db_backend_generator.rb +48 -0
  83. data/lib/generators/deimos/db_poller/templates/migration +11 -0
  84. data/lib/generators/deimos/db_poller/templates/rails3_migration +16 -0
  85. data/lib/generators/deimos/db_poller_generator.rb +48 -0
  86. data/lib/tasks/deimos.rake +34 -0
  87. data/spec/active_record_batch_consumer_spec.rb +481 -0
  88. data/spec/active_record_consume/batch_slicer_spec.rb +42 -0
  89. data/spec/active_record_consume/schema_model_converter_spec.rb +105 -0
  90. data/spec/active_record_consumer_spec.rb +154 -0
  91. data/spec/active_record_producer_spec.rb +85 -0
  92. data/spec/backends/base_spec.rb +10 -0
  93. data/spec/backends/db_spec.rb +54 -0
  94. data/spec/backends/kafka_async_spec.rb +11 -0
  95. data/spec/backends/kafka_spec.rb +11 -0
  96. data/spec/batch_consumer_spec.rb +256 -0
  97. data/spec/config/configuration_spec.rb +248 -0
  98. data/spec/consumer_spec.rb +209 -0
  99. data/spec/deimos_spec.rb +169 -0
  100. data/spec/generators/active_record_generator_spec.rb +56 -0
  101. data/spec/handlers/my_batch_consumer.rb +10 -0
  102. data/spec/handlers/my_consumer.rb +10 -0
  103. data/spec/kafka_listener_spec.rb +55 -0
  104. data/spec/kafka_source_spec.rb +381 -0
  105. data/spec/kafka_topic_info_spec.rb +111 -0
  106. data/spec/message_spec.rb +19 -0
  107. data/spec/phobos.bad_db.yml +73 -0
  108. data/spec/phobos.yml +77 -0
  109. data/spec/producer_spec.rb +498 -0
  110. data/spec/rake_spec.rb +19 -0
  111. data/spec/schema_backends/avro_base_shared.rb +199 -0
  112. data/spec/schema_backends/avro_local_spec.rb +32 -0
  113. data/spec/schema_backends/avro_schema_registry_spec.rb +32 -0
  114. data/spec/schema_backends/avro_validation_spec.rb +24 -0
  115. data/spec/schema_backends/base_spec.rb +33 -0
  116. data/spec/schemas/com/my-namespace/Generated.avsc +71 -0
  117. data/spec/schemas/com/my-namespace/MyNestedSchema.avsc +62 -0
  118. data/spec/schemas/com/my-namespace/MySchema-key.avsc +13 -0
  119. data/spec/schemas/com/my-namespace/MySchema.avsc +18 -0
  120. data/spec/schemas/com/my-namespace/MySchemaCompound-key.avsc +18 -0
  121. data/spec/schemas/com/my-namespace/MySchemaWithBooleans.avsc +18 -0
  122. data/spec/schemas/com/my-namespace/MySchemaWithDateTimes.avsc +33 -0
  123. data/spec/schemas/com/my-namespace/MySchemaWithId.avsc +28 -0
  124. data/spec/schemas/com/my-namespace/MySchemaWithUniqueId.avsc +32 -0
  125. data/spec/schemas/com/my-namespace/Wibble.avsc +43 -0
  126. data/spec/schemas/com/my-namespace/Widget.avsc +27 -0
  127. data/spec/schemas/com/my-namespace/WidgetTheSecond.avsc +27 -0
  128. data/spec/schemas/com/my-namespace/request/CreateTopic.avsc +11 -0
  129. data/spec/schemas/com/my-namespace/request/Index.avsc +11 -0
  130. data/spec/schemas/com/my-namespace/request/UpdateRequest.avsc +11 -0
  131. data/spec/schemas/com/my-namespace/response/CreateTopic.avsc +11 -0
  132. data/spec/schemas/com/my-namespace/response/Index.avsc +11 -0
  133. data/spec/schemas/com/my-namespace/response/UpdateResponse.avsc +11 -0
  134. data/spec/spec_helper.rb +267 -0
  135. data/spec/utils/db_poller_spec.rb +320 -0
  136. data/spec/utils/db_producer_spec.rb +514 -0
  137. data/spec/utils/deadlock_retry_spec.rb +74 -0
  138. data/spec/utils/inline_consumer_spec.rb +31 -0
  139. data/spec/utils/lag_reporter_spec.rb +76 -0
  140. data/spec/utils/platform_schema_validation_spec.rb +0 -0
  141. data/spec/utils/schema_controller_mixin_spec.rb +84 -0
  142. data/support/deimos-solo.png +0 -0
  143. data/support/deimos-with-name-next.png +0 -0
  144. data/support/deimos-with-name.png +0 -0
  145. data/support/flipp-logo.png +0 -0
  146. metadata +551 -0
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deimos
4
+ module SchemaBackends
5
+ # Mock implementation of a schema backend that does no encoding or validation.
6
+ class Mock < Base
7
+ # @override
8
+ def decode_payload(payload, schema:)
9
+ payload.is_a?(String) ? 'payload-decoded' : payload.map { |k, v| [k, "decoded-#{v}"] }
10
+ end
11
+
12
+ # @override
13
+ def encode_payload(payload, schema:, topic: nil)
14
+ payload.is_a?(String) ? 'payload-encoded' : payload.map { |k, v| [k, "encoded-#{v}"] }
15
+ end
16
+
17
+ # @override
18
+ def validate(_payload, schema:)
19
+ end
20
+
21
+ # @override
22
+ def schema_fields
23
+ []
24
+ end
25
+
26
+ # @override
27
+ def coerce_field(_type, value)
28
+ value
29
+ end
30
+
31
+ # @override
32
+ def encode_key(key_id, key)
33
+ { key_id => key }
34
+ end
35
+
36
+ # @override
37
+ def decode_key(payload, key_id)
38
+ payload[key_id]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,63 @@
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
+ # default to none: true
18
+ @config = {
19
+ key_configured: false,
20
+ encode_key: false,
21
+ no_keys: true
22
+ }
23
+ klass = self.superclass
24
+ while klass.respond_to?(:config)
25
+ klass_config = klass.config
26
+ if klass_config
27
+ # default is true for this so don't include it in the merge
28
+ klass_config.delete(:encode_key) if klass_config[:encode_key]
29
+ @config.merge!(klass_config) if klass.config
30
+ end
31
+ klass = klass.superclass
32
+ end
33
+ @config
34
+ end
35
+
36
+ # Set the schema.
37
+ # @param schema [String]
38
+ def schema(schema)
39
+ config[:schema] = schema
40
+ end
41
+
42
+ # Set the namespace.
43
+ # @param namespace [String]
44
+ def namespace(namespace)
45
+ config[:namespace] = namespace
46
+ end
47
+
48
+ # Set key configuration.
49
+ # @param field [Symbol] the name of a field to use in the value schema as
50
+ # a generated key schema
51
+ # @param schema [String|Symbol] the name of a schema to use for the key
52
+ # @param plain [Boolean] if true, do not encode keys at all
53
+ # @param none [Boolean] if true, do not use keys at all
54
+ def key_config(plain: nil, field: nil, schema: nil, none: nil)
55
+ config[:key_configured] = true
56
+ config[:no_keys] = none
57
+ config[:encode_key] = !plain && !none
58
+ config[:key_field] = field&.to_s
59
+ config[:key_schema] = schema
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,360 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+ require 'active_support/core_ext'
5
+ require 'deimos/tracing/mock'
6
+ require 'deimos/metrics/mock'
7
+
8
+ module Deimos
9
+ # Include this module in your RSpec spec_helper
10
+ # to stub out external dependencies
11
+ # and add methods to use to test encoding/decoding.
12
+ module TestHelpers
13
+ extend ActiveSupport::Concern
14
+
15
+ class << self
16
+ # for backwards compatibility
17
+ # @return [Array<Hash>]
18
+ def sent_messages
19
+ Deimos::Backends::Test.sent_messages
20
+ end
21
+
22
+ # Set the config to the right settings for a unit test
23
+ def unit_test!
24
+ Deimos.configure do |deimos_config|
25
+ deimos_config.logger = Logger.new(STDOUT)
26
+ deimos_config.consumers.reraise_errors = true
27
+ deimos_config.kafka.seed_brokers ||= ['test_broker']
28
+ deimos_config.schema.backend = Deimos.schema_backend_class.mock_backend
29
+ deimos_config.producers.backend = :test
30
+ end
31
+ end
32
+
33
+ # Kafka test config with avro schema registry
34
+ def full_integration_test!
35
+ Deimos.configure do |deimos_config|
36
+ deimos_config.producers.backend = :kafka
37
+ deimos_config.schema.backend = :avro_schema_registry
38
+ end
39
+ end
40
+
41
+ # Set the config to the right settings for a kafka test
42
+ def kafka_test!
43
+ Deimos.configure do |deimos_config|
44
+ deimos_config.producers.backend = :kafka
45
+ deimos_config.schema.backend = :avro_validation
46
+ end
47
+ end
48
+ end
49
+
50
+ included do
51
+
52
+ RSpec.configure do |config|
53
+
54
+ config.prepend_before(:each) do
55
+ client = double('client').as_null_object
56
+ allow(client).to receive(:time) do |*_args, &block|
57
+ block.call
58
+ end
59
+ Deimos::Backends::Test.sent_messages.clear
60
+ end
61
+ end
62
+
63
+ end
64
+
65
+ # @deprecated
66
+ def stub_producers_and_consumers!
67
+ warn('stub_producers_and_consumers! is no longer necessary and this method will be removed in 3.0')
68
+ end
69
+
70
+ # @deprecated
71
+ def stub_producer(_klass)
72
+ warn('Stubbing producers is no longer necessary and this method will be removed in 3.0')
73
+ end
74
+
75
+ # @deprecated
76
+ def stub_consumer(_klass)
77
+ warn('Stubbing consumers is no longer necessary and this method will be removed in 3.0')
78
+ end
79
+
80
+ # @deprecated
81
+ def stub_batch_consumer(_klass)
82
+ warn('Stubbing batch consumers is no longer necessary and this method will be removed in 3.0')
83
+ end
84
+
85
+ # get the difference of 2 hashes.
86
+ # @param hash1 [Hash]
87
+ # @param hash2 [Hash]
88
+ def _hash_diff(hash1, hash2)
89
+ if hash1.nil? || !hash1.is_a?(Hash)
90
+ hash2
91
+ elsif hash2.nil? || !hash2.is_a?(Hash)
92
+ hash1
93
+ else
94
+ hash1.dup.
95
+ delete_if { |k, v| hash2[k] == v }.
96
+ merge!(hash2.dup.delete_if { |k, _v| hash1.key?(k) })
97
+ end
98
+ end
99
+
100
+ # :nodoc:
101
+ def _frk_failure_message(topic, message, key=nil, partition_key=nil, was_negated=false)
102
+ messages = Deimos::Backends::Test.sent_messages.
103
+ select { |m| m[:topic] == topic }.
104
+ map { |m| m.except(:topic) }
105
+ message_string = ''
106
+ diff = nil
107
+ min_hash_diff = nil
108
+ if messages.any?
109
+ message_string = messages.map(&:inspect).join("\n")
110
+ min_hash_diff = messages.min_by { |m| _hash_diff(m, message).keys.size }
111
+ diff = RSpec::Expectations.differ.
112
+ diff_as_object(message, min_hash_diff[:payload])
113
+ end
114
+ description = if message.respond_to?(:description)
115
+ message.description
116
+ elsif message.nil?
117
+ 'nil'
118
+ else
119
+ message
120
+ end
121
+ str = "Expected #{topic} #{'not ' if was_negated}to have sent #{description}"
122
+ str += " with key #{key}" if key
123
+ str += " with partition key #{partition_key}" if partition_key
124
+ str += "\nClosest message received: #{min_hash_diff}" if min_hash_diff
125
+ str += "\nDiff: #{diff}" if diff
126
+ str + "\nAll Messages received:\n#{message_string}"
127
+ end
128
+
129
+ RSpec::Matchers.define :have_sent do |msg, key=nil, partition_key=nil|
130
+ message = if msg.respond_to?(:with_indifferent_access)
131
+ msg.with_indifferent_access
132
+ else
133
+ msg
134
+ end
135
+ match do |topic|
136
+ Deimos::Backends::Test.sent_messages.any? do |m|
137
+ hash_matcher = RSpec::Matchers::BuiltIn::Match.new(message)
138
+ hash_matcher.send(:match,
139
+ message,
140
+ m[:payload]&.with_indifferent_access) &&
141
+ topic == m[:topic] &&
142
+ (key.present? ? key == m[:key] : true) &&
143
+ (partition_key.present? ? partition_key == m[:partition_key] : true)
144
+ end
145
+ end
146
+
147
+ if respond_to?(:failure_message)
148
+ failure_message do |topic|
149
+ _frk_failure_message(topic, message, key, partition_key)
150
+ end
151
+ failure_message_when_negated do |topic|
152
+ _frk_failure_message(topic, message, key, partition_key, true)
153
+ end
154
+ else
155
+ failure_message_for_should do |topic|
156
+ _frk_failure_message(topic, message, key, partition_key)
157
+ end
158
+ failure_message_for_should_not do |topic|
159
+ _frk_failure_message(topic, message, key, partition_key, true)
160
+ end
161
+ end
162
+ end
163
+
164
+ # Clear all sent messages - e.g. if we want to check that
165
+ # particular messages were sent or not sent after a point in time.
166
+ def clear_kafka_messages!
167
+ Deimos::Backends::Test.sent_messages.clear
168
+ end
169
+
170
+ # Test that a given handler will consume a given payload correctly, i.e.
171
+ # that the schema is correct. If
172
+ # a block is given, that block will be executed when `consume` is called.
173
+ # Otherwise it will just confirm that `consume` is called at all.
174
+ # @param handler_class_or_topic [Class|String] Class which inherits from
175
+ # Deimos::Consumer or the topic as a string
176
+ # @param payload [Hash] the payload to consume
177
+ # @param call_original [Boolean] if true, allow the consume handler
178
+ # to continue as normal. Not compatible with a block.
179
+ # @param ignore_expectation [Boolean] Set to true to not place any
180
+ # expectations on the consumer. Primarily used internally to Deimos.
181
+ # @param key [Object] the key to use.
182
+ # @param partition_key [Object] the partition key to use.
183
+ def test_consume_message(handler_class_or_topic,
184
+ payload,
185
+ call_original: false,
186
+ key: nil,
187
+ partition_key: nil,
188
+ skip_expectation: false,
189
+ &block)
190
+ raise 'Cannot have both call_original and be given a block!' if call_original && block_given?
191
+
192
+ payload&.stringify_keys!
193
+ handler_class = if handler_class_or_topic.is_a?(String)
194
+ _get_handler_class_from_topic(handler_class_or_topic)
195
+ else
196
+ handler_class_or_topic
197
+ end
198
+ handler = handler_class.new
199
+ allow(handler_class).to receive(:new).and_return(handler)
200
+ listener = double('listener',
201
+ handler_class: handler_class,
202
+ encoding: nil)
203
+ key ||= _key_from_consumer(handler_class)
204
+ message = double('message',
205
+ 'key' => key,
206
+ 'partition_key' => partition_key,
207
+ 'partition' => 1,
208
+ 'offset' => 1,
209
+ 'headers' => {},
210
+ 'value' => payload)
211
+
212
+ unless skip_expectation
213
+ expectation = expect(handler).to receive(:consume).
214
+ with(payload, anything, &block)
215
+ expectation.and_call_original if call_original
216
+ end
217
+ Phobos::Actions::ProcessMessage.new(
218
+ listener: listener,
219
+ message: message,
220
+ listener_metadata: { topic: 'my-topic' }
221
+ ).send(:process_message, payload)
222
+ end
223
+
224
+ # Check to see that a given message will fail due to validation errors.
225
+ # @param handler_class [Class]
226
+ # @param payload [Hash]
227
+ def test_consume_invalid_message(handler_class, payload)
228
+ expect {
229
+ handler_class.decoder.validate(payload,
230
+ schema: handler_class.decoder.schema)
231
+ }.to raise_error(Avro::SchemaValidator::ValidationError)
232
+ end
233
+
234
+ # Test that a given handler will consume a given batch payload correctly,
235
+ # i.e. that the schema is correct. If
236
+ # a block is given, that block will be executed when `consume` is called.
237
+ # Otherwise it will just confirm that `consume` is called at all.
238
+ # @param handler_class_or_topic [Class|String] Class which inherits from
239
+ # Deimos::Consumer or the topic as a string
240
+ # @param payloads [Array<Hash>] the payload to consume
241
+ def test_consume_batch(handler_class_or_topic,
242
+ payloads,
243
+ keys: [],
244
+ partition_keys: [],
245
+ call_original: false,
246
+ skip_expectation: false,
247
+ &block)
248
+ if call_original && block_given?
249
+ raise 'Cannot have both call_original and be given a block!'
250
+ end
251
+
252
+ topic_name = 'my-topic'
253
+ handler_class = if handler_class_or_topic.is_a?(String)
254
+ _get_handler_class_from_topic(handler_class_or_topic)
255
+ else
256
+ handler_class_or_topic
257
+ end
258
+ handler = handler_class.new
259
+ allow(handler_class).to receive(:new).and_return(handler)
260
+ listener = double('listener',
261
+ handler_class: handler_class,
262
+ encoding: nil)
263
+ batch_messages = payloads.zip(keys, partition_keys).map do |payload, key, partition_key|
264
+ key ||= _key_from_consumer(handler_class)
265
+
266
+ double('message',
267
+ 'key' => key,
268
+ 'partition_key' => partition_key,
269
+ 'partition' => 1,
270
+ 'offset' => 1,
271
+ 'headers' => {},
272
+ 'value' => payload)
273
+ end
274
+ batch = double('fetched_batch',
275
+ 'messages' => batch_messages,
276
+ 'topic' => topic_name,
277
+ 'partition' => 1,
278
+ 'offset_lag' => 0)
279
+ unless skip_expectation
280
+ expectation = expect(handler).to receive(:consume_batch).
281
+ with(payloads, anything, &block)
282
+ expectation.and_call_original if call_original
283
+ end
284
+ action = Phobos::Actions::ProcessBatchInline.new(
285
+ listener: listener,
286
+ batch: batch,
287
+ metadata: { topic: topic_name }
288
+ )
289
+ allow(action).to receive(:backoff_interval).and_return(0)
290
+ allow(action).to receive(:handle_error) { |e| raise e }
291
+ action.send(:execute)
292
+ end
293
+
294
+ # Check to see that a given message will fail due to validation errors.
295
+ # @param handler_class [Class]
296
+ # @param payloads [Array<Hash>]
297
+ def test_consume_batch_invalid_message(handler_class, payloads)
298
+ topic_name = 'my-topic'
299
+ handler = handler_class.new
300
+ allow(handler_class).to receive(:new).and_return(handler)
301
+ listener = double('listener',
302
+ handler_class: handler_class,
303
+ encoding: nil)
304
+ batch_messages = payloads.map do |payload|
305
+ key ||= _key_from_consumer(handler_class)
306
+
307
+ double('message',
308
+ 'key' => key,
309
+ 'partition' => 1,
310
+ 'offset' => 1,
311
+ 'value' => payload)
312
+ end
313
+ batch = double('fetched_batch',
314
+ 'messages' => batch_messages,
315
+ 'topic' => topic_name,
316
+ 'partition' => 1,
317
+ 'offset_lag' => 0)
318
+
319
+ action = Phobos::Actions::ProcessBatchInline.new(
320
+ listener: listener,
321
+ batch: batch,
322
+ metadata: { topic: topic_name }
323
+ )
324
+ allow(action).to receive(:backoff_interval).and_return(0)
325
+ allow(action).to receive(:handle_error) { |e| raise e }
326
+
327
+ expect { action.send(:execute) }.
328
+ to raise_error
329
+ end
330
+
331
+ private
332
+
333
+ def _key_from_consumer(consumer)
334
+ if consumer.config[:key_field]
335
+ { consumer.config[:key_field] => 1 }
336
+ elsif consumer.config[:key_schema]
337
+ backend = consumer.decoder
338
+ old_schema = backend.schema
339
+ backend.schema = consumer.config[:key_schema]
340
+ key = backend.schema_fields.map { |field| [field.name, 1] }.to_h
341
+ backend.schema = old_schema
342
+ key
343
+ elsif consumer.config[:no_keys]
344
+ nil
345
+ else
346
+ 1
347
+ end
348
+ end
349
+
350
+ # @param topic [String]
351
+ # @return [Class]
352
+ def _get_handler_class_from_topic(topic)
353
+ listeners = Phobos.config['listeners']
354
+ handler = listeners.find { |l| l.topic == topic }
355
+ raise "No consumer found in Phobos configuration for topic #{topic}!" if handler.nil?
356
+
357
+ handler.handler.constantize
358
+ end
359
+ end
360
+ end