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,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/notifications'
|
4
|
+
require 'active_support/concern'
|
5
|
+
|
6
|
+
# :nodoc:
|
7
|
+
module Deimos
|
8
|
+
# Copied from Phobos instrumentation.
|
9
|
+
module Instrumentation
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
NAMESPACE = 'Deimos'
|
12
|
+
|
13
|
+
# :nodoc:
|
14
|
+
module ClassMethods
|
15
|
+
# :nodoc:
|
16
|
+
def subscribe(event)
|
17
|
+
ActiveSupport::Notifications.subscribe("#{NAMESPACE}.#{event}") do |*args|
|
18
|
+
yield(ActiveSupport::Notifications::Event.new(*args)) if block_given?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# :nodoc:
|
23
|
+
def unsubscribe(subscriber)
|
24
|
+
ActiveSupport::Notifications.unsubscribe(subscriber)
|
25
|
+
end
|
26
|
+
|
27
|
+
# :nodoc:
|
28
|
+
def instrument(event, extra={})
|
29
|
+
ActiveSupport::Notifications.instrument("#{NAMESPACE}.#{event}", extra) do |extra2|
|
30
|
+
yield(extra2) if block_given?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
include Instrumentation
|
37
|
+
|
38
|
+
# This module listens to events published by RubyKafka.
|
39
|
+
module KafkaListener
|
40
|
+
# Listens for any exceptions that happen during publishing and re-publishes
|
41
|
+
# as a Deimos event.
|
42
|
+
# @param event [ActiveSupport::Notification]
|
43
|
+
def self.send_produce_error(event)
|
44
|
+
exception = event.payload[:exception_object]
|
45
|
+
return if !exception || !exception.respond_to?(:failed_messages)
|
46
|
+
|
47
|
+
messages = exception.failed_messages
|
48
|
+
messages.group_by(&:topic).each do |topic, batch|
|
49
|
+
producer = Deimos::Producer.descendants.find { |c| c.topic == topic }
|
50
|
+
next if batch.empty? || !producer
|
51
|
+
|
52
|
+
decoder = Deimos.schema_backend(schema: producer.config[:schema],
|
53
|
+
namespace: producer.config[:namespace])
|
54
|
+
payloads = batch.map { |m| decoder.decode(m.value) }
|
55
|
+
|
56
|
+
Deimos.config.metrics&.increment(
|
57
|
+
'publish_error',
|
58
|
+
tags: %W(topic:#{topic}),
|
59
|
+
by: payloads.size
|
60
|
+
)
|
61
|
+
Deimos.instrument(
|
62
|
+
'produce_error',
|
63
|
+
producer: producer,
|
64
|
+
topic: topic,
|
65
|
+
exception_object: exception,
|
66
|
+
payloads: payloads
|
67
|
+
)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
ActiveSupport::Notifications.subscribe('deliver_messages.producer.kafka') do |*args|
|
73
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
74
|
+
KafkaListener.send_produce_error(event)
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deimos
|
4
|
+
# Store Kafka messages into the database.
|
5
|
+
class KafkaMessage < ActiveRecord::Base
|
6
|
+
self.table_name = 'kafka_messages'
|
7
|
+
|
8
|
+
validates_presence_of :topic
|
9
|
+
|
10
|
+
# Ensure it gets turned into a string, e.g. for testing purposes. It
|
11
|
+
# should already be a string.
|
12
|
+
# @param mess [Object]
|
13
|
+
def message=(mess)
|
14
|
+
write_attribute(:message, mess ? mess.to_s : nil)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Decoded payload for this message.
|
18
|
+
# @return [Hash]
|
19
|
+
def decoded_message
|
20
|
+
self.class.decoded([self]).first
|
21
|
+
end
|
22
|
+
|
23
|
+
# Get a decoder to decode a set of messages on the given topic.
|
24
|
+
# @param topic [String]
|
25
|
+
# @return [Deimos::Consumer]
|
26
|
+
def self.decoder(topic)
|
27
|
+
producer = Deimos::Producer.descendants.find { |c| c.topic == topic }
|
28
|
+
return nil unless producer
|
29
|
+
|
30
|
+
consumer = Class.new(Deimos::Consumer)
|
31
|
+
consumer.config.merge!(producer.config)
|
32
|
+
consumer
|
33
|
+
end
|
34
|
+
|
35
|
+
# Decoded payloads for a list of messages.
|
36
|
+
# @param messages [Array<Deimos::KafkaMessage>]
|
37
|
+
# @return [Array<Hash>]
|
38
|
+
def self.decoded(messages=[])
|
39
|
+
return [] if messages.empty?
|
40
|
+
|
41
|
+
decoder = self.decoder(messages.first.topic)&.new
|
42
|
+
messages.map do |m|
|
43
|
+
{
|
44
|
+
key: m.key.present? ? decoder&.decode_key(m.key) || m.key : nil,
|
45
|
+
payload: decoder&.decoder&.decode(m.message) || m.message
|
46
|
+
}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Hash]
|
51
|
+
def phobos_message
|
52
|
+
{
|
53
|
+
payload: self.message,
|
54
|
+
partition_key: self.partition_key,
|
55
|
+
key: self.key,
|
56
|
+
topic: self.topic
|
57
|
+
}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deimos
|
4
|
+
# Represents an object which needs to inform Kafka when it is saved or
|
5
|
+
# bulk imported.
|
6
|
+
module KafkaSource
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
DEPRECATION_WARNING = 'The kafka_producer interface will be deprecated ' \
|
10
|
+
'in future releases. Please use kafka_producers instead.'
|
11
|
+
|
12
|
+
included do
|
13
|
+
after_create(:send_kafka_event_on_create)
|
14
|
+
after_update(:send_kafka_event_on_update)
|
15
|
+
after_destroy(:send_kafka_event_on_destroy)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Send the newly created model to Kafka.
|
19
|
+
def send_kafka_event_on_create
|
20
|
+
return unless self.persisted?
|
21
|
+
return unless self.class.kafka_config[:create]
|
22
|
+
|
23
|
+
self.class.kafka_producers.each { |p| p.send_event(self) }
|
24
|
+
end
|
25
|
+
|
26
|
+
# Send the newly updated model to Kafka.
|
27
|
+
def send_kafka_event_on_update
|
28
|
+
return unless self.class.kafka_config[:update]
|
29
|
+
|
30
|
+
producers = self.class.kafka_producers
|
31
|
+
fields = producers.flat_map(&:watched_attributes).uniq
|
32
|
+
fields -= ['updated_at']
|
33
|
+
# Only send an event if a field we care about was changed.
|
34
|
+
any_changes = fields.any? do |field|
|
35
|
+
field_change = self.previous_changes[field]
|
36
|
+
field_change.present? && field_change[0] != field_change[1]
|
37
|
+
end
|
38
|
+
return unless any_changes
|
39
|
+
|
40
|
+
producers.each { |p| p.send_event(self) }
|
41
|
+
end
|
42
|
+
|
43
|
+
# Send a deletion (null payload) event to Kafka.
|
44
|
+
def send_kafka_event_on_destroy
|
45
|
+
return unless self.class.kafka_config[:delete]
|
46
|
+
|
47
|
+
self.class.kafka_producers.each { |p| p.publish_list([self.deletion_payload]) }
|
48
|
+
end
|
49
|
+
|
50
|
+
# Payload to send after we are destroyed.
|
51
|
+
# @return [Hash]
|
52
|
+
def deletion_payload
|
53
|
+
{ payload_key: self[self.class.primary_key] }
|
54
|
+
end
|
55
|
+
|
56
|
+
# :nodoc:
|
57
|
+
module ClassMethods
|
58
|
+
# @return [Hash]
|
59
|
+
def kafka_config
|
60
|
+
{
|
61
|
+
update: true,
|
62
|
+
delete: true,
|
63
|
+
import: true,
|
64
|
+
create: true
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [Array<Deimos::ActiveRecordProducer>] the producers to run.
|
69
|
+
def kafka_producers
|
70
|
+
if self.respond_to?(:kafka_producer)
|
71
|
+
Deimos.config.logger.warn(message: DEPRECATION_WARNING)
|
72
|
+
return [self.kafka_producer]
|
73
|
+
end
|
74
|
+
|
75
|
+
raise NotImplementedError
|
76
|
+
end
|
77
|
+
|
78
|
+
# This is an internal method, part of the activerecord_import gem. It's
|
79
|
+
# the one that actually does the importing, having already normalized
|
80
|
+
# the inputs (arrays, hashes, records etc.)
|
81
|
+
# Basically we want to first do the import, then reload the records
|
82
|
+
# and send them to Kafka.
|
83
|
+
def import_without_validations_or_callbacks(column_names,
|
84
|
+
array_of_attributes,
|
85
|
+
options={})
|
86
|
+
results = super
|
87
|
+
if !self.kafka_config[:import] || array_of_attributes.empty?
|
88
|
+
return results
|
89
|
+
end
|
90
|
+
|
91
|
+
# This will contain an array of hashes, where each hash is the actual
|
92
|
+
# attribute hash that created the object.
|
93
|
+
array_of_hashes = []
|
94
|
+
array_of_attributes.each do |array|
|
95
|
+
array_of_hashes << column_names.zip(array).to_h.with_indifferent_access
|
96
|
+
end
|
97
|
+
hashes_with_id, hashes_without_id = array_of_hashes.partition { |arr| arr[:id].present? }
|
98
|
+
|
99
|
+
self.kafka_producers.each { |p| p.send_events(hashes_with_id) }
|
100
|
+
|
101
|
+
if hashes_without_id.any?
|
102
|
+
if options[:on_duplicate_key_update].present? &&
|
103
|
+
options[:on_duplicate_key_update] != [:updated_at]
|
104
|
+
unique_columns = column_names.map(&:to_s) -
|
105
|
+
options[:on_duplicate_key_update].map(&:to_s) - %w(id created_at)
|
106
|
+
records = hashes_without_id.map do |hash|
|
107
|
+
self.where(unique_columns.map { |c| [c, hash[c]] }.to_h).first
|
108
|
+
end
|
109
|
+
self.kafka_producers.each { |p| p.send_events(records) }
|
110
|
+
else
|
111
|
+
# re-fill IDs based on what was just entered into the DB.
|
112
|
+
last_id = if self.connection.adapter_name.downcase =~ /sqlite/
|
113
|
+
self.connection.select_value('select last_insert_rowid()') -
|
114
|
+
hashes_without_id.size + 1
|
115
|
+
else # mysql
|
116
|
+
self.connection.select_value('select LAST_INSERT_ID()')
|
117
|
+
end
|
118
|
+
hashes_without_id.each_with_index do |attrs, i|
|
119
|
+
attrs[:id] = last_id + i
|
120
|
+
end
|
121
|
+
self.kafka_producers.each { |p| p.send_events(hashes_without_id) }
|
122
|
+
end
|
123
|
+
end
|
124
|
+
results
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deimos
|
4
|
+
# Record that keeps track of which topics are being worked on by DbProducers.
|
5
|
+
class KafkaTopicInfo < ActiveRecord::Base
|
6
|
+
self.table_name = 'kafka_topic_info'
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# Lock a topic for the given ID. Returns whether the lock was successful.
|
10
|
+
# @param topic [String]
|
11
|
+
# @param lock_id [String]
|
12
|
+
# @return [Boolean]
|
13
|
+
def lock(topic, lock_id)
|
14
|
+
# Try to create it - it's fine if it already exists
|
15
|
+
begin
|
16
|
+
self.create(topic: topic, last_processed_at: Time.zone.now)
|
17
|
+
rescue ActiveRecord::RecordNotUnique
|
18
|
+
# continue on
|
19
|
+
end
|
20
|
+
|
21
|
+
# Lock the record
|
22
|
+
qtopic = self.connection.quote(topic)
|
23
|
+
qlock_id = self.connection.quote(lock_id)
|
24
|
+
qtable = self.connection.quote_table_name('kafka_topic_info')
|
25
|
+
qnow = self.connection.quote(Time.zone.now.to_s(:db))
|
26
|
+
qfalse = self.connection.quoted_false
|
27
|
+
qtime = self.connection.quote(1.minute.ago.to_s(:db))
|
28
|
+
|
29
|
+
# If a record is marked as error and less than 1 minute old,
|
30
|
+
# we don't want to pick it up even if not currently locked because
|
31
|
+
# we worry we'll run into the same problem again.
|
32
|
+
# Once it's more than 1 minute old, we figure it's OK to try again
|
33
|
+
# so we can pick up any topic that's that old, even if it was
|
34
|
+
# locked by someone, because it's the job of the producer to keep
|
35
|
+
# updating the locked_at timestamp as they work on messages in that
|
36
|
+
# topic. If the locked_at timestamp is that old, chances are that
|
37
|
+
# the producer crashed.
|
38
|
+
sql = <<~SQL
|
39
|
+
UPDATE #{qtable}
|
40
|
+
SET locked_by=#{qlock_id}, locked_at=#{qnow}, error=#{qfalse}
|
41
|
+
WHERE topic=#{qtopic} AND
|
42
|
+
((locked_by IS NULL AND error=#{qfalse}) OR locked_at < #{qtime})
|
43
|
+
SQL
|
44
|
+
self.connection.update(sql)
|
45
|
+
self.where(locked_by: lock_id, topic: topic).any?
|
46
|
+
end
|
47
|
+
|
48
|
+
# This is called once a producer is finished working on a topic, i.e.
|
49
|
+
# there are no more messages to fetch. It unlocks the topic and
|
50
|
+
# moves on to the next one.
|
51
|
+
# @param topic [String]
|
52
|
+
# @param lock_id [String]
|
53
|
+
def clear_lock(topic, lock_id)
|
54
|
+
self.where(topic: topic, locked_by: lock_id).
|
55
|
+
update_all(locked_by: nil,
|
56
|
+
locked_at: nil,
|
57
|
+
error: false,
|
58
|
+
retries: 0,
|
59
|
+
last_processed_at: Time.zone.now)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Update all topics that aren't currently locked and have no messages
|
63
|
+
# waiting. It's OK if some messages get inserted in the middle of this
|
64
|
+
# because the point is that at least within a few milliseconds of each
|
65
|
+
# other, it wasn't locked and had no messages, meaning the topic
|
66
|
+
# was in a good state.
|
67
|
+
# @param except_topics [Array<String>] the list of topics we've just
|
68
|
+
# realized had messages in them, meaning all other topics were empty.
|
69
|
+
def ping_empty_topics(except_topics)
|
70
|
+
records = KafkaTopicInfo.where(locked_by: nil).
|
71
|
+
where('topic not in(?)', except_topics)
|
72
|
+
records.each do |info|
|
73
|
+
info.update_attribute(:last_processed_at, Time.zone.now)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# The producer calls this if it gets an error sending messages. This
|
78
|
+
# essentially locks down this topic for 1 minute (for all producers)
|
79
|
+
# and allows the caller to continue to the next topic.
|
80
|
+
# @param topic [String]
|
81
|
+
# @param lock_id [String]
|
82
|
+
def register_error(topic, lock_id)
|
83
|
+
record = self.where(topic: topic, locked_by: lock_id).last
|
84
|
+
attr_hash = { locked_by: nil,
|
85
|
+
locked_at: Time.zone.now,
|
86
|
+
error: true,
|
87
|
+
retries: record.retries + 1 }
|
88
|
+
record.attributes = attr_hash
|
89
|
+
record.save!
|
90
|
+
end
|
91
|
+
|
92
|
+
# Update the locked_at timestamp to indicate that the producer is still
|
93
|
+
# working on those messages and to continue.
|
94
|
+
# @param topic [String]
|
95
|
+
# @param lock_id [String]
|
96
|
+
def heartbeat(topic, lock_id)
|
97
|
+
self.where(topic: topic, locked_by: lock_id).
|
98
|
+
update_all(locked_at: Time.zone.now)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deimos
|
4
|
+
# Basically a struct to hold the message as it's processed.
|
5
|
+
class Message
|
6
|
+
attr_accessor :payload, :key, :partition_key, :encoded_key,
|
7
|
+
:encoded_payload, :topic, :producer_name
|
8
|
+
|
9
|
+
# @param payload [Hash]
|
10
|
+
# @param producer [Class]
|
11
|
+
def initialize(payload, producer, topic: nil, key: nil, partition_key: nil)
|
12
|
+
@payload = payload&.with_indifferent_access
|
13
|
+
@producer_name = producer&.name
|
14
|
+
@topic = topic
|
15
|
+
@key = key
|
16
|
+
@partition_key = partition_key
|
17
|
+
end
|
18
|
+
|
19
|
+
# Add message_id and timestamp default values if they are in the
|
20
|
+
# schema and don't already have values.
|
21
|
+
# @param fields [Array<String>] existing name fields in the schema.
|
22
|
+
def add_fields(fields)
|
23
|
+
return if @payload.except(:payload_key, :partition_key).blank?
|
24
|
+
|
25
|
+
if fields.include?('message_id')
|
26
|
+
@payload['message_id'] ||= SecureRandom.uuid
|
27
|
+
end
|
28
|
+
if fields.include?('timestamp')
|
29
|
+
@payload['timestamp'] ||= Time.now.in_time_zone.to_s
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param encoder [Deimos::SchemaBackends::Base]
|
34
|
+
def coerce_fields(encoder)
|
35
|
+
return if payload.nil?
|
36
|
+
|
37
|
+
@payload = encoder.coerce(@payload)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Hash]
|
41
|
+
def encoded_hash
|
42
|
+
{
|
43
|
+
topic: @topic,
|
44
|
+
key: @encoded_key,
|
45
|
+
partition_key: @partition_key || @encoded_key,
|
46
|
+
payload: @encoded_payload,
|
47
|
+
metadata: {
|
48
|
+
decoded_payload: @payload,
|
49
|
+
producer_name: @producer_name
|
50
|
+
}
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [Hash]
|
55
|
+
def to_h
|
56
|
+
{
|
57
|
+
topic: @topic,
|
58
|
+
key: @key,
|
59
|
+
partition_key: @partition_key || @key,
|
60
|
+
payload: @payload,
|
61
|
+
metadata: {
|
62
|
+
decoded_payload: @payload,
|
63
|
+
producer_name: @producer_name
|
64
|
+
}
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
# @param other [Message]
|
69
|
+
# @return [Boolean]
|
70
|
+
def ==(other)
|
71
|
+
self.to_h == other.to_h
|
72
|
+
end
|
73
|
+
|
74
|
+
# @return [Boolean] True if this message is a tombstone
|
75
|
+
def tombstone?
|
76
|
+
payload.nil?
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|