deimos-ruby 2.6.0.pre.beta1 → 2.6.0
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/CHANGELOG.md +8 -2
- data/README.md +45 -20
- data/deimos-ruby.gemspec +0 -1
- data/docs/CONFIGURATION.md +5 -11
- data/lib/deimos/active_record_consume/batch_consumption.rb +128 -31
- data/lib/deimos/active_record_consume/batch_record.rb +3 -0
- data/lib/deimos/active_record_consume/message_consumption.rb +2 -2
- data/lib/deimos/active_record_consumer.rb +6 -1
- data/lib/deimos/active_record_producer.rb +1 -1
- data/lib/deimos/config/configuration.rb +11 -34
- data/lib/deimos/exceptions.rb +17 -0
- data/lib/deimos/ext/consumer_route.rb +3 -1
- data/lib/deimos/ext/producer_middleware.rb +2 -2
- data/lib/deimos/logging.rb +0 -9
- data/lib/deimos/producer.rb +5 -5
- data/lib/deimos/schema_backends/avro_base.rb +36 -7
- data/lib/deimos/schema_class/base.rb +62 -0
- data/lib/deimos/schema_class/enum.rb +49 -0
- data/lib/deimos/schema_class/record.rb +100 -0
- data/lib/deimos/transcoder.rb +6 -6
- data/lib/deimos/utils/deadlock_retry.rb +16 -2
- data/lib/deimos/utils/schema_class.rb +47 -14
- data/lib/deimos/version.rb +1 -1
- data/lib/deimos.rb +4 -3
- data/lib/generators/deimos/schema_class/templates/schema_class.rb.tt +8 -0
- data/lib/generators/deimos/schema_class/templates/schema_enum.rb.tt +13 -0
- data/lib/generators/deimos/schema_class/templates/schema_record.rb.tt +102 -0
- data/lib/generators/deimos/schema_class_generator.rb +369 -0
- data/lib/tasks/deimos.rake +4 -16
- data/regenerate_test_schema_classes.rb +3 -12
- data/spec/active_record_batch_consumer_association_spec.rb +1 -1
- data/spec/active_record_batch_consumer_spec.rb +286 -1
- data/spec/active_record_consume/batch_consumption_spec.rb +1 -1
- data/spec/active_record_consumer_spec.rb +3 -3
- data/spec/active_record_producer_spec.rb +1 -1
- data/spec/batch_consumer_spec.rb +2 -2
- data/spec/consumer_spec.rb +5 -5
- data/spec/generators/schema_class/my_schema_spec.rb +16 -0
- data/spec/generators/schema_class/my_schema_with_circular_reference_spec.rb +98 -0
- data/spec/generators/schema_class/my_schema_with_complex_types_spec.rb +237 -0
- data/spec/generators/schema_class_generator_spec.rb +283 -0
- data/spec/schemas/my_namespace/generated.rb +4 -4
- data/spec/schemas/my_namespace/my_long_namespace_schema.rb +2 -2
- data/spec/schemas/my_namespace/my_nested_schema.rb +3 -3
- data/spec/schemas/my_namespace/my_schema.rb +2 -2
- data/spec/schemas/my_namespace/my_schema_compound_key.rb +2 -2
- data/spec/schemas/my_namespace/my_schema_id_key.rb +2 -2
- data/spec/schemas/my_namespace/my_schema_key.rb +2 -2
- data/spec/schemas/my_namespace/my_schema_with_boolean.rb +2 -2
- data/spec/schemas/my_namespace/my_schema_with_circular_reference.rb +3 -3
- data/spec/schemas/my_namespace/my_schema_with_complex_type.rb +6 -6
- data/spec/schemas/my_namespace/my_schema_with_date_time.rb +2 -2
- data/spec/schemas/my_namespace/my_schema_with_id.rb +2 -2
- data/spec/schemas/my_namespace/my_schema_with_title.rb +2 -2
- data/spec/schemas/my_namespace/my_schema_with_union_type.rb +6 -6
- data/spec/schemas/my_namespace/my_schema_with_unique_id.rb +2 -2
- data/spec/schemas/my_namespace/my_updated_schema.rb +1 -1
- data/spec/schemas/my_namespace/request/create_topic.rb +2 -2
- data/spec/schemas/my_namespace/request/index.rb +2 -2
- data/spec/schemas/my_namespace/request/update_request.rb +2 -2
- data/spec/schemas/my_namespace/response/create_topic.rb +2 -2
- data/spec/schemas/my_namespace/response/index.rb +2 -2
- data/spec/schemas/my_namespace/response/update_response.rb +2 -2
- data/spec/schemas/my_namespace/wibble.rb +2 -2
- data/spec/schemas/my_namespace/widget.rb +2 -2
- data/spec/schemas/my_namespace/widget_the_second.rb +2 -2
- data/spec/schemas/my_namespace/widget_the_third.rb +2 -2
- data/spec/snapshots/consumers-no-nest.snap +1740 -0
- data/spec/snapshots/consumers.snap +1720 -0
- data/spec/snapshots/consumers_and_producers-no-nest.snap +1740 -0
- data/spec/snapshots/consumers_and_producers.snap +1720 -0
- data/spec/snapshots/consumers_circular-no-nest.snap +1740 -0
- data/spec/snapshots/consumers_circular.snap +1720 -0
- data/spec/snapshots/consumers_complex_types-no-nest.snap +1740 -0
- data/spec/snapshots/consumers_complex_types.snap +1720 -0
- data/spec/snapshots/consumers_nested-no-nest.snap +1740 -0
- data/spec/snapshots/consumers_nested.snap +1720 -0
- data/spec/snapshots/namespace_folders.snap +1800 -0
- data/spec/snapshots/namespace_map.snap +1800 -0
- data/spec/snapshots/producers_with_key-no-nest.snap +1740 -0
- data/spec/snapshots/producers_with_key.snap +1720 -0
- data/spec/spec_helper.rb +2 -2
- data/spec/utils/db_poller_spec.rb +1 -1
- data/spec/utils/deadlock_retry_spec.rb +57 -0
- metadata +27 -18
- data/lib/deimos/schema_class.rb +0 -72
- data/spec/schema_class_spec.rb +0 -65
data/lib/deimos/producer.rb
CHANGED
|
@@ -75,7 +75,7 @@ module Deimos
|
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
# Publish the payload to the topic.
|
|
78
|
-
# @param payload [Hash,
|
|
78
|
+
# @param payload [Hash, SchemaClass::Record] with an optional payload_key hash key.
|
|
79
79
|
# @param topic [String] if specifying the topic
|
|
80
80
|
# @param headers [Hash] if specifying headers
|
|
81
81
|
# @return [void]
|
|
@@ -99,7 +99,7 @@ module Deimos
|
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
# Publish a list of messages.
|
|
102
|
-
# @param payloads [Array<Hash,
|
|
102
|
+
# @param payloads [Array<Hash, SchemaClass::Record>] with optional payload_key hash key.
|
|
103
103
|
# @param sync [Boolean] if given, override the default setting of
|
|
104
104
|
# whether to publish synchronously.
|
|
105
105
|
# @param force_send [Boolean] if true, ignore the configured backend
|
|
@@ -112,7 +112,7 @@ module Deimos
|
|
|
112
112
|
|
|
113
113
|
messages = Array(payloads).map do |p|
|
|
114
114
|
payload = p
|
|
115
|
-
payload = payload.to_h if p.is_a?(
|
|
115
|
+
payload = payload.to_h if p.is_a?(SchemaClass::Record)
|
|
116
116
|
m = {
|
|
117
117
|
payload: payload,
|
|
118
118
|
headers: headers,
|
|
@@ -121,10 +121,10 @@ module Deimos
|
|
|
121
121
|
}
|
|
122
122
|
if payload.is_a?(Hash) && payload.key?(:key) && payload.key?(:message)
|
|
123
123
|
m[:key] = payload[:key]
|
|
124
|
-
m[:key] = m[:key].to_h if m[:key].nil? || m[:key].is_a?(
|
|
124
|
+
m[:key] = m[:key].to_h if m[:key].nil? || m[:key].is_a?(SchemaClass::Record)
|
|
125
125
|
m[:payload] = payload[:message]
|
|
126
126
|
m[:payload] = m[:payload].to_h if m[:payload].nil? ||
|
|
127
|
-
m[:payload].is_a?(
|
|
127
|
+
m[:payload].is_a?(SchemaClass::Record)
|
|
128
128
|
end
|
|
129
129
|
m
|
|
130
130
|
end
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'base'
|
|
4
4
|
require 'schema_registry_client'
|
|
5
|
-
require 'avro_gen/avro_parser'
|
|
6
5
|
require_relative 'avro_schema_coercer'
|
|
7
6
|
|
|
8
7
|
module Deimos
|
|
@@ -107,25 +106,55 @@ module Deimos
|
|
|
107
106
|
|
|
108
107
|
# @param schema [Avro::Schema::NamedSchema] A named schema
|
|
109
108
|
# @return [String]
|
|
110
|
-
# @deprecated Use AvroGen::AvroParser.schema_classname instead.
|
|
111
109
|
def self.schema_classname(schema)
|
|
112
|
-
|
|
110
|
+
schema.name.underscore.camelize.singularize
|
|
113
111
|
end
|
|
114
112
|
|
|
115
113
|
# Converts Avro::Schema::NamedSchema's to String form for generated YARD docs.
|
|
114
|
+
# Recursively handles the typing for Arrays, Maps and Unions.
|
|
116
115
|
# @param avro_schema [Avro::Schema::NamedSchema]
|
|
117
116
|
# @return [String] A string representation of the Type of this SchemaField
|
|
118
|
-
# @deprecated Use AvroGen::AvroParser.field_type instead.
|
|
119
117
|
def self.field_type(avro_schema)
|
|
120
|
-
|
|
118
|
+
case avro_schema.type_sym
|
|
119
|
+
when :string, :boolean
|
|
120
|
+
avro_schema.type_sym.to_s.titleize
|
|
121
|
+
when :int, :long
|
|
122
|
+
'Integer'
|
|
123
|
+
when :float, :double
|
|
124
|
+
'Float'
|
|
125
|
+
when :record, :enum
|
|
126
|
+
schema_classname(avro_schema)
|
|
127
|
+
when :array
|
|
128
|
+
arr_t = field_type(Deimos::SchemaField.new('n/a', avro_schema.items).type)
|
|
129
|
+
"Array<#{arr_t}>"
|
|
130
|
+
when :map
|
|
131
|
+
map_t = field_type(Deimos::SchemaField.new('n/a', avro_schema.values).type)
|
|
132
|
+
"Hash<String, #{map_t}>"
|
|
133
|
+
when :union
|
|
134
|
+
types = avro_schema.schemas.map do |t|
|
|
135
|
+
field_type(Deimos::SchemaField.new('n/a', t).type)
|
|
136
|
+
end
|
|
137
|
+
types.join(', ')
|
|
138
|
+
when :null
|
|
139
|
+
'nil'
|
|
140
|
+
end
|
|
121
141
|
end
|
|
122
142
|
|
|
123
143
|
# Returns the base type of this schema. Decodes Arrays, Maps and Unions
|
|
124
144
|
# @param schema [Avro::Schema::NamedSchema]
|
|
125
145
|
# @return [Avro::Schema::NamedSchema]
|
|
126
|
-
# @deprecated Use AvroGen::AvroParser.schema_base_class instead.
|
|
127
146
|
def self.schema_base_class(schema)
|
|
128
|
-
|
|
147
|
+
case schema.type_sym
|
|
148
|
+
when :array
|
|
149
|
+
schema_base_class(schema.items)
|
|
150
|
+
when :map
|
|
151
|
+
schema_base_class(schema.values)
|
|
152
|
+
when :union
|
|
153
|
+
schema.schemas.map(&method(:schema_base_class)).
|
|
154
|
+
reject { |s| s.type_sym == :null }.first
|
|
155
|
+
else
|
|
156
|
+
schema
|
|
157
|
+
end
|
|
129
158
|
end
|
|
130
159
|
|
|
131
160
|
def generate_key_schema(field_name)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Deimos
|
|
6
|
+
module SchemaClass
|
|
7
|
+
# Base Class for Schema Classes generated from Avro.
|
|
8
|
+
class Base
|
|
9
|
+
|
|
10
|
+
# @param _args [Array<Object>]
|
|
11
|
+
def initialize(*_args)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Converts the object to a hash which can be used for debugging or comparing objects.
|
|
15
|
+
# @param _opts [Hash]
|
|
16
|
+
# @return [Hash] a hash representation of the payload
|
|
17
|
+
def as_json(_opts={})
|
|
18
|
+
raise MissingImplementationError
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @param key [String,Symbol]
|
|
22
|
+
# @param val [Object]
|
|
23
|
+
# @return [void]
|
|
24
|
+
def []=(key, val)
|
|
25
|
+
self.send("#{key}=", val)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @param other [SchemaClass::Base]
|
|
29
|
+
# @return [Boolean]
|
|
30
|
+
def ==(other)
|
|
31
|
+
comparison = other
|
|
32
|
+
if other.class == self.class
|
|
33
|
+
comparison = other.as_json
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
comparison == self.as_json
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
alias_method :eql?, :==
|
|
40
|
+
|
|
41
|
+
# @return [String]
|
|
42
|
+
def inspect
|
|
43
|
+
klass = self.class
|
|
44
|
+
"#{klass}(#{self.as_json})"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Initializes this class from a given value
|
|
48
|
+
# @param value [Object]
|
|
49
|
+
# @return [SchemaClass::Base]
|
|
50
|
+
def self.initialize_from_value(_value)
|
|
51
|
+
raise MissingImplementationError
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
protected
|
|
55
|
+
|
|
56
|
+
# @return [Integer]
|
|
57
|
+
def hash
|
|
58
|
+
as_json.hash
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Deimos
|
|
7
|
+
module SchemaClass
|
|
8
|
+
# Base Class for Enum Classes generated from Avro.
|
|
9
|
+
class Enum < Base
|
|
10
|
+
|
|
11
|
+
# @return [String]
|
|
12
|
+
attr_accessor :value
|
|
13
|
+
|
|
14
|
+
# @param other [Deimos::SchemaClass::Enum]
|
|
15
|
+
# @return [Boolean]
|
|
16
|
+
def ==(other)
|
|
17
|
+
other.is_a?(self.class) ? other.value == @value : other == @value
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [String]
|
|
21
|
+
def to_s
|
|
22
|
+
@value.to_s
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param value [String]
|
|
26
|
+
def initialize(value)
|
|
27
|
+
@value = value
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns all the valid symbols for this enum.
|
|
31
|
+
# @return [Array<String>]
|
|
32
|
+
def symbols
|
|
33
|
+
raise MissingImplementationError
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [String]
|
|
37
|
+
def as_json(_opts={})
|
|
38
|
+
@value
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [SchemaClass::Enum]
|
|
42
|
+
def self.initialize_from_value(value, from_message: false)
|
|
43
|
+
return nil if value.nil?
|
|
44
|
+
|
|
45
|
+
value.is_a?(self) ? value : self.new(value)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Deimos
|
|
7
|
+
module SchemaClass
|
|
8
|
+
# Base Class of Record Classes generated from Avro.
|
|
9
|
+
class Record < Base
|
|
10
|
+
|
|
11
|
+
attr_accessor :tombstone_key, :_from_message
|
|
12
|
+
|
|
13
|
+
# Converts the object attributes to a hash which can be used for Kafka
|
|
14
|
+
# @return [Hash] the payload as a hash.
|
|
15
|
+
def to_h
|
|
16
|
+
if self.tombstone_key
|
|
17
|
+
{ payload_key: self.tombstone_key&.as_json }
|
|
18
|
+
else
|
|
19
|
+
self.as_json
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Merge a hash or an identical schema object with this one and return a new object.
|
|
24
|
+
# @param other_hash [Hash,SchemaClass::Base]
|
|
25
|
+
# @return [SchemaClass::Base]
|
|
26
|
+
def merge(other_hash)
|
|
27
|
+
obj = self.class.new(**self.to_h.symbolize_keys)
|
|
28
|
+
other_hash.to_h.each do |k, v|
|
|
29
|
+
obj.send("#{k}=", v)
|
|
30
|
+
end
|
|
31
|
+
obj
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Element access method as if this Object were a hash
|
|
35
|
+
# @param key[String,Symbol]
|
|
36
|
+
# @return [Object] The value of the attribute if exists, nil otherwise
|
|
37
|
+
def [](key)
|
|
38
|
+
self.try(key.to_sym)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [SchemaClass::Record]
|
|
42
|
+
def with_indifferent_access
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns the schema name of the inheriting class.
|
|
47
|
+
# @return [String]
|
|
48
|
+
def schema
|
|
49
|
+
raise MissingImplementationError
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns the namespace for the schema of the inheriting class.
|
|
53
|
+
# @return [String]
|
|
54
|
+
def namespace
|
|
55
|
+
raise MissingImplementationError
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns the full schema name of the inheriting class.
|
|
59
|
+
# @return [String]
|
|
60
|
+
def full_schema
|
|
61
|
+
"#{namespace}.#{schema}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Returns the schema validator from the schema backend
|
|
65
|
+
# @return [Deimos::SchemaBackends::Base]
|
|
66
|
+
def validator
|
|
67
|
+
Deimos.schema_backend(schema: schema, namespace: namespace)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @return [Array<String>] an array of fields names in the schema.
|
|
71
|
+
def schema_fields
|
|
72
|
+
validator.schema_fields.map(&:name)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Used internally within Deimos so that we don't crash on unknown fields that come from
|
|
76
|
+
# a backwards compatible schema.
|
|
77
|
+
# @param kwargs [Hash] the attributes to set on the new object.
|
|
78
|
+
# @return [SchemaClass::Record]
|
|
79
|
+
def self.new_from_message(**kwargs)
|
|
80
|
+
record = self.new
|
|
81
|
+
attrs = kwargs.select { |k, _v| record.respond_to?("#{k}=") }
|
|
82
|
+
self.new(_from_message: true, **attrs)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @return [SchemaClass::Record]
|
|
86
|
+
# @param from_message [Boolean] whether it's being initialized from a real Avro message.
|
|
87
|
+
def self.initialize_from_value(value, from_message: false)
|
|
88
|
+
return nil if value.nil?
|
|
89
|
+
|
|
90
|
+
return value if value.is_a?(self)
|
|
91
|
+
|
|
92
|
+
if from_message
|
|
93
|
+
self.new_from_message(**value&.symbolize_keys || {})
|
|
94
|
+
else
|
|
95
|
+
self.new(**value&.symbolize_keys || {})
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
data/lib/deimos/transcoder.rb
CHANGED
|
@@ -58,9 +58,9 @@ module Deimos
|
|
|
58
58
|
|
|
59
59
|
schema_key = decoded_key.is_a?(Hash) ? decoded_key : { self.key_field => decoded_key }
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
Utils::SchemaClass.instance(schema_key,
|
|
62
|
+
"#{@schema}_key",
|
|
63
|
+
@namespace)
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
# @param payload [String]
|
|
@@ -71,9 +71,9 @@ module Deimos
|
|
|
71
71
|
decoded_payload = self.backend.decode(payload)
|
|
72
72
|
return decoded_payload if !@use_schema_classes || !decoded_payload.is_a?(Hash)
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
Utils::SchemaClass.instance(decoded_payload,
|
|
75
|
+
@schema,
|
|
76
|
+
@namespace)
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
# @param payload [Object]
|
|
@@ -21,6 +21,15 @@ module Deimos
|
|
|
21
21
|
'deadlock detected'
|
|
22
22
|
].freeze
|
|
23
23
|
|
|
24
|
+
# Whether the given exception is a deadlock or lock wait timeout, i.e. transient
|
|
25
|
+
# contention on the database rather than a problem with the data being written.
|
|
26
|
+
# @param error [Exception]
|
|
27
|
+
# @return [Boolean]
|
|
28
|
+
def deadlock?(error)
|
|
29
|
+
error.is_a?(ActiveRecord::StatementInvalid) &&
|
|
30
|
+
DEADLOCK_MESSAGES.any? { |m| error.message.include?(m) }
|
|
31
|
+
end
|
|
32
|
+
|
|
24
33
|
# Retry the given block when encountering a deadlock. For any other
|
|
25
34
|
# exceptions, they are reraised. This is used to handle cases where
|
|
26
35
|
# the database may be busy but the transaction would succeed if
|
|
@@ -28,10 +37,15 @@ module Deimos
|
|
|
28
37
|
# be wrapped in a transaction.
|
|
29
38
|
# Sleeps for a random number of seconds to prevent multiple transactions
|
|
30
39
|
# from retrying at the same time.
|
|
31
|
-
# @param tags [Array] Tags to attach when logging and reporting metrics.
|
|
40
|
+
# @param tags [Array, String, nil] Tags to attach when logging and reporting metrics.
|
|
41
|
+
# A scalar (e.g. a topic String) is coerced to a single-element array.
|
|
32
42
|
# @yield Yields to the block that may deadlock.
|
|
33
43
|
# @return [void]
|
|
34
44
|
def wrap(tags=[])
|
|
45
|
+
# Normalize to an Array so downstream logging/metrics (and dogstatsd's
|
|
46
|
+
# `tags.to_a`) never receive a bare String, which would raise
|
|
47
|
+
# NoMethodError inside this rescue and defeat the retry mechanism.
|
|
48
|
+
tags = Array(tags)
|
|
35
49
|
count = RETRY_COUNT
|
|
36
50
|
|
|
37
51
|
begin
|
|
@@ -40,7 +54,7 @@ module Deimos
|
|
|
40
54
|
end
|
|
41
55
|
rescue ActiveRecord::StatementInvalid => e
|
|
42
56
|
# Reraise if not a known deadlock
|
|
43
|
-
raise
|
|
57
|
+
raise unless deadlock?(e)
|
|
44
58
|
|
|
45
59
|
# Reraise if all retries exhausted
|
|
46
60
|
raise if count <= 0
|
|
@@ -1,27 +1,60 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'avro_gen'
|
|
4
|
-
|
|
5
3
|
module Deimos
|
|
6
4
|
module Utils
|
|
7
|
-
#
|
|
8
|
-
# AvroGen::SchemaClass. This delegates with a deprecation warning.
|
|
5
|
+
# Class used by SchemaClassGenerator and Consumer/Producer interfaces
|
|
9
6
|
module SchemaClass
|
|
10
7
|
class << self
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
|
|
9
|
+
# @param namespace [String]
|
|
10
|
+
# @return [Array<String>]
|
|
11
|
+
def modules_for(namespace)
|
|
12
|
+
modules = ['Schemas']
|
|
13
|
+
namespace_override = nil
|
|
14
|
+
module_namespace = namespace
|
|
15
|
+
|
|
16
|
+
if Deimos.config.schema.use_full_namespace
|
|
17
|
+
if Deimos.config.schema.schema_namespace_map.present?
|
|
18
|
+
namespace_keys = Deimos.config.schema.schema_namespace_map.keys.sort_by { |k| -k.length }
|
|
19
|
+
namespace_override = namespace_keys.find { |k| module_namespace.include?(k) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
if namespace_override.present?
|
|
23
|
+
# override default module
|
|
24
|
+
modules = Array(Deimos.config.schema.schema_namespace_map[namespace_override])
|
|
25
|
+
module_namespace = module_namespace.gsub(/#{namespace_override}\.?/, '')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
namespace_folders = module_namespace.split('.').map { |f| f.underscore.camelize }
|
|
29
|
+
modules.concat(namespace_folders) if namespace_folders.any?
|
|
19
30
|
end
|
|
31
|
+
|
|
32
|
+
modules
|
|
20
33
|
end
|
|
21
34
|
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
# Converts a raw payload into an instance of the Schema Class
|
|
36
|
+
# @param payload [Hash, Deimos::SchemaClass::Base]
|
|
37
|
+
# @param schema [String]
|
|
38
|
+
# @param namespace [String]
|
|
39
|
+
# @return [Deimos::SchemaClass::Record]
|
|
40
|
+
def instance(payload, schema, namespace='')
|
|
41
|
+
return payload if payload.is_a?(Deimos::SchemaClass::Base)
|
|
42
|
+
|
|
43
|
+
klass = klass(schema, namespace)
|
|
44
|
+
return payload if klass.nil? || payload.nil?
|
|
45
|
+
|
|
46
|
+
klass.new_from_message(**payload.symbolize_keys)
|
|
24
47
|
end
|
|
48
|
+
|
|
49
|
+
# Determine and return the SchemaClass with the provided schema and namespace
|
|
50
|
+
# @param schema [String]
|
|
51
|
+
# @param namespace [String]
|
|
52
|
+
# @return [Deimos::SchemaClass]
|
|
53
|
+
def klass(schema, namespace)
|
|
54
|
+
constants = modules_for(namespace) + [schema.underscore.camelize.singularize]
|
|
55
|
+
constants.join('::').safe_constantize
|
|
56
|
+
end
|
|
57
|
+
|
|
25
58
|
end
|
|
26
59
|
end
|
|
27
60
|
end
|
data/lib/deimos/version.rb
CHANGED
data/lib/deimos.rb
CHANGED
|
@@ -17,9 +17,9 @@ require 'deimos/backends/kafka_async'
|
|
|
17
17
|
require 'deimos/backends/test'
|
|
18
18
|
|
|
19
19
|
require 'deimos/schema_backends/base'
|
|
20
|
-
require 'avro_gen'
|
|
21
|
-
require 'deimos/schema_class'
|
|
22
20
|
require 'deimos/utils/schema_class'
|
|
21
|
+
require 'deimos/schema_class/enum'
|
|
22
|
+
require 'deimos/schema_class/record'
|
|
23
23
|
|
|
24
24
|
require 'deimos/ext/schema_route'
|
|
25
25
|
require 'deimos/ext/consumer_route'
|
|
@@ -49,6 +49,7 @@ module Deimos
|
|
|
49
49
|
deimos.encode_message
|
|
50
50
|
deimos.batch_consumption.invalid_records
|
|
51
51
|
deimos.batch_consumption.valid_records
|
|
52
|
+
deimos.batch_consumption.initial_failure
|
|
52
53
|
deimos.outbox.produce
|
|
53
54
|
).freeze
|
|
54
55
|
|
|
@@ -102,7 +103,7 @@ module Deimos
|
|
|
102
103
|
# Initialize an instance of the provided schema
|
|
103
104
|
# in the event the schema class is an override, the inherited
|
|
104
105
|
# schema and namespace will be applied
|
|
105
|
-
schema_class =
|
|
106
|
+
schema_class = Utils::SchemaClass.klass(schema, namespace)
|
|
106
107
|
if schema_class.nil?
|
|
107
108
|
schema_backend_class(backend: backend).
|
|
108
109
|
new(
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Autogenerated Schema for Enum at <%= @current_schema.namespace %>.<%= @current_schema.name %>
|
|
2
|
+
class <%= Deimos::SchemaBackends::AvroBase.schema_classname(@current_schema) %> < Deimos::SchemaClass::Enum
|
|
3
|
+
# @override
|
|
4
|
+
def symbols
|
|
5
|
+
%w(<%= @current_schema.symbols.join(' ') %>)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
<% @current_schema.symbols.each do |symbol| %>
|
|
9
|
+
def <%= symbol.underscore %>?
|
|
10
|
+
@value == '<%= symbol %>'
|
|
11
|
+
end
|
|
12
|
+
<% end %>
|
|
13
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Autogenerated Schema for Record at <%= @current_schema.namespace %>.<%= @current_schema.name %>
|
|
2
|
+
class <%= Deimos::SchemaBackends::AvroBase.schema_classname(@current_schema) %> < Deimos::SchemaClass::Record
|
|
3
|
+
<% if @sub_schema_templates.present? -%>
|
|
4
|
+
|
|
5
|
+
### Secondary Schema Classes ###
|
|
6
|
+
<%- @sub_schema_templates.each do |schema_template| -%>
|
|
7
|
+
<%=- schema_template.gsub(/^/, " ") %>
|
|
8
|
+
|
|
9
|
+
<%- end -%>
|
|
10
|
+
<% end -%>
|
|
11
|
+
|
|
12
|
+
<%- if @field_assignments.select{ |h| h[:is_schema_class] }.any? -%>
|
|
13
|
+
### Attribute Readers ###
|
|
14
|
+
<%- @field_assignments.select{ |h| h[:is_schema_class] }.each do |method_definition| -%>
|
|
15
|
+
# @return [<%= method_definition[:deimos_type] %>]
|
|
16
|
+
attr_reader :<%= method_definition[:field].name %>
|
|
17
|
+
<%- end -%>
|
|
18
|
+
|
|
19
|
+
<% end -%>
|
|
20
|
+
<%- if @field_assignments.select{ |h| !h[:is_schema_class] }.any? -%>
|
|
21
|
+
### Attribute Accessors ###
|
|
22
|
+
<%- @field_assignments.select{ |h| !h[:is_schema_class] }.each do |method_definition| -%>
|
|
23
|
+
# @return [<%= method_definition[:deimos_type] %>]
|
|
24
|
+
attr_accessor :<%= method_definition[:field].name %>
|
|
25
|
+
<%- end -%>
|
|
26
|
+
|
|
27
|
+
<% end -%>
|
|
28
|
+
<%- if @field_assignments.select{ |h| h[:is_schema_class] }.any? -%>
|
|
29
|
+
### Attribute Writers ###
|
|
30
|
+
<%- @field_assignments.select{ |h| h[:is_schema_class] }.each do |method_definition| -%>
|
|
31
|
+
# @return [<%= method_definition[:deimos_type] %>]
|
|
32
|
+
def <%= method_definition[:field].name %>=(<%= method_definition[:method_argument] %>)
|
|
33
|
+
<%- if method_definition[:field_type] == :array -%>
|
|
34
|
+
@<%= method_definition[:field].name %> = values&.map do |value|
|
|
35
|
+
<%= method_definition[:field_initialization] %>
|
|
36
|
+
end
|
|
37
|
+
<%- elsif method_definition[:field_type] == :map -%>
|
|
38
|
+
@<%= method_definition[:field].name %> = values&.transform_values do |value|
|
|
39
|
+
<%= method_definition[:field_initialization] %>
|
|
40
|
+
end
|
|
41
|
+
<%- else -%>
|
|
42
|
+
@<%= method_definition[:field].name %> = <%= method_definition[:field_initialization] %>
|
|
43
|
+
<%- end -%>
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
<%- end -%>
|
|
47
|
+
<% end -%>
|
|
48
|
+
<%- if @field_assignments.select{ |h| h[:is_complex_union] }.any? -%>
|
|
49
|
+
<%- @field_assignments.select{ |h| h[:is_complex_union] }.each do |method_definition| -%>
|
|
50
|
+
# Helper method to determine which schema type to use for <%= method_definition[:field].name %>
|
|
51
|
+
# @param value [Hash, nil]
|
|
52
|
+
# @param from_message [Boolean] whether this was initialized from a real Avro message
|
|
53
|
+
# @return [Object, nil]
|
|
54
|
+
def initialize_<%= method_definition[:field].name %>_type(value, from_message: false)
|
|
55
|
+
return nil if value.nil?
|
|
56
|
+
|
|
57
|
+
klass = [<%= method_definition[:field].type.schemas.reject { |s| s.type_sym == :null }.select { |s| s.type_sym == :record }.map { |s| Deimos::SchemaBackends::AvroBase.schema_classname(s) }.join(', ') %>].find do |candidate|
|
|
58
|
+
fields = candidate.new.as_json.keys
|
|
59
|
+
(value.keys - fields).empty?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
klass.initialize_from_value(value, from_message: self._from_message)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
<%- end -%>
|
|
66
|
+
<% end -%>
|
|
67
|
+
# @override
|
|
68
|
+
<%= @initialization_definition %>
|
|
69
|
+
@_from_message = _from_message
|
|
70
|
+
super
|
|
71
|
+
<%- @fields.each do |field| -%>
|
|
72
|
+
self.<%= field.name %> = <%= field.name %>
|
|
73
|
+
<% end -%>
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @override
|
|
77
|
+
def schema
|
|
78
|
+
'<%= @current_schema.name %>'
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @override
|
|
82
|
+
def namespace
|
|
83
|
+
'<%= @current_schema.namespace %>'
|
|
84
|
+
end
|
|
85
|
+
<%- if @tombstone_assignment %>
|
|
86
|
+
def self.tombstone(key)
|
|
87
|
+
record = self.allocate
|
|
88
|
+
<%- if @tombstone_assignment.present? -%>
|
|
89
|
+
<%= @tombstone_assignment %>
|
|
90
|
+
<%- end -%>
|
|
91
|
+
record
|
|
92
|
+
end
|
|
93
|
+
<%- end %>
|
|
94
|
+
# @override
|
|
95
|
+
def as_json(_opts={})
|
|
96
|
+
{
|
|
97
|
+
<%- @fields.each do |field| -%>
|
|
98
|
+
<%= field_as_json(field) %>
|
|
99
|
+
<% end -%>
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
end
|