deimos-kafka 1.0.0.pre.beta15
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 +74 -0
- data/.gitignore +41 -0
- data/.gitmodules +0 -0
- data/.rspec +1 -0
- data/.rubocop.yml +321 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +9 -0
- data/CODE_OF_CONDUCT.md +77 -0
- data/Dockerfile +23 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +165 -0
- data/Guardfile +22 -0
- data/LICENSE.md +195 -0
- data/README.md +742 -0
- data/Rakefile +13 -0
- data/bin/deimos +4 -0
- data/deimos-kafka.gemspec +42 -0
- data/docker-compose.yml +71 -0
- data/docs/DATABASE_BACKEND.md +147 -0
- data/docs/PULL_REQUEST_TEMPLATE.md +34 -0
- data/lib/deimos.rb +134 -0
- data/lib/deimos/active_record_consumer.rb +81 -0
- data/lib/deimos/active_record_producer.rb +64 -0
- data/lib/deimos/avro_data_coder.rb +89 -0
- data/lib/deimos/avro_data_decoder.rb +36 -0
- data/lib/deimos/avro_data_encoder.rb +51 -0
- data/lib/deimos/backends/db.rb +27 -0
- data/lib/deimos/backends/kafka.rb +27 -0
- data/lib/deimos/backends/kafka_async.rb +27 -0
- data/lib/deimos/configuration.rb +88 -0
- data/lib/deimos/consumer.rb +164 -0
- data/lib/deimos/instrumentation.rb +71 -0
- data/lib/deimos/kafka_message.rb +27 -0
- data/lib/deimos/kafka_source.rb +126 -0
- data/lib/deimos/kafka_topic_info.rb +79 -0
- data/lib/deimos/message.rb +74 -0
- data/lib/deimos/metrics/datadog.rb +47 -0
- data/lib/deimos/metrics/mock.rb +39 -0
- data/lib/deimos/metrics/provider.rb +38 -0
- data/lib/deimos/monkey_patches/phobos_cli.rb +35 -0
- data/lib/deimos/monkey_patches/phobos_producer.rb +51 -0
- data/lib/deimos/monkey_patches/ruby_kafka_heartbeat.rb +85 -0
- data/lib/deimos/monkey_patches/schema_store.rb +19 -0
- data/lib/deimos/producer.rb +218 -0
- data/lib/deimos/publish_backend.rb +30 -0
- data/lib/deimos/railtie.rb +8 -0
- data/lib/deimos/schema_coercer.rb +108 -0
- data/lib/deimos/shared_config.rb +59 -0
- data/lib/deimos/test_helpers.rb +356 -0
- data/lib/deimos/tracing/datadog.rb +35 -0
- data/lib/deimos/tracing/mock.rb +40 -0
- data/lib/deimos/tracing/provider.rb +31 -0
- data/lib/deimos/utils/db_producer.rb +95 -0
- data/lib/deimos/utils/executor.rb +117 -0
- data/lib/deimos/utils/inline_consumer.rb +144 -0
- data/lib/deimos/utils/lag_reporter.rb +182 -0
- data/lib/deimos/utils/platform_schema_validation.rb +0 -0
- data/lib/deimos/utils/signal_handler.rb +68 -0
- data/lib/deimos/version.rb +5 -0
- data/lib/generators/deimos/db_backend/templates/migration +24 -0
- data/lib/generators/deimos/db_backend/templates/rails3_migration +30 -0
- data/lib/generators/deimos/db_backend_generator.rb +48 -0
- data/lib/tasks/deimos.rake +17 -0
- data/spec/active_record_consumer_spec.rb +81 -0
- data/spec/active_record_producer_spec.rb +107 -0
- data/spec/avro_data_decoder_spec.rb +18 -0
- data/spec/avro_data_encoder_spec.rb +37 -0
- data/spec/backends/db_spec.rb +35 -0
- data/spec/backends/kafka_async_spec.rb +11 -0
- data/spec/backends/kafka_spec.rb +11 -0
- data/spec/consumer_spec.rb +169 -0
- data/spec/deimos_spec.rb +117 -0
- data/spec/kafka_source_spec.rb +168 -0
- data/spec/kafka_topic_info_spec.rb +88 -0
- data/spec/phobos.bad_db.yml +73 -0
- data/spec/phobos.yml +73 -0
- data/spec/producer_spec.rb +397 -0
- data/spec/publish_backend_spec.rb +10 -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/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/Widget.avsc +27 -0
- data/spec/schemas/com/my-namespace/WidgetTheSecond.avsc +27 -0
- data/spec/spec_helper.rb +207 -0
- data/spec/updateable_schema_store_spec.rb +36 -0
- data/spec/utils/db_producer_spec.rb +208 -0
- data/spec/utils/executor_spec.rb +42 -0
- data/spec/utils/lag_reporter_spec.rb +69 -0
- data/spec/utils/platform_schema_validation_spec.rb +0 -0
- data/spec/utils/signal_handler_spec.rb +16 -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 +452 -0
@@ -0,0 +1,74 @@
|
|
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 schema [Avro::Schema]
|
22
|
+
def add_fields(schema)
|
23
|
+
return if @payload.except(:payload_key, :partition_key).blank?
|
24
|
+
|
25
|
+
if schema.fields.any? { |f| f.name == 'message_id' }
|
26
|
+
@payload['message_id'] ||= SecureRandom.uuid
|
27
|
+
end
|
28
|
+
if schema.fields.any? { |f| f.name == 'timestamp' }
|
29
|
+
@payload['timestamp'] ||= Time.now.in_time_zone.to_s
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param schema [Avro::Schema]
|
34
|
+
def coerce_fields(schema)
|
35
|
+
return if payload.nil?
|
36
|
+
|
37
|
+
@payload = SchemaCoercer.new(schema).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
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'deimos/metrics/provider'
|
4
|
+
|
5
|
+
module Deimos
|
6
|
+
module Metrics
|
7
|
+
# A Metrics wrapper class for Datadog.
|
8
|
+
class Datadog < Metrics::Provider
|
9
|
+
# :nodoc:
|
10
|
+
def initialize(config, logger)
|
11
|
+
raise 'Metrics config must specify host_ip' if config[:host_ip].nil?
|
12
|
+
raise 'Metrics config must specify host_port' if config[:host_port].nil?
|
13
|
+
raise 'Metrics config must specify namespace' if config[:namespace].nil?
|
14
|
+
|
15
|
+
logger.info("DatadogMetricsProvider configured with: #{config}")
|
16
|
+
@client = Datadog::Statsd.new(
|
17
|
+
config[:host_ip],
|
18
|
+
config[:host_port]
|
19
|
+
)
|
20
|
+
@client.tags = config[:tags]
|
21
|
+
@client.namespace = config[:namespace]
|
22
|
+
end
|
23
|
+
|
24
|
+
# :nodoc:
|
25
|
+
def increment(metric_name, options={})
|
26
|
+
@client.increment(metric_name, options)
|
27
|
+
end
|
28
|
+
|
29
|
+
# :nodoc:
|
30
|
+
def gauge(metric_name, count, options={})
|
31
|
+
@client.gauge(metric_name, count, options)
|
32
|
+
end
|
33
|
+
|
34
|
+
# :nodoc:
|
35
|
+
def histogram(metric_name, count, options={})
|
36
|
+
@client.histogram(metric_name, count, options)
|
37
|
+
end
|
38
|
+
|
39
|
+
# :nodoc:
|
40
|
+
def time(metric_name, options={})
|
41
|
+
@client.time(metric_name, options) do
|
42
|
+
yield
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'deimos/metrics/provider'
|
4
|
+
|
5
|
+
module Deimos
|
6
|
+
module Metrics
|
7
|
+
# A mock Metrics wrapper which just logs the metrics
|
8
|
+
class Mock
|
9
|
+
# :nodoc:
|
10
|
+
def initialize(logger=nil)
|
11
|
+
@logger = logger || Logger.new(STDOUT)
|
12
|
+
@logger.info('MockMetricsProvider initialized')
|
13
|
+
end
|
14
|
+
|
15
|
+
# :nodoc:
|
16
|
+
def increment(metric_name, options)
|
17
|
+
@logger.info("MockMetricsProvider.increment: #{metric_name}, #{options}")
|
18
|
+
end
|
19
|
+
|
20
|
+
# :nodoc:
|
21
|
+
def gauge(metric_name, count, options)
|
22
|
+
@logger.info("MockMetricsProvider.gauge: #{metric_name}, #{count}, #{options}")
|
23
|
+
end
|
24
|
+
|
25
|
+
# :nodoc:
|
26
|
+
def histogram(metric_name, count, options)
|
27
|
+
@logger.info("MockMetricsProvider.histogram: #{metric_name}, #{count}, #{options}")
|
28
|
+
end
|
29
|
+
|
30
|
+
# :nodoc:
|
31
|
+
def time(metric_name, options={})
|
32
|
+
start_time = Time.now
|
33
|
+
yield
|
34
|
+
total_time = (Time.now - start_time).to_i
|
35
|
+
@logger.info("MockMetricsProvider.time: #{metric_name}, #{total_time}, #{options}")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
4
|
+
module Deimos
|
5
|
+
module Metrics
|
6
|
+
# Base class for all metrics providers.
|
7
|
+
class Provider
|
8
|
+
# Send an counter increment metric
|
9
|
+
# @param metric_name [String] The name of the counter metric
|
10
|
+
# @param options [Hash] Any additional options, e.g. :tags
|
11
|
+
def increment(metric_name, options)
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
# Send an counter increment metric
|
16
|
+
# @param metric_name [String] The name of the counter metric
|
17
|
+
# @param options [Hash] Any additional options, e.g. :tags
|
18
|
+
def gauge(metric_name, count, options)
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
# Send an counter increment metric
|
23
|
+
# @param metric_name [String] The name of the counter metric
|
24
|
+
# @param options [Hash] Any additional options, e.g. :tags
|
25
|
+
def histogram(metric_name, count, options)
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
# Time a yielded block, and send a timer metric
|
30
|
+
# @param metric_name [String] The name of the metric
|
31
|
+
# @param options [Hash] Any additional options, e.g. :tags
|
32
|
+
def time(metric_name, options={})
|
33
|
+
raise NotImplementedError
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'phobos/cli/start'
|
4
|
+
|
5
|
+
# :nodoc:
|
6
|
+
module Phobos
|
7
|
+
# :nodoc:
|
8
|
+
module CLI
|
9
|
+
# :nodoc:
|
10
|
+
class Start
|
11
|
+
# :nodoc:
|
12
|
+
def validate_listeners!
|
13
|
+
Phobos.config.listeners.each do |listener|
|
14
|
+
handler = listener.handler
|
15
|
+
begin
|
16
|
+
handler.constantize
|
17
|
+
rescue NameError
|
18
|
+
error_exit("Handler '#{handler}' not defined")
|
19
|
+
end
|
20
|
+
|
21
|
+
delivery = listener.delivery
|
22
|
+
if delivery.nil?
|
23
|
+
Phobos::CLI.logger.warn do
|
24
|
+
Hash(message: "Delivery option should be specified, defaulting to 'batch'"\
|
25
|
+
' - specify this option to silence this message')
|
26
|
+
end
|
27
|
+
elsif !Listener::DELIVERY_OPTS.include?(delivery)
|
28
|
+
error_exit("Invalid delivery option '#{delivery}'. Please specify one of: "\
|
29
|
+
"#{Listener::DELIVERY_OPTS.join(', ')}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'phobos/producer'
|
4
|
+
|
5
|
+
module Phobos
|
6
|
+
module Producer
|
7
|
+
# :nodoc:
|
8
|
+
class PublicAPI
|
9
|
+
# :nodoc:
|
10
|
+
def publish(topic, payload, key=nil, partition_key=nil)
|
11
|
+
class_producer.publish(topic, payload, key, partition_key)
|
12
|
+
end
|
13
|
+
|
14
|
+
# :nodoc:
|
15
|
+
def async_publish(topic, payload, key=nil, partition_key=nil)
|
16
|
+
class_producer.async_publish(topic, payload, key, partition_key)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# :nodoc:
|
21
|
+
module ClassMethods
|
22
|
+
# :nodoc:
|
23
|
+
class PublicAPI
|
24
|
+
# :nodoc:
|
25
|
+
def publish(topic, payload, key=nil, partition_key=nil)
|
26
|
+
publish_list([{ topic: topic, payload: payload, key: key,
|
27
|
+
partition_key: partition_key }])
|
28
|
+
end
|
29
|
+
|
30
|
+
# :nodoc:
|
31
|
+
def async_publish(topic, payload, key=nil, partition_key=nil)
|
32
|
+
async_publish_list([{ topic: topic, payload: payload, key: key,
|
33
|
+
partition_key: partition_key }])
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# :nodoc:
|
39
|
+
def produce_messages(producer, messages)
|
40
|
+
messages.each do |message|
|
41
|
+
partition_key = message[:partition_key] || message[:key]
|
42
|
+
producer.produce(message[:payload],
|
43
|
+
topic: message[:topic],
|
44
|
+
key: message[:key],
|
45
|
+
partition_key: partition_key)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kafka
|
4
|
+
class Heartbeat
|
5
|
+
def initialize(group:, interval:, instrumenter:)
|
6
|
+
@group = group
|
7
|
+
@interval = interval
|
8
|
+
@last_heartbeat = Time.now
|
9
|
+
@instrumenter = instrumenter
|
10
|
+
end
|
11
|
+
|
12
|
+
def trigger!
|
13
|
+
@instrumenter.instrument('heartbeat.consumer',
|
14
|
+
group_id: @group.group_id,
|
15
|
+
topic_partitions: @group.assigned_partitions) do
|
16
|
+
@group.heartbeat
|
17
|
+
@last_heartbeat = Time.now
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Client
|
23
|
+
def consumer(
|
24
|
+
group_id:,
|
25
|
+
session_timeout: 30,
|
26
|
+
offset_commit_interval: 10,
|
27
|
+
offset_commit_threshold: 0,
|
28
|
+
heartbeat_interval: 10,
|
29
|
+
offset_retention_time: nil,
|
30
|
+
fetcher_max_queue_size: 100
|
31
|
+
)
|
32
|
+
cluster = initialize_cluster
|
33
|
+
|
34
|
+
instrumenter = DecoratingInstrumenter.new(@instrumenter,
|
35
|
+
group_id: group_id)
|
36
|
+
|
37
|
+
# The Kafka protocol expects the retention time to be in ms.
|
38
|
+
retention_time = (offset_retention_time && offset_retention_time * 1_000) || -1
|
39
|
+
|
40
|
+
group = ConsumerGroup.new(
|
41
|
+
cluster: cluster,
|
42
|
+
logger: @logger,
|
43
|
+
group_id: group_id,
|
44
|
+
session_timeout: session_timeout,
|
45
|
+
retention_time: retention_time,
|
46
|
+
instrumenter: instrumenter
|
47
|
+
)
|
48
|
+
|
49
|
+
fetcher = Fetcher.new(
|
50
|
+
cluster: initialize_cluster,
|
51
|
+
group: group,
|
52
|
+
logger: @logger,
|
53
|
+
instrumenter: instrumenter,
|
54
|
+
max_queue_size: fetcher_max_queue_size
|
55
|
+
)
|
56
|
+
|
57
|
+
offset_manager = OffsetManager.new(
|
58
|
+
cluster: cluster,
|
59
|
+
group: group,
|
60
|
+
fetcher: fetcher,
|
61
|
+
logger: @logger,
|
62
|
+
commit_interval: offset_commit_interval,
|
63
|
+
commit_threshold: offset_commit_threshold,
|
64
|
+
offset_retention_time: offset_retention_time
|
65
|
+
)
|
66
|
+
|
67
|
+
heartbeat = Heartbeat.new(
|
68
|
+
group: group,
|
69
|
+
interval: heartbeat_interval,
|
70
|
+
instrumenter: instrumenter
|
71
|
+
)
|
72
|
+
|
73
|
+
Consumer.new(
|
74
|
+
cluster: cluster,
|
75
|
+
logger: @logger,
|
76
|
+
instrumenter: instrumenter,
|
77
|
+
group: group,
|
78
|
+
offset_manager: offset_manager,
|
79
|
+
fetcher: fetcher,
|
80
|
+
session_timeout: session_timeout,
|
81
|
+
heartbeat: heartbeat
|
82
|
+
)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'avro_turf/schema_store'
|
4
|
+
|
5
|
+
# Allows us to add in-memory schemas to the schema store in
|
6
|
+
# addition to the ones stored in the file system.
|
7
|
+
class AvroTurf::SchemaStore
|
8
|
+
attr_accessor :schemas
|
9
|
+
|
10
|
+
# @param schema_hash [Hash]
|
11
|
+
def add_schema(schema_hash)
|
12
|
+
name = schema_hash['name']
|
13
|
+
namespace = schema_hash['namespace']
|
14
|
+
full_name = Avro::Name.make_fullname(name, namespace)
|
15
|
+
return if @schemas.key?(full_name)
|
16
|
+
|
17
|
+
Avro::Schema.real_parse(schema_hash, @schemas)
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'deimos/avro_data_encoder'
|
4
|
+
require 'deimos/message'
|
5
|
+
require 'deimos/shared_config'
|
6
|
+
require 'deimos/schema_coercer'
|
7
|
+
require 'phobos/producer'
|
8
|
+
require 'active_support/notifications'
|
9
|
+
|
10
|
+
# :nodoc:
|
11
|
+
module Deimos
|
12
|
+
class << self
|
13
|
+
# Run a block without allowing any messages to be produced to Kafka.
|
14
|
+
# Optionally add a list of producer classes to limit the disabling to those
|
15
|
+
# classes.
|
16
|
+
# @param producer_classes [Array<Class>|Class]
|
17
|
+
def disable_producers(*producer_classes, &block)
|
18
|
+
if producer_classes.any?
|
19
|
+
_disable_producer_classes(producer_classes, &block)
|
20
|
+
return
|
21
|
+
end
|
22
|
+
|
23
|
+
if Thread.current[:frk_disable_all_producers] # nested disable block
|
24
|
+
yield
|
25
|
+
return
|
26
|
+
end
|
27
|
+
|
28
|
+
Thread.current[:frk_disable_all_producers] = true
|
29
|
+
yield
|
30
|
+
Thread.current[:frk_disable_all_producers] = false
|
31
|
+
end
|
32
|
+
|
33
|
+
# :nodoc:
|
34
|
+
def _disable_producer_classes(producer_classes)
|
35
|
+
Thread.current[:frk_disabled_producers] ||= Set.new
|
36
|
+
producers_to_disable = producer_classes -
|
37
|
+
Thread.current[:frk_disabled_producers].to_a
|
38
|
+
Thread.current[:frk_disabled_producers] += producers_to_disable
|
39
|
+
yield
|
40
|
+
Thread.current[:frk_disabled_producers] -= producers_to_disable
|
41
|
+
end
|
42
|
+
|
43
|
+
# Are producers disabled? If a class is passed in, check only that class.
|
44
|
+
# Otherwise check if the global disable flag is set.
|
45
|
+
# @return [Boolean]
|
46
|
+
def producers_disabled?(producer_class=nil)
|
47
|
+
Thread.current[:frk_disable_all_producers] ||
|
48
|
+
Thread.current[:frk_disabled_producers]&.include?(producer_class)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Producer to publish messages to a given kafka topic.
|
53
|
+
class Producer
|
54
|
+
include SharedConfig
|
55
|
+
|
56
|
+
MAX_BATCH_SIZE = 500
|
57
|
+
|
58
|
+
class << self
|
59
|
+
# @return [Hash]
|
60
|
+
def config
|
61
|
+
@config ||= {
|
62
|
+
encode_key: true,
|
63
|
+
namespace: Deimos.config.producer_schema_namespace
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
# Set the topic.
|
68
|
+
# @param topic [String]
|
69
|
+
# @return [String] the current topic if no argument given.
|
70
|
+
def topic(topic=nil)
|
71
|
+
if topic
|
72
|
+
config[:topic] = topic
|
73
|
+
return
|
74
|
+
end
|
75
|
+
# accessor
|
76
|
+
"#{Deimos.config.producer_topic_prefix}#{config[:topic]}"
|
77
|
+
end
|
78
|
+
|
79
|
+
# Override the default partition key (which is the payload key).
|
80
|
+
# @param _payload [Hash] the payload being passed into the produce method.
|
81
|
+
# Will include `payload_key` if it is part of the original payload.
|
82
|
+
# @return [String]
|
83
|
+
def partition_key(_payload)
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
|
87
|
+
# Publish the payload to the topic.
|
88
|
+
# @param payload [Hash] with an optional payload_key hash key.
|
89
|
+
def publish(payload)
|
90
|
+
publish_list([payload])
|
91
|
+
end
|
92
|
+
|
93
|
+
# Publish a list of messages.
|
94
|
+
# @param payloads [Hash|Array<Hash>] with optional payload_key hash key.
|
95
|
+
# @param sync [Boolean] if given, override the default setting of
|
96
|
+
# whether to publish synchronously.
|
97
|
+
# @param force_send [Boolean] if true, ignore the configured backend
|
98
|
+
# and send immediately to Kafka.
|
99
|
+
def publish_list(payloads, sync: nil, force_send: false)
|
100
|
+
return if Deimos.config.seed_broker.blank? ||
|
101
|
+
Deimos.config.disable_producers ||
|
102
|
+
Deimos.producers_disabled?(self)
|
103
|
+
|
104
|
+
backend_class = determine_backend_class(sync, force_send)
|
105
|
+
Deimos.instrument(
|
106
|
+
'encode_messages',
|
107
|
+
producer: self,
|
108
|
+
topic: topic,
|
109
|
+
payloads: payloads
|
110
|
+
) do
|
111
|
+
messages = Array(payloads).map { |p| Deimos::Message.new(p, self) }
|
112
|
+
messages.each(&method(:_process_message))
|
113
|
+
messages.in_groups_of(MAX_BATCH_SIZE, false) do |batch|
|
114
|
+
self.produce_batch(backend_class, batch)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# @param sync [Boolean]
|
120
|
+
# @param force_send [Boolean]
|
121
|
+
# @return [Class < Deimos::Backend]
|
122
|
+
def determine_backend_class(sync, force_send)
|
123
|
+
backend = if force_send
|
124
|
+
:kafka
|
125
|
+
else
|
126
|
+
Deimos.config.publish_backend
|
127
|
+
end
|
128
|
+
if backend == :kafka_async && sync
|
129
|
+
backend = :kafka
|
130
|
+
elsif backend == :kafka && sync == false
|
131
|
+
backend = :kafka_async
|
132
|
+
end
|
133
|
+
"Deimos::Backends::#{backend.to_s.classify}".constantize
|
134
|
+
end
|
135
|
+
|
136
|
+
# Send a batch to the backend.
|
137
|
+
# @param backend [Class < Deimos::Backend]
|
138
|
+
# @param batch [Array<Deimos::Message>]
|
139
|
+
def produce_batch(backend, batch)
|
140
|
+
backend.publish(producer_class: self, messages: batch)
|
141
|
+
end
|
142
|
+
|
143
|
+
# @return [AvroDataEncoder]
|
144
|
+
def encoder
|
145
|
+
@encoder ||= AvroDataEncoder.new(schema: config[:schema],
|
146
|
+
namespace: config[:namespace])
|
147
|
+
end
|
148
|
+
|
149
|
+
# @return [AvroDataEncoder]
|
150
|
+
def key_encoder
|
151
|
+
@key_encoder ||= AvroDataEncoder.new(schema: config[:key_schema],
|
152
|
+
namespace: config[:namespace])
|
153
|
+
end
|
154
|
+
|
155
|
+
# Override this in active record producers to add
|
156
|
+
# non-schema fields to check for updates
|
157
|
+
# @return [Array<String>] fields to check for updates
|
158
|
+
def watched_attributes
|
159
|
+
self.encoder.avro_schema.fields.map(&:name)
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
|
164
|
+
# @param message [Message]
|
165
|
+
def _process_message(message)
|
166
|
+
# this violates the Law of Demeter but it has to happen in a very
|
167
|
+
# specific order and requires a bunch of methods on the producer
|
168
|
+
# to work correctly.
|
169
|
+
message.add_fields(encoder.avro_schema)
|
170
|
+
message.partition_key = self.partition_key(message.payload)
|
171
|
+
message.key = _retrieve_key(message.payload)
|
172
|
+
# need to do this before _coerce_fields because that might result
|
173
|
+
# in an empty payload which is an *error* whereas this is intended.
|
174
|
+
message.payload = nil if message.payload.blank?
|
175
|
+
message.coerce_fields(encoder.avro_schema)
|
176
|
+
message.encoded_key = _encode_key(message.key)
|
177
|
+
message.topic = self.topic
|
178
|
+
message.encoded_payload = if message.payload.nil?
|
179
|
+
nil
|
180
|
+
else
|
181
|
+
encoder.encode(message.payload,
|
182
|
+
topic: "#{config[:topic]}-value")
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# @param key [Object]
|
187
|
+
# @return [String|Object]
|
188
|
+
def _encode_key(key)
|
189
|
+
if key.nil?
|
190
|
+
return nil if config[:no_keys] # no key is fine, otherwise it's a problem
|
191
|
+
|
192
|
+
raise 'No key given but a key is required! Use `key_config none: true` to avoid using keys.'
|
193
|
+
end
|
194
|
+
if config[:encode_key] && config[:key_field].nil? &&
|
195
|
+
config[:key_schema].nil?
|
196
|
+
raise 'No key config given - if you are not encoding keys, please use `key_config plain: true`'
|
197
|
+
end
|
198
|
+
|
199
|
+
if config[:key_field]
|
200
|
+
encoder.encode_key(config[:key_field], key, "#{config[:topic]}-key")
|
201
|
+
elsif config[:key_schema]
|
202
|
+
key_encoder.encode(key, topic: "#{config[:topic]}-key")
|
203
|
+
else
|
204
|
+
key
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# @param payload [Hash]
|
209
|
+
# @return [String]
|
210
|
+
def _retrieve_key(payload)
|
211
|
+
key = payload.delete(:payload_key)
|
212
|
+
return key if key
|
213
|
+
|
214
|
+
config[:key_field] ? payload[config[:key_field]] : nil
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|