deimos-ruby 1.8.1.pre.beta1 → 1.8.1.pre.beta6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/Gemfile.lock +11 -3
- data/README.md +55 -2
- data/deimos-ruby.gemspec +1 -1
- data/lib/deimos.rb +1 -0
- data/lib/deimos/active_record_consume/batch_consumption.rb +7 -2
- data/lib/deimos/active_record_consume/message_consumption.rb +8 -1
- data/lib/deimos/consume/message_consumption.rb +1 -1
- data/lib/deimos/instrumentation.rb +10 -5
- data/lib/deimos/schema_backends/avro_base.rb +5 -0
- data/lib/deimos/schema_backends/avro_schema_coercer.rb +30 -9
- data/lib/deimos/schema_backends/base.rb +6 -0
- data/lib/deimos/utils/schema_controller_mixin.rb +111 -0
- data/lib/deimos/version.rb +1 -1
- data/spec/consumer_spec.rb +6 -0
- data/spec/kafka_listener_spec.rb +55 -0
- data/spec/producer_spec.rb +36 -0
- data/spec/schemas/com/my-namespace/MyNestedSchema.avsc +62 -0
- data/spec/schemas/com/my-namespace/request/Index.avsc +11 -0
- data/spec/schemas/com/my-namespace/request/UpdateRequest.avsc +11 -0
- data/spec/schemas/com/my-namespace/response/Index.avsc +11 -0
- data/spec/schemas/com/my-namespace/response/UpdateResponse.avsc +11 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/utils/schema_controller_mixin_spec.rb +68 -0
- metadata +31 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d38b0803c074ff89dd8ff166ed8de2dcacb2ecaae46cb321abc0de630498e849
|
4
|
+
data.tar.gz: efecfba1d4d76a83270534ab7687c344253b2e4a7765dde9d42b6959083171db
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d435a7c9f130650f7f9849554f2d0a71b87a9f8bddca2f542b94848ca13e8fe3084aa095f19eadd581a7af42f43511509ae9dc06bbb69982da34463304945a69
|
7
|
+
data.tar.gz: 9cfd22453a8a59d6c74a46bf41c6feb874bf125fb335c4b084df04f681bb9f3d2b03a8295a18c86166c194e592855d8b811bad39c29565002ec6dd6100edd589
|
data/CHANGELOG.md
CHANGED
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
7
7
|
|
8
8
|
## UNRELEASED
|
9
9
|
|
10
|
+
## 1.8.1-beta6 - 2020-08-13
|
11
|
+
- Fix for consuming nil payloads with Ruby 2.3.
|
12
|
+
|
13
|
+
## 1.8.1-beta5 - 2020-08-13
|
14
|
+
|
15
|
+
### Fixes :wrench:
|
16
|
+
- Fix regression bug which introduces backwards incompatibility
|
17
|
+
with ActiveRecordProducer's `record_attributes` method.
|
18
|
+
|
19
|
+
## 1.8.1-beta4 - 2020-08-12
|
20
|
+
|
21
|
+
### Fixes :wrench:
|
22
|
+
- Fix regression bug where arrays were not being encoded
|
23
|
+
|
24
|
+
## 1.8.1-beta3 - 2020-08-05
|
25
|
+
|
26
|
+
### Fixes :wrench:
|
27
|
+
- Simplify decoding messages and handle producer not found
|
28
|
+
- Consolidate types in sub-records recursively
|
29
|
+
(fixes [#72](https://github.com/flipp-oss/deimos/issues/72))
|
30
|
+
|
31
|
+
## 1.8.1-beta2 - 2020-07-28
|
32
|
+
|
33
|
+
### Features :star:
|
34
|
+
- Add `SchemaControllerMixin` to encode and decode schema-encoded
|
35
|
+
payloads in Rails controllers.
|
36
|
+
|
10
37
|
## 1.8.1-beta1 - 2020-07-22
|
11
38
|
|
12
39
|
### Fixes :wrench:
|
@@ -35,6 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
35
62
|
- Added `ActiveRecordConsumer` batch mode
|
36
63
|
|
37
64
|
### Fixes :wrench:
|
65
|
+
- Fixes `send_produce_error` to decode `failed_messages` with built-in decoder.
|
38
66
|
- Lag calculation can be incorrect if no messages are being consumed.
|
39
67
|
- Fixed bug where printing messages on a MessageSizeTooLarge
|
40
68
|
error didn't work.
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
deimos-ruby (1.8.1.pre.
|
4
|
+
deimos-ruby (1.8.1.pre.beta6)
|
5
5
|
avro_turf (~> 0.11)
|
6
6
|
phobos (~> 1.9)
|
7
7
|
ruby-kafka (~> 0.7)
|
@@ -87,7 +87,7 @@ GEM
|
|
87
87
|
rake (~> 13.0)
|
88
88
|
dogstatsd-ruby (4.8.1)
|
89
89
|
erubi (1.9.0)
|
90
|
-
excon (0.
|
90
|
+
excon (0.76.0)
|
91
91
|
exponential-backoff (0.0.4)
|
92
92
|
ffi (1.13.1)
|
93
93
|
formatador (0.2.5)
|
@@ -205,6 +205,14 @@ GEM
|
|
205
205
|
rspec-mocks (3.9.1)
|
206
206
|
diff-lcs (>= 1.2.0, < 2.0)
|
207
207
|
rspec-support (~> 3.9.0)
|
208
|
+
rspec-rails (4.0.1)
|
209
|
+
actionpack (>= 4.2)
|
210
|
+
activesupport (>= 4.2)
|
211
|
+
railties (>= 4.2)
|
212
|
+
rspec-core (~> 3.9)
|
213
|
+
rspec-expectations (~> 3.9)
|
214
|
+
rspec-mocks (~> 3.9)
|
215
|
+
rspec-support (~> 3.9)
|
208
216
|
rspec-support (3.9.3)
|
209
217
|
rspec_junit_formatter (0.4.1)
|
210
218
|
rspec-core (>= 2, < 4, != 2.12.0)
|
@@ -250,7 +258,6 @@ PLATFORMS
|
|
250
258
|
ruby
|
251
259
|
|
252
260
|
DEPENDENCIES
|
253
|
-
activerecord (~> 6)
|
254
261
|
activerecord-import
|
255
262
|
avro (~> 1.9)
|
256
263
|
database_cleaner (~> 1.7)
|
@@ -265,6 +272,7 @@ DEPENDENCIES
|
|
265
272
|
rails (~> 6)
|
266
273
|
rake (~> 13)
|
267
274
|
rspec (~> 3)
|
275
|
+
rspec-rails (~> 4)
|
268
276
|
rspec_junit_formatter (~> 0.3)
|
269
277
|
rubocop (~> 0.72)
|
270
278
|
rubocop-rspec (~> 1.27)
|
data/README.md
CHANGED
@@ -22,6 +22,7 @@ Built on Phobos and hence Ruby-Kafka.
|
|
22
22
|
* [Kafka Message Keys](#kafka-message-keys)
|
23
23
|
* [Consumers](#consumers)
|
24
24
|
* [Rails Integration](#rails-integration)
|
25
|
+
* [Controller Mixin](#controller-mixin)
|
25
26
|
* [Database Backend](#database-backend)
|
26
27
|
* [Database Poller](#database-poller)
|
27
28
|
* [Running Consumers](#running-consumers)
|
@@ -447,6 +448,58 @@ class Widget < ActiveRecord::Base
|
|
447
448
|
end
|
448
449
|
```
|
449
450
|
|
451
|
+
### Controller Mixin
|
452
|
+
|
453
|
+
Deimos comes with a mixin for `ActionController` which automatically encodes and decodes schema
|
454
|
+
payloads. There are some advantages to encoding your data in e.g. Avro rather than straight JSON,
|
455
|
+
particularly if your service is talking to another backend service rather than the front-end
|
456
|
+
browser:
|
457
|
+
|
458
|
+
* It enforces a contract between services. Solutions like [OpenAPI](https://swagger.io/specification/)
|
459
|
+
do this as well, but in order for the client to know the contract, usually some kind of code
|
460
|
+
generation has to happen. Using schemas ensures both sides know the contract without having to change code.
|
461
|
+
In addition, OpenAPI is now a huge and confusing format, and using simpler schema formats
|
462
|
+
can be beneficial.
|
463
|
+
* Using Avro or Protobuf ensures both forwards and backwards compatibility,
|
464
|
+
which reduces the need for versioning since both sides can simply ignore fields they aren't aware
|
465
|
+
of.
|
466
|
+
* Encoding and decoding using Avro or Protobuf is generally faster than straight JSON, and
|
467
|
+
results in smaller payloads and therefore less network traffic.
|
468
|
+
|
469
|
+
To use the mixin, add the following to your `WhateverController`:
|
470
|
+
|
471
|
+
```ruby
|
472
|
+
class WhateverController < ApplicationController
|
473
|
+
include Deimos::Utils::SchemaControllerMixin
|
474
|
+
|
475
|
+
request_namespace 'my.namespace.requests'
|
476
|
+
response_namespace 'my.namespace.responses'
|
477
|
+
|
478
|
+
# Add a "schemas" line for all routes that should encode/decode schemas.
|
479
|
+
# Default is to match the schema name to the route name.
|
480
|
+
schemas :index
|
481
|
+
# will look for: my.namespace.requests.Index.avsc
|
482
|
+
# my.namespace.responses.Index.avsc
|
483
|
+
|
484
|
+
# If all routes use the default, you can add them all at once
|
485
|
+
schemas :index, :show, :update
|
486
|
+
|
487
|
+
# Different schemas can be specified as well
|
488
|
+
schemas :index, :show, request: 'IndexRequest', response: 'IndexResponse'
|
489
|
+
|
490
|
+
# To access the encoded data, use the `payload` helper method, and to render it back,
|
491
|
+
# use the `render_schema` method.
|
492
|
+
|
493
|
+
def index
|
494
|
+
response = { 'response_id' => payload['request_id'] + 'hi mom' }
|
495
|
+
render_schema(response)
|
496
|
+
end
|
497
|
+
end
|
498
|
+
```
|
499
|
+
|
500
|
+
To make use of this feature, your requests and responses need to have the correct content type.
|
501
|
+
For Avro content, this is the `avro/binary` content type.
|
502
|
+
|
450
503
|
# Database Backend
|
451
504
|
|
452
505
|
Deimos provides a way to allow Kafka messages to be created inside a
|
@@ -546,7 +599,7 @@ class MyConsumer < Deimos::ActiveRecordConsumer
|
|
546
599
|
|
547
600
|
# Optional override to change the attributes of the record before they
|
548
601
|
# are saved.
|
549
|
-
def record_attributes(payload)
|
602
|
+
def record_attributes(payload, key)
|
550
603
|
super.merge(:some_field => 'some_value')
|
551
604
|
end
|
552
605
|
|
@@ -627,7 +680,7 @@ class MyConsumer < Deimos::ActiveRecordConsumer
|
|
627
680
|
|
628
681
|
# Optional override to change the attributes of the record before they
|
629
682
|
# are saved.
|
630
|
-
def record_attributes(payload)
|
683
|
+
def record_attributes(payload, key)
|
631
684
|
super.merge(:some_field => 'some_value')
|
632
685
|
end
|
633
686
|
end
|
data/deimos-ruby.gemspec
CHANGED
@@ -23,7 +23,6 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.add_runtime_dependency('ruby-kafka', '~> 0.7')
|
24
24
|
spec.add_runtime_dependency('sigurd', '0.0.1')
|
25
25
|
|
26
|
-
spec.add_development_dependency('activerecord', '~> 6')
|
27
26
|
spec.add_development_dependency('activerecord-import')
|
28
27
|
spec.add_development_dependency('avro', '~> 1.9')
|
29
28
|
spec.add_development_dependency('database_cleaner', '~> 1.7')
|
@@ -38,6 +37,7 @@ Gem::Specification.new do |spec|
|
|
38
37
|
spec.add_development_dependency('rake', '~> 13')
|
39
38
|
spec.add_development_dependency('rspec', '~> 3')
|
40
39
|
spec.add_development_dependency('rspec_junit_formatter', '~>0.3')
|
40
|
+
spec.add_development_dependency('rspec-rails', '~> 4')
|
41
41
|
spec.add_development_dependency('rubocop', '~> 0.72')
|
42
42
|
spec.add_development_dependency('rubocop-rspec', '~> 1.27')
|
43
43
|
spec.add_development_dependency('sqlite3', '~> 1.3')
|
data/lib/deimos.rb
CHANGED
@@ -23,6 +23,7 @@ require 'deimos/monkey_patches/phobos_producer'
|
|
23
23
|
require 'deimos/monkey_patches/phobos_cli'
|
24
24
|
|
25
25
|
require 'deimos/railtie' if defined?(Rails)
|
26
|
+
require 'deimos/utils/schema_controller_mixin' if defined?(ActionController)
|
26
27
|
|
27
28
|
if defined?(ActiveRecord)
|
28
29
|
require 'deimos/kafka_source'
|
@@ -88,8 +88,13 @@ module Deimos
|
|
88
88
|
|
89
89
|
# Create payloads with payload + key attributes
|
90
90
|
upserts = messages.map do |m|
|
91
|
-
record_attributes
|
92
|
-
|
91
|
+
attrs = if self.method(:record_attributes).parameters.size == 2
|
92
|
+
record_attributes(m.payload, m.key)
|
93
|
+
else
|
94
|
+
record_attributes(m.payload)
|
95
|
+
end
|
96
|
+
|
97
|
+
attrs&.merge(record_key(m.key))
|
93
98
|
end
|
94
99
|
|
95
100
|
# If overridden record_attributes indicated no record, skip
|
@@ -37,7 +37,14 @@ module Deimos
|
|
37
37
|
record = klass.new
|
38
38
|
assign_key(record, payload, key)
|
39
39
|
end
|
40
|
-
|
40
|
+
|
41
|
+
# for backwards compatibility
|
42
|
+
# TODO next major release we should deprecate this
|
43
|
+
attrs = if self.method(:record_attributes).parameters.size == 2
|
44
|
+
record_attributes(payload.with_indifferent_access, key)
|
45
|
+
else
|
46
|
+
record_attributes(payload.with_indifferent_access)
|
47
|
+
end
|
41
48
|
# don't use attributes= - bypass Rails < 5 attr_protected
|
42
49
|
attrs.each do |k, v|
|
43
50
|
record.send("#{k}=", v)
|
@@ -46,13 +46,18 @@ module Deimos
|
|
46
46
|
|
47
47
|
messages = exception.failed_messages
|
48
48
|
messages.group_by(&:topic).each do |topic, batch|
|
49
|
-
|
49
|
+
producer = Deimos::Producer.descendants.find { |c| c.topic == topic }
|
50
|
+
next if batch.empty? || !producer
|
50
51
|
|
51
|
-
|
52
|
-
|
52
|
+
decoder = Deimos.schema_backend(schema: producer.config[:schema],
|
53
|
+
namespace: producer.config[:namespace])
|
54
|
+
payloads = batch.map { |m| decoder.decode(m.value) }
|
53
55
|
|
54
|
-
Deimos.config.metrics&.
|
55
|
-
|
56
|
+
Deimos.config.metrics&.increment(
|
57
|
+
'publish_error',
|
58
|
+
tags: %W(topic:#{topic}),
|
59
|
+
by: payloads.size
|
60
|
+
)
|
56
61
|
Deimos.instrument(
|
57
62
|
'produce_error',
|
58
63
|
producer: producer,
|
@@ -10,18 +10,37 @@ module Deimos
|
|
10
10
|
@schema = schema
|
11
11
|
end
|
12
12
|
|
13
|
-
#
|
13
|
+
# Coerce sub-records in a payload to match the schema.
|
14
|
+
# @param type [Avro::Schema::UnionSchema]
|
15
|
+
# @param val [Object]
|
16
|
+
# @return [Object]
|
17
|
+
def coerce_union(type, val)
|
18
|
+
union_types = type.schemas.map { |s| s.type.to_sym }
|
19
|
+
return nil if val.nil? && union_types.include?(:null)
|
20
|
+
|
21
|
+
schema_type = type.schemas.find { |s| s.type.to_sym != :null }
|
22
|
+
coerce_type(schema_type, val)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Coerce sub-records in a payload to match the schema.
|
26
|
+
# @param type [Avro::Schema::RecordSchema]
|
27
|
+
# @param val [Object]
|
28
|
+
# @return [Object]
|
29
|
+
def coerce_record(type, val)
|
30
|
+
record = val.map do |name, value|
|
31
|
+
field = type.fields.find { |f| f.name == name }
|
32
|
+
coerce_type(field.type, value)
|
33
|
+
end
|
34
|
+
val.keys.zip(record).to_h
|
35
|
+
end
|
36
|
+
|
37
|
+
# Coerce values in a payload to match the schema.
|
38
|
+
# @param type [Avro::Schema]
|
14
39
|
# @param val [Object]
|
15
40
|
# @return [Object]
|
16
41
|
def coerce_type(type, val)
|
17
42
|
int_classes = [Time, ActiveSupport::TimeWithZone]
|
18
43
|
field_type = type.type.to_sym
|
19
|
-
if field_type == :union
|
20
|
-
union_types = type.schemas.map { |s| s.type.to_sym }
|
21
|
-
return nil if val.nil? && union_types.include?(:null)
|
22
|
-
|
23
|
-
field_type = union_types.find { |t| t != :null }
|
24
|
-
end
|
25
44
|
|
26
45
|
case field_type
|
27
46
|
when :int, :long
|
@@ -32,14 +51,12 @@ module Deimos
|
|
32
51
|
else
|
33
52
|
val # this will fail
|
34
53
|
end
|
35
|
-
|
36
54
|
when :float, :double
|
37
55
|
if val.is_a?(Numeric) || _is_float_string?(val)
|
38
56
|
val.to_f
|
39
57
|
else
|
40
58
|
val # this will fail
|
41
59
|
end
|
42
|
-
|
43
60
|
when :string
|
44
61
|
if val.respond_to?(:to_str)
|
45
62
|
val.to_s
|
@@ -54,6 +71,10 @@ module Deimos
|
|
54
71
|
else
|
55
72
|
true
|
56
73
|
end
|
74
|
+
when :union
|
75
|
+
coerce_union(type, val)
|
76
|
+
when :record
|
77
|
+
coerce_record(type, val)
|
57
78
|
else
|
58
79
|
val
|
59
80
|
end
|
@@ -71,6 +71,12 @@ module Deimos
|
|
71
71
|
:mock
|
72
72
|
end
|
73
73
|
|
74
|
+
# The content type to use when encoding / decoding requests over HTTP via ActionController.
|
75
|
+
# @return [String]
|
76
|
+
def self.content_type
|
77
|
+
raise NotImplementedError
|
78
|
+
end
|
79
|
+
|
74
80
|
# Encode a payload. To be defined by subclass.
|
75
81
|
# @param payload [Hash]
|
76
82
|
# @param schema [Symbol|String]
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deimos
|
4
|
+
module Utils
|
5
|
+
# Mixin to automatically decode schema-encoded payloads when given the correct content type,
|
6
|
+
# and provide the `render_schema` method to encode the payload for responses.
|
7
|
+
module SchemaControllerMixin
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
Mime::Type.register('avro/binary', :avro)
|
12
|
+
|
13
|
+
attr_accessor :payload
|
14
|
+
|
15
|
+
if respond_to?(:before_filter)
|
16
|
+
before_filter(:decode_schema, if: :schema_format?)
|
17
|
+
else
|
18
|
+
before_action(:decode_schema, if: :schema_format?)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# :nodoc:
|
23
|
+
module ClassMethods
|
24
|
+
# @return [Hash<String, Hash<Symbol, String>>]
|
25
|
+
def schema_mapping
|
26
|
+
@schema_mapping ||= {}
|
27
|
+
end
|
28
|
+
|
29
|
+
# Indicate which schemas should be assigned to actions.
|
30
|
+
# @param actions [Symbol]
|
31
|
+
# @param request [String]
|
32
|
+
# @param response [String]
|
33
|
+
def schemas(*actions, request: nil, response: nil)
|
34
|
+
actions.each do |action|
|
35
|
+
request ||= action.to_s.titleize
|
36
|
+
response ||= action.to_s.titleize
|
37
|
+
schema_mapping[action.to_s] = { request: request, response: response }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Hash<Symbol, String>]
|
42
|
+
def namespaces
|
43
|
+
@namespaces ||= {}
|
44
|
+
end
|
45
|
+
|
46
|
+
# Set the namespace for both requests and responses.
|
47
|
+
# @param name [String]
|
48
|
+
def namespace(name)
|
49
|
+
request_namespace(name)
|
50
|
+
response_namespace(name)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Set the namespace for requests.
|
54
|
+
# @param name [String]
|
55
|
+
def request_namespace(name)
|
56
|
+
namespaces[:request] = name
|
57
|
+
end
|
58
|
+
|
59
|
+
# Set the namespace for repsonses.
|
60
|
+
# @param name [String]
|
61
|
+
def response_namespace(name)
|
62
|
+
namespaces[:response] = name
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# @return [Boolean]
|
67
|
+
def schema_format?
|
68
|
+
request.content_type == Deimos.schema_backend_class.content_type
|
69
|
+
end
|
70
|
+
|
71
|
+
# Get the namespace from either an existing instance variable, or tease it out of the schema.
|
72
|
+
# @param type [Symbol] :request or :response
|
73
|
+
# @return [Array<String, String>] the namespace and schema.
|
74
|
+
def parse_namespace(type)
|
75
|
+
namespace = self.class.namespaces[type]
|
76
|
+
schema = self.class.schema_mapping[params['action']][type]
|
77
|
+
if schema.nil?
|
78
|
+
raise "No #{type} schema defined for #{params[:controller]}##{params[:action]}!"
|
79
|
+
end
|
80
|
+
|
81
|
+
if namespace.nil?
|
82
|
+
last_period = schema.rindex('.')
|
83
|
+
namespace, schema = schema.split(last_period)
|
84
|
+
end
|
85
|
+
if namespace.nil? || schema.nil?
|
86
|
+
raise "No request namespace defined for #{params[:controller]}##{params[:action]}!"
|
87
|
+
end
|
88
|
+
|
89
|
+
[namespace, schema]
|
90
|
+
end
|
91
|
+
|
92
|
+
# Decode the payload with the parameters.
|
93
|
+
def decode_schema
|
94
|
+
namespace, schema = parse_namespace(:request)
|
95
|
+
decoder = Deimos.schema_backend(schema: schema, namespace: namespace)
|
96
|
+
@payload = decoder.decode(request.body.read).with_indifferent_access
|
97
|
+
request.body.rewind if request.body.respond_to?(:rewind)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Render a hash into a payload as specified by the configured schema and namespace.
|
101
|
+
# @param payload [Hash]
|
102
|
+
def render_schema(payload, schema: nil, namespace: nil)
|
103
|
+
namespace, schema = parse_namespace(:response) if !schema && !namespace
|
104
|
+
encoder = Deimos.schema_backend(schema: schema, namespace: namespace)
|
105
|
+
encoded = encoder.encode(payload)
|
106
|
+
response.headers['Content-Type'] = encoder.class.content_type
|
107
|
+
send_data(encoded)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
data/lib/deimos/version.rb
CHANGED
data/spec/consumer_spec.rb
CHANGED
@@ -32,6 +32,12 @@ module ConsumerTest
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
+
it 'should consume a nil message' do
|
36
|
+
test_consume_message(MyConsumer, nil) do |payload, _metadata|
|
37
|
+
expect(payload).to be_nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
35
41
|
it 'should consume a message idempotently' do
|
36
42
|
# testing for a crash and re-consuming the same message/metadata
|
37
43
|
key = { 'test_id' => 'foo' }
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
describe Deimos::KafkaListener do
|
4
|
+
include_context 'with widgets'
|
5
|
+
|
6
|
+
prepend_before(:each) do
|
7
|
+
producer_class = Class.new(Deimos::Producer) do
|
8
|
+
schema 'MySchema'
|
9
|
+
namespace 'com.my-namespace'
|
10
|
+
topic 'my-topic'
|
11
|
+
key_config none: true
|
12
|
+
end
|
13
|
+
stub_const('MyProducer', producer_class)
|
14
|
+
end
|
15
|
+
|
16
|
+
before(:each) do
|
17
|
+
Deimos.configure do |c|
|
18
|
+
c.producers.backend = :kafka
|
19
|
+
c.schema.backend = :avro_local
|
20
|
+
end
|
21
|
+
allow_any_instance_of(Kafka::Cluster).to receive(:add_target_topics)
|
22
|
+
allow_any_instance_of(Kafka::Cluster).to receive(:partitions_for).
|
23
|
+
and_raise(Kafka::Error)
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '.send_produce_error' do
|
27
|
+
let(:payloads) do
|
28
|
+
[{ 'test_id' => 'foo', 'some_int' => 123 },
|
29
|
+
{ 'test_id' => 'bar', 'some_int' => 124 }]
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should listen to publishing errors and republish as Deimos events' do
|
33
|
+
allow(Deimos::Producer).to receive(:descendants).and_return([MyProducer])
|
34
|
+
Deimos.subscribe('produce_error') do |event|
|
35
|
+
expect(event.payload).to include(
|
36
|
+
producer: MyProducer,
|
37
|
+
topic: 'my-topic',
|
38
|
+
payloads: payloads
|
39
|
+
)
|
40
|
+
end
|
41
|
+
expect(Deimos.config.metrics).to receive(:increment).
|
42
|
+
with('publish_error', tags: %w(topic:my-topic), by: 2)
|
43
|
+
expect { MyProducer.publish_list(payloads) }.to raise_error(Kafka::DeliveryFailed)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should not send any notifications when producer is not found' do
|
47
|
+
Deimos.subscribe('produce_error') do |_|
|
48
|
+
raise 'OH NOES'
|
49
|
+
end
|
50
|
+
allow(Deimos::Producer).to receive(:descendants).and_return([])
|
51
|
+
expect(Deimos.config.metrics).not_to receive(:increment).with('publish_error', anything)
|
52
|
+
expect { MyProducer.publish_list(payloads) }.to raise_error(Kafka::DeliveryFailed)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/spec/producer_spec.rb
CHANGED
@@ -41,6 +41,14 @@ module ProducerTest
|
|
41
41
|
end
|
42
42
|
stub_const('MyNoKeyProducer', producer_class)
|
43
43
|
|
44
|
+
producer_class = Class.new(Deimos::Producer) do
|
45
|
+
schema 'MyNestedSchema'
|
46
|
+
namespace 'com.my-namespace'
|
47
|
+
topic 'my-topic'
|
48
|
+
key_config field: 'test_id'
|
49
|
+
end
|
50
|
+
stub_const('MyNestedSchemaProducer', producer_class)
|
51
|
+
|
44
52
|
producer_class = Class.new(Deimos::Producer) do
|
45
53
|
schema 'MySchema'
|
46
54
|
namespace 'com.my-namespace'
|
@@ -233,6 +241,34 @@ module ProducerTest
|
|
233
241
|
)
|
234
242
|
end
|
235
243
|
|
244
|
+
it 'should properly encode and coerce values with a nested record' do
|
245
|
+
expect(MyNestedSchemaProducer.encoder).to receive(:encode_key).with('test_id', 'foo', topic: 'my-topic-key')
|
246
|
+
MyNestedSchemaProducer.publish(
|
247
|
+
'test_id' => 'foo',
|
248
|
+
'test_float' => BigDecimal('123.456'),
|
249
|
+
'test_array' => ['1'],
|
250
|
+
'some_nested_record' => {
|
251
|
+
'some_int' => 123,
|
252
|
+
'some_float' => BigDecimal('456.789'),
|
253
|
+
'some_string' => '123',
|
254
|
+
'some_optional_int' => nil
|
255
|
+
},
|
256
|
+
'some_optional_record' => nil
|
257
|
+
)
|
258
|
+
expect(MyNestedSchemaProducer.topic).to have_sent(
|
259
|
+
'test_id' => 'foo',
|
260
|
+
'test_float' => 123.456,
|
261
|
+
'test_array' => ['1'],
|
262
|
+
'some_nested_record' => {
|
263
|
+
'some_int' => 123,
|
264
|
+
'some_float' => 456.789,
|
265
|
+
'some_string' => '123',
|
266
|
+
'some_optional_int' => nil
|
267
|
+
},
|
268
|
+
'some_optional_record' => nil
|
269
|
+
)
|
270
|
+
end
|
271
|
+
|
236
272
|
it 'should error with nothing set' do
|
237
273
|
expect {
|
238
274
|
MyErrorProducer.publish_list(
|
@@ -0,0 +1,62 @@
|
|
1
|
+
{
|
2
|
+
"namespace": "com.my-namespace",
|
3
|
+
"name": "MyNestedSchema",
|
4
|
+
"type": "record",
|
5
|
+
"doc": "Test schema",
|
6
|
+
"fields": [
|
7
|
+
{
|
8
|
+
"name": "test_id",
|
9
|
+
"type": "string",
|
10
|
+
"doc": "test string"
|
11
|
+
},
|
12
|
+
{
|
13
|
+
"name": "test_float",
|
14
|
+
"type": "float",
|
15
|
+
"doc": "test float"
|
16
|
+
},
|
17
|
+
{
|
18
|
+
"name": "test_array",
|
19
|
+
"type": {
|
20
|
+
"type": "array",
|
21
|
+
"items": "string"
|
22
|
+
}
|
23
|
+
},
|
24
|
+
{
|
25
|
+
"name": "some_nested_record",
|
26
|
+
"doc": "some nested record",
|
27
|
+
"type": {
|
28
|
+
"name": "MyNestedRecord",
|
29
|
+
"type": "record",
|
30
|
+
"fields": [
|
31
|
+
{
|
32
|
+
"name": "some_int",
|
33
|
+
"type": "int",
|
34
|
+
"doc": "some int"
|
35
|
+
},
|
36
|
+
{
|
37
|
+
"name": "some_float",
|
38
|
+
"type": "float",
|
39
|
+
"doc": "some float"
|
40
|
+
},
|
41
|
+
{
|
42
|
+
"name": "some_string",
|
43
|
+
"type": "string",
|
44
|
+
"doc": "some string"
|
45
|
+
},
|
46
|
+
{
|
47
|
+
"name": "some_optional_int",
|
48
|
+
"type": [ "null", "int" ],
|
49
|
+
"doc": "some optional int",
|
50
|
+
"default": null
|
51
|
+
}
|
52
|
+
]
|
53
|
+
}
|
54
|
+
},
|
55
|
+
{
|
56
|
+
"name": "some_optional_record",
|
57
|
+
"doc": "some optional record",
|
58
|
+
"type": [ "null", "MyNestedRecord" ],
|
59
|
+
"default": null
|
60
|
+
}
|
61
|
+
]
|
62
|
+
}
|
data/spec/spec_helper.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
4
4
|
require 'active_record'
|
5
|
+
require 'action_controller/railtie'
|
5
6
|
require 'database_cleaner'
|
6
7
|
require 'deimos'
|
7
8
|
require 'deimos/metrics/mock'
|
@@ -11,6 +12,11 @@ require 'active_support/testing/time_helpers'
|
|
11
12
|
require 'activerecord-import'
|
12
13
|
require 'handlers/my_batch_consumer'
|
13
14
|
require 'handlers/my_consumer'
|
15
|
+
require 'rspec/rails'
|
16
|
+
|
17
|
+
class DeimosApp < Rails::Application
|
18
|
+
end
|
19
|
+
DeimosApp.initialize!
|
14
20
|
|
15
21
|
# Helpers for Executor/DbProducer
|
16
22
|
module TestRunners
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'deimos/utils/schema_controller_mixin'
|
4
|
+
require 'deimos/schema_backends/avro_local'
|
5
|
+
|
6
|
+
RSpec.describe Deimos::Utils::SchemaControllerMixin, type: :controller do
|
7
|
+
|
8
|
+
before(:each) do
|
9
|
+
Deimos.configure do
|
10
|
+
schema.backend(:avro_local)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
controller(ActionController::Base) do
|
15
|
+
include Deimos::Utils::SchemaControllerMixin # rubocop:disable RSpec/DescribedClass
|
16
|
+
|
17
|
+
request_namespace 'com.my-namespace.request'
|
18
|
+
response_namespace 'com.my-namespace.response'
|
19
|
+
schemas :index, :show
|
20
|
+
schemas :update, request: 'UpdateRequest', response: 'UpdateResponse'
|
21
|
+
|
22
|
+
# :nodoc:
|
23
|
+
def index
|
24
|
+
render_schema({ 'response_id' => payload[:request_id] + ' mom' })
|
25
|
+
end
|
26
|
+
|
27
|
+
# :nodoc:
|
28
|
+
def show
|
29
|
+
render_schema({ 'response_id' => payload[:request_id] + ' dad' })
|
30
|
+
end
|
31
|
+
|
32
|
+
# :nodoc:
|
33
|
+
def update
|
34
|
+
render_schema({ 'update_response_id' => payload[:update_request_id] + ' sis' })
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should render the correct response for index' do
|
39
|
+
request_backend = Deimos.schema_backend(schema: 'Index',
|
40
|
+
namespace: 'com.my-namespace.request')
|
41
|
+
response_backend = Deimos.schema_backend(schema: 'Index',
|
42
|
+
namespace: 'com.my-namespace.response')
|
43
|
+
request.content_type = 'avro/binary'
|
44
|
+
get :index, body: request_backend.encode({ 'request_id' => 'hi' })
|
45
|
+
expect(response_backend.decode(response.body)).to eq({ 'response_id' => 'hi mom' })
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should render the correct response for show' do
|
49
|
+
request_backend = Deimos.schema_backend(schema: 'Index',
|
50
|
+
namespace: 'com.my-namespace.request')
|
51
|
+
response_backend = Deimos.schema_backend(schema: 'Index',
|
52
|
+
namespace: 'com.my-namespace.response')
|
53
|
+
request.content_type = 'avro/binary'
|
54
|
+
get :show, params: { id: 1 }, body: request_backend.encode({ 'request_id' => 'hi' })
|
55
|
+
expect(response_backend.decode(response.body)).to eq({ 'response_id' => 'hi dad' })
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should render the correct response for update' do
|
59
|
+
request_backend = Deimos.schema_backend(schema: 'UpdateRequest',
|
60
|
+
namespace: 'com.my-namespace.request')
|
61
|
+
response_backend = Deimos.schema_backend(schema: 'UpdateResponse',
|
62
|
+
namespace: 'com.my-namespace.response')
|
63
|
+
request.content_type = 'avro/binary'
|
64
|
+
post :update, params: { id: 1 }, body: request_backend.encode({ 'update_request_id' => 'hi' })
|
65
|
+
expect(response_backend.decode(response.body)).to eq({ 'update_response_id' => 'hi sis' })
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
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.8.1.pre.
|
4
|
+
version: 1.8.1.pre.beta6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Orner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-08-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: avro_turf
|
@@ -66,20 +66,6 @@ dependencies:
|
|
66
66
|
- - '='
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: 0.0.1
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: activerecord
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - "~>"
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '6'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - "~>"
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '6'
|
83
69
|
- !ruby/object:Gem::Dependency
|
84
70
|
name: activerecord-import
|
85
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -276,6 +262,20 @@ dependencies:
|
|
276
262
|
- - "~>"
|
277
263
|
- !ruby/object:Gem::Version
|
278
264
|
version: '0.3'
|
265
|
+
- !ruby/object:Gem::Dependency
|
266
|
+
name: rspec-rails
|
267
|
+
requirement: !ruby/object:Gem::Requirement
|
268
|
+
requirements:
|
269
|
+
- - "~>"
|
270
|
+
- !ruby/object:Gem::Version
|
271
|
+
version: '4'
|
272
|
+
type: :development
|
273
|
+
prerelease: false
|
274
|
+
version_requirements: !ruby/object:Gem::Requirement
|
275
|
+
requirements:
|
276
|
+
- - "~>"
|
277
|
+
- !ruby/object:Gem::Version
|
278
|
+
version: '4'
|
279
279
|
- !ruby/object:Gem::Dependency
|
280
280
|
name: rubocop
|
281
281
|
requirement: !ruby/object:Gem::Requirement
|
@@ -399,6 +399,7 @@ files:
|
|
399
399
|
- lib/deimos/utils/deadlock_retry.rb
|
400
400
|
- lib/deimos/utils/inline_consumer.rb
|
401
401
|
- lib/deimos/utils/lag_reporter.rb
|
402
|
+
- lib/deimos/utils/schema_controller_mixin.rb
|
402
403
|
- lib/deimos/version.rb
|
403
404
|
- lib/generators/deimos/active_record/templates/migration.rb.tt
|
404
405
|
- lib/generators/deimos/active_record/templates/model.rb.tt
|
@@ -427,6 +428,7 @@ files:
|
|
427
428
|
- spec/generators/active_record_generator_spec.rb
|
428
429
|
- spec/handlers/my_batch_consumer.rb
|
429
430
|
- spec/handlers/my_consumer.rb
|
431
|
+
- spec/kafka_listener_spec.rb
|
430
432
|
- spec/kafka_source_spec.rb
|
431
433
|
- spec/kafka_topic_info_spec.rb
|
432
434
|
- spec/message_spec.rb
|
@@ -440,6 +442,7 @@ files:
|
|
440
442
|
- spec/schema_backends/avro_validation_spec.rb
|
441
443
|
- spec/schema_backends/base_spec.rb
|
442
444
|
- spec/schemas/com/my-namespace/Generated.avsc
|
445
|
+
- spec/schemas/com/my-namespace/MyNestedSchema.avsc
|
443
446
|
- spec/schemas/com/my-namespace/MySchema-key.avsc
|
444
447
|
- spec/schemas/com/my-namespace/MySchema.avsc
|
445
448
|
- spec/schemas/com/my-namespace/MySchemaCompound-key.avsc
|
@@ -450,12 +453,17 @@ files:
|
|
450
453
|
- spec/schemas/com/my-namespace/Wibble.avsc
|
451
454
|
- spec/schemas/com/my-namespace/Widget.avsc
|
452
455
|
- spec/schemas/com/my-namespace/WidgetTheSecond.avsc
|
456
|
+
- spec/schemas/com/my-namespace/request/Index.avsc
|
457
|
+
- spec/schemas/com/my-namespace/request/UpdateRequest.avsc
|
458
|
+
- spec/schemas/com/my-namespace/response/Index.avsc
|
459
|
+
- spec/schemas/com/my-namespace/response/UpdateResponse.avsc
|
453
460
|
- spec/spec_helper.rb
|
454
461
|
- spec/utils/db_poller_spec.rb
|
455
462
|
- spec/utils/db_producer_spec.rb
|
456
463
|
- spec/utils/deadlock_retry_spec.rb
|
457
464
|
- spec/utils/lag_reporter_spec.rb
|
458
465
|
- spec/utils/platform_schema_validation_spec.rb
|
466
|
+
- spec/utils/schema_controller_mixin_spec.rb
|
459
467
|
- support/deimos-solo.png
|
460
468
|
- support/deimos-with-name-next.png
|
461
469
|
- support/deimos-with-name.png
|
@@ -501,6 +509,7 @@ test_files:
|
|
501
509
|
- spec/generators/active_record_generator_spec.rb
|
502
510
|
- spec/handlers/my_batch_consumer.rb
|
503
511
|
- spec/handlers/my_consumer.rb
|
512
|
+
- spec/kafka_listener_spec.rb
|
504
513
|
- spec/kafka_source_spec.rb
|
505
514
|
- spec/kafka_topic_info_spec.rb
|
506
515
|
- spec/message_spec.rb
|
@@ -514,6 +523,7 @@ test_files:
|
|
514
523
|
- spec/schema_backends/avro_validation_spec.rb
|
515
524
|
- spec/schema_backends/base_spec.rb
|
516
525
|
- spec/schemas/com/my-namespace/Generated.avsc
|
526
|
+
- spec/schemas/com/my-namespace/MyNestedSchema.avsc
|
517
527
|
- spec/schemas/com/my-namespace/MySchema-key.avsc
|
518
528
|
- spec/schemas/com/my-namespace/MySchema.avsc
|
519
529
|
- spec/schemas/com/my-namespace/MySchemaCompound-key.avsc
|
@@ -524,9 +534,14 @@ test_files:
|
|
524
534
|
- spec/schemas/com/my-namespace/Wibble.avsc
|
525
535
|
- spec/schemas/com/my-namespace/Widget.avsc
|
526
536
|
- spec/schemas/com/my-namespace/WidgetTheSecond.avsc
|
537
|
+
- spec/schemas/com/my-namespace/request/Index.avsc
|
538
|
+
- spec/schemas/com/my-namespace/request/UpdateRequest.avsc
|
539
|
+
- spec/schemas/com/my-namespace/response/Index.avsc
|
540
|
+
- spec/schemas/com/my-namespace/response/UpdateResponse.avsc
|
527
541
|
- spec/spec_helper.rb
|
528
542
|
- spec/utils/db_poller_spec.rb
|
529
543
|
- spec/utils/db_producer_spec.rb
|
530
544
|
- spec/utils/deadlock_retry_spec.rb
|
531
545
|
- spec/utils/lag_reporter_spec.rb
|
532
546
|
- spec/utils/platform_schema_validation_spec.rb
|
547
|
+
- spec/utils/schema_controller_mixin_spec.rb
|