deimos-temp-fork 0.0.1
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 +83 -0
- data/.gitignore +41 -0
- data/.gitmodules +0 -0
- data/.rspec +1 -0
- data/.rubocop.yml +333 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +349 -0
- data/CODE_OF_CONDUCT.md +77 -0
- data/Dockerfile +23 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +286 -0
- data/Guardfile +22 -0
- data/LICENSE.md +195 -0
- data/README.md +1099 -0
- data/Rakefile +13 -0
- data/bin/deimos +4 -0
- data/deimos-ruby.gemspec +44 -0
- data/docker-compose.yml +71 -0
- data/docs/ARCHITECTURE.md +140 -0
- data/docs/CONFIGURATION.md +236 -0
- data/docs/DATABASE_BACKEND.md +147 -0
- data/docs/INTEGRATION_TESTS.md +52 -0
- data/docs/PULL_REQUEST_TEMPLATE.md +35 -0
- data/docs/UPGRADING.md +128 -0
- data/lib/deimos-temp-fork.rb +95 -0
- data/lib/deimos/active_record_consume/batch_consumption.rb +164 -0
- data/lib/deimos/active_record_consume/batch_slicer.rb +27 -0
- data/lib/deimos/active_record_consume/message_consumption.rb +79 -0
- data/lib/deimos/active_record_consume/schema_model_converter.rb +52 -0
- data/lib/deimos/active_record_consumer.rb +67 -0
- data/lib/deimos/active_record_producer.rb +87 -0
- data/lib/deimos/backends/base.rb +32 -0
- data/lib/deimos/backends/db.rb +41 -0
- data/lib/deimos/backends/kafka.rb +33 -0
- data/lib/deimos/backends/kafka_async.rb +33 -0
- data/lib/deimos/backends/test.rb +20 -0
- data/lib/deimos/batch_consumer.rb +7 -0
- data/lib/deimos/config/configuration.rb +381 -0
- data/lib/deimos/config/phobos_config.rb +137 -0
- data/lib/deimos/consume/batch_consumption.rb +150 -0
- data/lib/deimos/consume/message_consumption.rb +94 -0
- data/lib/deimos/consumer.rb +104 -0
- data/lib/deimos/instrumentation.rb +76 -0
- data/lib/deimos/kafka_message.rb +60 -0
- data/lib/deimos/kafka_source.rb +128 -0
- data/lib/deimos/kafka_topic_info.rb +102 -0
- data/lib/deimos/message.rb +79 -0
- data/lib/deimos/metrics/datadog.rb +47 -0
- data/lib/deimos/metrics/mock.rb +39 -0
- data/lib/deimos/metrics/provider.rb +36 -0
- data/lib/deimos/monkey_patches/phobos_cli.rb +35 -0
- data/lib/deimos/monkey_patches/phobos_producer.rb +51 -0
- data/lib/deimos/poll_info.rb +9 -0
- data/lib/deimos/producer.rb +224 -0
- data/lib/deimos/railtie.rb +8 -0
- data/lib/deimos/schema_backends/avro_base.rb +140 -0
- data/lib/deimos/schema_backends/avro_local.rb +30 -0
- data/lib/deimos/schema_backends/avro_schema_coercer.rb +119 -0
- data/lib/deimos/schema_backends/avro_schema_registry.rb +34 -0
- data/lib/deimos/schema_backends/avro_validation.rb +21 -0
- data/lib/deimos/schema_backends/base.rb +150 -0
- data/lib/deimos/schema_backends/mock.rb +42 -0
- data/lib/deimos/shared_config.rb +63 -0
- data/lib/deimos/test_helpers.rb +360 -0
- data/lib/deimos/tracing/datadog.rb +35 -0
- data/lib/deimos/tracing/mock.rb +40 -0
- data/lib/deimos/tracing/provider.rb +29 -0
- data/lib/deimos/utils/db_poller.rb +150 -0
- data/lib/deimos/utils/db_producer.rb +243 -0
- data/lib/deimos/utils/deadlock_retry.rb +68 -0
- data/lib/deimos/utils/inline_consumer.rb +150 -0
- data/lib/deimos/utils/lag_reporter.rb +175 -0
- data/lib/deimos/utils/schema_controller_mixin.rb +115 -0
- data/lib/deimos/version.rb +5 -0
- data/lib/generators/deimos/active_record/templates/migration.rb.tt +28 -0
- data/lib/generators/deimos/active_record/templates/model.rb.tt +5 -0
- data/lib/generators/deimos/active_record_generator.rb +79 -0
- data/lib/generators/deimos/db_backend/templates/migration +25 -0
- data/lib/generators/deimos/db_backend/templates/rails3_migration +31 -0
- data/lib/generators/deimos/db_backend_generator.rb +48 -0
- data/lib/generators/deimos/db_poller/templates/migration +11 -0
- data/lib/generators/deimos/db_poller/templates/rails3_migration +16 -0
- data/lib/generators/deimos/db_poller_generator.rb +48 -0
- data/lib/tasks/deimos.rake +34 -0
- data/spec/active_record_batch_consumer_spec.rb +481 -0
- data/spec/active_record_consume/batch_slicer_spec.rb +42 -0
- data/spec/active_record_consume/schema_model_converter_spec.rb +105 -0
- data/spec/active_record_consumer_spec.rb +154 -0
- data/spec/active_record_producer_spec.rb +85 -0
- data/spec/backends/base_spec.rb +10 -0
- data/spec/backends/db_spec.rb +54 -0
- data/spec/backends/kafka_async_spec.rb +11 -0
- data/spec/backends/kafka_spec.rb +11 -0
- data/spec/batch_consumer_spec.rb +256 -0
- data/spec/config/configuration_spec.rb +248 -0
- data/spec/consumer_spec.rb +209 -0
- data/spec/deimos_spec.rb +169 -0
- data/spec/generators/active_record_generator_spec.rb +56 -0
- data/spec/handlers/my_batch_consumer.rb +10 -0
- data/spec/handlers/my_consumer.rb +10 -0
- data/spec/kafka_listener_spec.rb +55 -0
- data/spec/kafka_source_spec.rb +381 -0
- data/spec/kafka_topic_info_spec.rb +111 -0
- data/spec/message_spec.rb +19 -0
- data/spec/phobos.bad_db.yml +73 -0
- data/spec/phobos.yml +77 -0
- data/spec/producer_spec.rb +498 -0
- data/spec/rake_spec.rb +19 -0
- data/spec/schema_backends/avro_base_shared.rb +199 -0
- data/spec/schema_backends/avro_local_spec.rb +32 -0
- data/spec/schema_backends/avro_schema_registry_spec.rb +32 -0
- data/spec/schema_backends/avro_validation_spec.rb +24 -0
- data/spec/schema_backends/base_spec.rb +33 -0
- data/spec/schemas/com/my-namespace/Generated.avsc +71 -0
- data/spec/schemas/com/my-namespace/MyNestedSchema.avsc +62 -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/MySchemaCompound-key.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/Wibble.avsc +43 -0
- data/spec/schemas/com/my-namespace/Widget.avsc +27 -0
- data/spec/schemas/com/my-namespace/WidgetTheSecond.avsc +27 -0
- data/spec/schemas/com/my-namespace/request/CreateTopic.avsc +11 -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/CreateTopic.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 +267 -0
- data/spec/utils/db_poller_spec.rb +320 -0
- data/spec/utils/db_producer_spec.rb +514 -0
- data/spec/utils/deadlock_retry_spec.rb +74 -0
- data/spec/utils/inline_consumer_spec.rb +31 -0
- data/spec/utils/lag_reporter_spec.rb +76 -0
- data/spec/utils/platform_schema_validation_spec.rb +0 -0
- data/spec/utils/schema_controller_mixin_spec.rb +84 -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 +551 -0
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deimos
|
4
|
+
module Utils
|
5
|
+
# Utility class to retry a given block if a a deadlock is encountered.
|
6
|
+
# Supports Postgres and MySQL deadlocks and lock wait timeouts.
|
7
|
+
class DeadlockRetry
|
8
|
+
class << self
|
9
|
+
# Maximum number of times to retry the block after encountering a deadlock
|
10
|
+
RETRY_COUNT = 2
|
11
|
+
|
12
|
+
# Need to match on error messages to support older Rails versions
|
13
|
+
DEADLOCK_MESSAGES = [
|
14
|
+
# MySQL
|
15
|
+
'Deadlock found when trying to get lock',
|
16
|
+
'Lock wait timeout exceeded',
|
17
|
+
|
18
|
+
# Postgres
|
19
|
+
'deadlock detected'
|
20
|
+
].freeze
|
21
|
+
|
22
|
+
# Retry the given block when encountering a deadlock. For any other
|
23
|
+
# exceptions, they are reraised. This is used to handle cases where
|
24
|
+
# the database may be busy but the transaction would succeed if
|
25
|
+
# retried later. Note that your block should be idempotent and it will
|
26
|
+
# be wrapped in a transaction.
|
27
|
+
# Sleeps for a random number of seconds to prevent multiple transactions
|
28
|
+
# from retrying at the same time.
|
29
|
+
# @param tags [Array] Tags to attach when logging and reporting metrics.
|
30
|
+
# @yield Yields to the block that may deadlock.
|
31
|
+
def wrap(tags=[])
|
32
|
+
count = RETRY_COUNT
|
33
|
+
|
34
|
+
begin
|
35
|
+
ActiveRecord::Base.transaction do
|
36
|
+
yield
|
37
|
+
end
|
38
|
+
rescue ActiveRecord::StatementInvalid => e
|
39
|
+
# Reraise if not a known deadlock
|
40
|
+
raise if DEADLOCK_MESSAGES.none? { |m| e.message.include?(m) }
|
41
|
+
|
42
|
+
# Reraise if all retries exhausted
|
43
|
+
raise if count <= 0
|
44
|
+
|
45
|
+
Deimos.config.logger.warn(
|
46
|
+
message: 'Deadlock encountered when trying to execute query. '\
|
47
|
+
"Retrying. #{count} attempt(s) remaining",
|
48
|
+
tags: tags
|
49
|
+
)
|
50
|
+
|
51
|
+
Deimos.config.metrics&.increment(
|
52
|
+
'deadlock',
|
53
|
+
tags: tags
|
54
|
+
)
|
55
|
+
|
56
|
+
count -= 1
|
57
|
+
|
58
|
+
# Sleep for a random amount so that if there are multiple
|
59
|
+
# transactions deadlocking, they don't all retry at the same time
|
60
|
+
sleep(Random.rand(5.0) + 0.5)
|
61
|
+
|
62
|
+
retry
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Class to consume messages. Can be used with integration testing frameworks.
|
4
|
+
# Assumes that you have a topic with only one partition.
|
5
|
+
module Deimos
|
6
|
+
module Utils
|
7
|
+
# Listener that can seek to get the last X messages in a topic.
|
8
|
+
class SeekListener < Phobos::Listener
|
9
|
+
MAX_SEEK_RETRIES = 3
|
10
|
+
attr_accessor :num_messages
|
11
|
+
|
12
|
+
# :nodoc:
|
13
|
+
def start_listener
|
14
|
+
@num_messages ||= 10
|
15
|
+
@consumer = create_kafka_consumer
|
16
|
+
@consumer.subscribe(topic, @subscribe_opts)
|
17
|
+
attempt = 0
|
18
|
+
|
19
|
+
begin
|
20
|
+
attempt += 1
|
21
|
+
last_offset = @kafka_client.last_offset_for(topic, 0)
|
22
|
+
offset = last_offset - num_messages
|
23
|
+
if offset.positive?
|
24
|
+
Deimos.config.logger.info("Seeking to #{offset}")
|
25
|
+
@consumer.seek(topic, 0, offset)
|
26
|
+
end
|
27
|
+
rescue StandardError => e
|
28
|
+
if attempt < MAX_SEEK_RETRIES
|
29
|
+
sleep(1.seconds * attempt)
|
30
|
+
retry
|
31
|
+
end
|
32
|
+
log_error("Could not seek to offset: #{e.message} after #{MAX_SEEK_RETRIES} retries", listener_metadata)
|
33
|
+
end
|
34
|
+
|
35
|
+
instrument('listener.start_handler', listener_metadata) do
|
36
|
+
@handler_class.start(@kafka_client)
|
37
|
+
end
|
38
|
+
log_info('Listener started', listener_metadata)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Class to return the messages consumed.
|
43
|
+
class MessageBankHandler < Deimos::Consumer
|
44
|
+
include Phobos::Handler
|
45
|
+
|
46
|
+
cattr_accessor :total_messages
|
47
|
+
|
48
|
+
# @param klass [Class < Deimos::Consumer]
|
49
|
+
def self.config_class=(klass)
|
50
|
+
self.config.merge!(klass.config)
|
51
|
+
end
|
52
|
+
|
53
|
+
# :nodoc:
|
54
|
+
def self.start(_kafka_client)
|
55
|
+
self.total_messages = []
|
56
|
+
end
|
57
|
+
|
58
|
+
# :nodoc:
|
59
|
+
def consume(payload, metadata)
|
60
|
+
self.class.total_messages << {
|
61
|
+
key: metadata[:key],
|
62
|
+
payload: payload
|
63
|
+
}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Class which can process/consume messages inline.
|
68
|
+
class InlineConsumer
|
69
|
+
MAX_MESSAGE_WAIT_TIME = 1.second
|
70
|
+
MAX_TOPIC_WAIT_TIME = 10.seconds
|
71
|
+
|
72
|
+
# Get the last X messages from a topic. You can specify a subclass of
|
73
|
+
# Deimos::Consumer or Deimos::Producer, or provide the
|
74
|
+
# schema, namespace and key_config directly.
|
75
|
+
# @param topic [String]
|
76
|
+
# @param config_class [Class < Deimos::Consumer|Deimos::Producer>]
|
77
|
+
# @param schema [String]
|
78
|
+
# @param namespace [String]
|
79
|
+
# @param key_config [Hash]
|
80
|
+
# @param num_messages [Number]
|
81
|
+
# @return [Array<Hash>]
|
82
|
+
def self.get_messages_for(topic:, schema: nil, namespace: nil, key_config: nil,
|
83
|
+
config_class: nil, num_messages: 10)
|
84
|
+
if config_class
|
85
|
+
MessageBankHandler.config_class = config_class
|
86
|
+
elsif schema.nil? || key_config.nil?
|
87
|
+
raise 'You must specify either a config_class or a schema, namespace and key_config!'
|
88
|
+
else
|
89
|
+
MessageBankHandler.class_eval do
|
90
|
+
schema schema
|
91
|
+
namespace namespace
|
92
|
+
key_config key_config
|
93
|
+
@decoder = nil
|
94
|
+
@key_decoder = nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
self.consume(topic: topic,
|
98
|
+
frk_consumer: MessageBankHandler,
|
99
|
+
num_messages: num_messages)
|
100
|
+
messages = MessageBankHandler.total_messages
|
101
|
+
messages.size <= num_messages ? messages : messages[-num_messages..-1]
|
102
|
+
end
|
103
|
+
|
104
|
+
# Consume the last X messages from a topic.
|
105
|
+
# @param topic [String]
|
106
|
+
# @param frk_consumer [Class]
|
107
|
+
# @param num_messages [Integer] If this number is >= the number
|
108
|
+
# of messages in the topic, all messages will be consumed.
|
109
|
+
def self.consume(topic:, frk_consumer:, num_messages: 10)
|
110
|
+
listener = SeekListener.new(
|
111
|
+
handler: frk_consumer,
|
112
|
+
group_id: SecureRandom.hex,
|
113
|
+
topic: topic,
|
114
|
+
heartbeat_interval: 1
|
115
|
+
)
|
116
|
+
listener.num_messages = num_messages
|
117
|
+
|
118
|
+
# Add the start_time and last_message_time attributes to the
|
119
|
+
# consumer class so we can kill it if it's gone on too long
|
120
|
+
class << frk_consumer
|
121
|
+
attr_accessor :start_time, :last_message_time
|
122
|
+
end
|
123
|
+
|
124
|
+
subscribers = []
|
125
|
+
subscribers << ActiveSupport::Notifications.
|
126
|
+
subscribe('phobos.listener.process_message') do
|
127
|
+
frk_consumer.last_message_time = Time.zone.now
|
128
|
+
end
|
129
|
+
subscribers << ActiveSupport::Notifications.
|
130
|
+
subscribe('phobos.listener.start_handler') do
|
131
|
+
frk_consumer.start_time = Time.zone.now
|
132
|
+
frk_consumer.last_message_time = nil
|
133
|
+
end
|
134
|
+
subscribers << ActiveSupport::Notifications.
|
135
|
+
subscribe('heartbeat.consumer.kafka') do
|
136
|
+
if frk_consumer.last_message_time
|
137
|
+
if Time.zone.now - frk_consumer.last_message_time > MAX_MESSAGE_WAIT_TIME
|
138
|
+
raise Phobos::AbortError
|
139
|
+
end
|
140
|
+
elsif Time.zone.now - frk_consumer.start_time > MAX_TOPIC_WAIT_TIME
|
141
|
+
Deimos.config.logger.error('Aborting - initial wait too long')
|
142
|
+
raise Phobos::AbortError
|
143
|
+
end
|
144
|
+
end
|
145
|
+
listener.start
|
146
|
+
subscribers.each { |s| ActiveSupport::Notifications.unsubscribe(s) }
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'mutex_m'
|
4
|
+
|
5
|
+
# :nodoc:
|
6
|
+
module Deimos
|
7
|
+
module Utils
|
8
|
+
# Class that manages reporting lag.
|
9
|
+
class LagReporter
|
10
|
+
extend Mutex_m
|
11
|
+
|
12
|
+
# Class that has a list of topics
|
13
|
+
class ConsumerGroup
|
14
|
+
# @return [Hash<String, Topic>]
|
15
|
+
attr_accessor :topics
|
16
|
+
# @return [String]
|
17
|
+
attr_accessor :id
|
18
|
+
|
19
|
+
# @param id [String]
|
20
|
+
def initialize(id)
|
21
|
+
self.id = id
|
22
|
+
self.topics = {}
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param topic [String]
|
26
|
+
# @param partition [Integer]
|
27
|
+
def report_lag(topic, partition)
|
28
|
+
self.topics[topic.to_s] ||= Topic.new(topic, self)
|
29
|
+
self.topics[topic.to_s].report_lag(partition)
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param topic [String]
|
33
|
+
# @param partition [Integer]
|
34
|
+
# @param offset [Integer]
|
35
|
+
def assign_current_offset(topic, partition, offset)
|
36
|
+
self.topics[topic.to_s] ||= Topic.new(topic, self)
|
37
|
+
self.topics[topic.to_s].assign_current_offset(partition, offset)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Topic which has a hash of partition => last known current offsets
|
42
|
+
class Topic
|
43
|
+
# @return [String]
|
44
|
+
attr_accessor :topic_name
|
45
|
+
# @return [Hash<Integer, Integer>]
|
46
|
+
attr_accessor :partition_current_offsets
|
47
|
+
# @return [ConsumerGroup]
|
48
|
+
attr_accessor :consumer_group
|
49
|
+
|
50
|
+
# @param topic_name [String]
|
51
|
+
# @param group [ConsumerGroup]
|
52
|
+
def initialize(topic_name, group)
|
53
|
+
self.topic_name = topic_name
|
54
|
+
self.consumer_group = group
|
55
|
+
self.partition_current_offsets = {}
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param partition [Integer]
|
59
|
+
def assign_current_offset(partition, offset)
|
60
|
+
self.partition_current_offsets[partition.to_i] = offset
|
61
|
+
end
|
62
|
+
|
63
|
+
# @param partition [Integer]
|
64
|
+
def compute_lag(partition, offset)
|
65
|
+
begin
|
66
|
+
client = Phobos.create_kafka_client
|
67
|
+
last_offset = client.last_offset_for(self.topic_name, partition)
|
68
|
+
lag = last_offset - offset
|
69
|
+
rescue StandardError # don't do anything, just wait
|
70
|
+
Deimos.config.logger.
|
71
|
+
debug("Error computing lag for #{self.topic_name}, will retry")
|
72
|
+
end
|
73
|
+
lag || 0
|
74
|
+
end
|
75
|
+
|
76
|
+
# @param partition [Integer]
|
77
|
+
def report_lag(partition)
|
78
|
+
current_offset = self.partition_current_offsets[partition.to_i]
|
79
|
+
return unless current_offset
|
80
|
+
|
81
|
+
lag = compute_lag(partition, current_offset)
|
82
|
+
group = self.consumer_group.id
|
83
|
+
Deimos.config.logger.
|
84
|
+
debug("Sending lag: #{group}/#{partition}: #{lag}")
|
85
|
+
Deimos.config.metrics&.gauge('consumer_lag', lag, tags: %W(
|
86
|
+
consumer_group:#{group}
|
87
|
+
partition:#{partition}
|
88
|
+
topic:#{self.topic_name}
|
89
|
+
))
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
@groups = {}
|
94
|
+
|
95
|
+
class << self
|
96
|
+
# Reset all group information.
|
97
|
+
def reset
|
98
|
+
@groups = {}
|
99
|
+
end
|
100
|
+
|
101
|
+
# offset_lag = event.payload.fetch(:offset_lag)
|
102
|
+
# group_id = event.payload.fetch(:group_id)
|
103
|
+
# topic = event.payload.fetch(:topic)
|
104
|
+
# partition = event.payload.fetch(:partition)
|
105
|
+
# @param payload [Hash]
|
106
|
+
def message_processed(payload)
|
107
|
+
offset = payload[:offset] || payload[:last_offset]
|
108
|
+
topic = payload[:topic]
|
109
|
+
group = payload[:group_id]
|
110
|
+
partition = payload[:partition]
|
111
|
+
|
112
|
+
synchronize do
|
113
|
+
@groups[group.to_s] ||= ConsumerGroup.new(group)
|
114
|
+
@groups[group.to_s].assign_current_offset(topic, partition, offset)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# @param payload [Hash]
|
119
|
+
def offset_seek(payload)
|
120
|
+
offset = payload[:offset]
|
121
|
+
topic = payload[:topic]
|
122
|
+
group = payload[:group_id]
|
123
|
+
partition = payload[:partition]
|
124
|
+
|
125
|
+
synchronize do
|
126
|
+
@groups[group.to_s] ||= ConsumerGroup.new(group)
|
127
|
+
@groups[group.to_s].assign_current_offset(topic, partition, offset)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# @param payload [Hash]
|
132
|
+
def heartbeat(payload)
|
133
|
+
group = payload[:group_id]
|
134
|
+
synchronize do
|
135
|
+
@groups[group.to_s] ||= ConsumerGroup.new(group)
|
136
|
+
consumer_group = @groups[group.to_s]
|
137
|
+
payload[:topic_partitions].each do |topic, partitions|
|
138
|
+
partitions.each do |partition|
|
139
|
+
consumer_group.report_lag(topic, partition)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
ActiveSupport::Notifications.subscribe('start_process_message.consumer.kafka') do |*args|
|
149
|
+
next unless Deimos.config.consumers.report_lag
|
150
|
+
|
151
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
152
|
+
Deimos::Utils::LagReporter.message_processed(event.payload)
|
153
|
+
end
|
154
|
+
|
155
|
+
ActiveSupport::Notifications.subscribe('start_process_batch.consumer.kafka') do |*args|
|
156
|
+
next unless Deimos.config.consumers.report_lag
|
157
|
+
|
158
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
159
|
+
Deimos::Utils::LagReporter.message_processed(event.payload)
|
160
|
+
end
|
161
|
+
|
162
|
+
ActiveSupport::Notifications.subscribe('seek.consumer.kafka') do |*args|
|
163
|
+
next unless Deimos.config.consumers.report_lag
|
164
|
+
|
165
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
166
|
+
Deimos::Utils::LagReporter.offset_seek(event.payload)
|
167
|
+
end
|
168
|
+
|
169
|
+
ActiveSupport::Notifications.subscribe('heartbeat.consumer.kafka') do |*args|
|
170
|
+
next unless Deimos.config.consumers.report_lag
|
171
|
+
|
172
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
173
|
+
Deimos::Utils::LagReporter.heartbeat(event.payload)
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,115 @@
|
|
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 kwactions [String]
|
32
|
+
# @param request [String]
|
33
|
+
# @param response [String]
|
34
|
+
def schemas(*actions, request: nil, response: nil, **kwactions)
|
35
|
+
actions.each do |action|
|
36
|
+
request ||= action.to_s.titleize
|
37
|
+
response ||= action.to_s.titleize
|
38
|
+
schema_mapping[action.to_s] = { request: request, response: response }
|
39
|
+
end
|
40
|
+
kwactions.each do |key, val|
|
41
|
+
schema_mapping[key.to_s] = { request: val, response: val }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [Hash<Symbol, String>]
|
46
|
+
def namespaces
|
47
|
+
@namespaces ||= {}
|
48
|
+
end
|
49
|
+
|
50
|
+
# Set the namespace for both requests and responses.
|
51
|
+
# @param name [String]
|
52
|
+
def namespace(name)
|
53
|
+
request_namespace(name)
|
54
|
+
response_namespace(name)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Set the namespace for requests.
|
58
|
+
# @param name [String]
|
59
|
+
def request_namespace(name)
|
60
|
+
namespaces[:request] = name
|
61
|
+
end
|
62
|
+
|
63
|
+
# Set the namespace for repsonses.
|
64
|
+
# @param name [String]
|
65
|
+
def response_namespace(name)
|
66
|
+
namespaces[:response] = name
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Boolean]
|
71
|
+
def schema_format?
|
72
|
+
request.content_type == Deimos.schema_backend_class.content_type
|
73
|
+
end
|
74
|
+
|
75
|
+
# Get the namespace from either an existing instance variable, or tease it out of the schema.
|
76
|
+
# @param type [Symbol] :request or :response
|
77
|
+
# @return [Array<String, String>] the namespace and schema.
|
78
|
+
def parse_namespace(type)
|
79
|
+
namespace = self.class.namespaces[type]
|
80
|
+
schema = self.class.schema_mapping[params['action']][type]
|
81
|
+
if schema.nil?
|
82
|
+
raise "No #{type} schema defined for #{params[:controller]}##{params[:action]}!"
|
83
|
+
end
|
84
|
+
|
85
|
+
if namespace.nil?
|
86
|
+
last_period = schema.rindex('.')
|
87
|
+
namespace, schema = schema.split(last_period)
|
88
|
+
end
|
89
|
+
if namespace.nil? || schema.nil?
|
90
|
+
raise "No request namespace defined for #{params[:controller]}##{params[:action]}!"
|
91
|
+
end
|
92
|
+
|
93
|
+
[namespace, schema]
|
94
|
+
end
|
95
|
+
|
96
|
+
# Decode the payload with the parameters.
|
97
|
+
def decode_schema
|
98
|
+
namespace, schema = parse_namespace(:request)
|
99
|
+
decoder = Deimos.schema_backend(schema: schema, namespace: namespace)
|
100
|
+
@payload = decoder.decode(request.body.read).with_indifferent_access
|
101
|
+
request.body.rewind if request.body.respond_to?(:rewind)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Render a hash into a payload as specified by the configured schema and namespace.
|
105
|
+
# @param payload [Hash]
|
106
|
+
def render_schema(payload, schema: nil, namespace: nil)
|
107
|
+
namespace, schema = parse_namespace(:response) if !schema && !namespace
|
108
|
+
encoder = Deimos.schema_backend(schema: schema, namespace: namespace)
|
109
|
+
encoded = encoder.encode(payload)
|
110
|
+
response.headers['Content-Type'] = encoder.class.content_type
|
111
|
+
send_data(encoded)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|