deimos-ruby 1.24.3 → 2.0.0.pre.alpha2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +0 -17
- data/.tool-versions +1 -0
- data/CHANGELOG.md +1 -1
- data/README.md +287 -498
- data/deimos-ruby.gemspec +4 -4
- data/docs/CONFIGURATION.md +133 -227
- data/docs/UPGRADING.md +237 -0
- data/lib/deimos/active_record_consume/batch_consumption.rb +28 -29
- data/lib/deimos/active_record_consume/message_consumption.rb +15 -21
- data/lib/deimos/active_record_consumer.rb +36 -26
- data/lib/deimos/active_record_producer.rb +28 -9
- data/lib/deimos/backends/base.rb +4 -35
- data/lib/deimos/backends/kafka.rb +6 -22
- data/lib/deimos/backends/kafka_async.rb +6 -22
- data/lib/deimos/backends/{db.rb → outbox.rb} +13 -9
- data/lib/deimos/config/configuration.rb +116 -385
- data/lib/deimos/consume/batch_consumption.rb +24 -124
- data/lib/deimos/consume/message_consumption.rb +36 -63
- data/lib/deimos/consumer.rb +16 -75
- data/lib/deimos/ext/consumer_route.rb +35 -0
- data/lib/deimos/ext/producer_middleware.rb +94 -0
- data/lib/deimos/ext/producer_route.rb +22 -0
- data/lib/deimos/ext/routing_defaults.rb +72 -0
- data/lib/deimos/ext/schema_route.rb +70 -0
- data/lib/deimos/kafka_message.rb +2 -2
- data/lib/deimos/kafka_source.rb +2 -7
- data/lib/deimos/kafka_topic_info.rb +1 -1
- data/lib/deimos/logging.rb +71 -0
- data/lib/deimos/message.rb +2 -11
- data/lib/deimos/metrics/datadog.rb +40 -1
- data/lib/deimos/metrics/provider.rb +4 -4
- data/lib/deimos/producer.rb +39 -116
- data/lib/deimos/railtie.rb +6 -0
- data/lib/deimos/schema_backends/avro_base.rb +21 -21
- data/lib/deimos/schema_backends/avro_schema_registry.rb +1 -2
- data/lib/deimos/schema_backends/avro_validation.rb +2 -2
- data/lib/deimos/schema_backends/base.rb +19 -12
- data/lib/deimos/schema_backends/mock.rb +6 -1
- data/lib/deimos/schema_backends/plain.rb +47 -0
- data/lib/deimos/schema_class/base.rb +2 -2
- data/lib/deimos/schema_class/enum.rb +1 -1
- data/lib/deimos/schema_class/record.rb +2 -2
- data/lib/deimos/test_helpers.rb +95 -320
- data/lib/deimos/tracing/provider.rb +6 -6
- data/lib/deimos/transcoder.rb +88 -0
- data/lib/deimos/utils/db_poller/base.rb +16 -14
- data/lib/deimos/utils/db_poller/state_based.rb +3 -3
- data/lib/deimos/utils/db_poller/time_based.rb +4 -4
- data/lib/deimos/utils/db_poller.rb +1 -1
- data/lib/deimos/utils/deadlock_retry.rb +1 -1
- data/lib/deimos/utils/{db_producer.rb → outbox_producer.rb} +16 -47
- data/lib/deimos/utils/schema_class.rb +0 -7
- data/lib/deimos/version.rb +1 -1
- data/lib/deimos.rb +78 -26
- data/lib/generators/deimos/{db_backend_generator.rb → outbox_backend_generator.rb} +4 -4
- data/lib/generators/deimos/schema_class_generator.rb +0 -1
- data/lib/generators/deimos/v2/templates/karafka.rb.tt +149 -0
- data/lib/generators/deimos/v2_generator.rb +193 -0
- data/lib/tasks/deimos.rake +5 -7
- data/spec/active_record_batch_consumer_association_spec.rb +22 -13
- data/spec/active_record_batch_consumer_spec.rb +84 -65
- data/spec/active_record_consume/batch_consumption_spec.rb +10 -10
- data/spec/active_record_consume/batch_slicer_spec.rb +12 -12
- data/spec/active_record_consumer_spec.rb +29 -13
- data/spec/active_record_producer_spec.rb +36 -26
- data/spec/backends/base_spec.rb +0 -23
- data/spec/backends/kafka_async_spec.rb +1 -3
- data/spec/backends/kafka_spec.rb +1 -3
- data/spec/backends/{db_spec.rb → outbox_spec.rb} +14 -20
- data/spec/batch_consumer_spec.rb +66 -116
- data/spec/consumer_spec.rb +53 -147
- data/spec/deimos_spec.rb +10 -126
- data/spec/kafka_source_spec.rb +19 -52
- data/spec/karafka/karafka.rb +69 -0
- data/spec/karafka_config/karafka_spec.rb +97 -0
- data/spec/logging_spec.rb +25 -0
- data/spec/message_spec.rb +9 -9
- data/spec/producer_spec.rb +112 -254
- data/spec/rake_spec.rb +1 -3
- data/spec/schema_backends/avro_validation_spec.rb +1 -1
- data/spec/schemas/com/my-namespace/MySchemaWithTitle.avsc +22 -0
- data/spec/snapshots/consumers-no-nest.snap +49 -0
- data/spec/snapshots/consumers.snap +49 -0
- data/spec/snapshots/consumers_and_producers-no-nest.snap +49 -0
- data/spec/snapshots/consumers_and_producers.snap +49 -0
- data/spec/snapshots/consumers_circular-no-nest.snap +49 -0
- data/spec/snapshots/consumers_circular.snap +49 -0
- data/spec/snapshots/consumers_complex_types-no-nest.snap +49 -0
- data/spec/snapshots/consumers_complex_types.snap +49 -0
- data/spec/snapshots/consumers_nested-no-nest.snap +49 -0
- data/spec/snapshots/consumers_nested.snap +49 -0
- data/spec/snapshots/namespace_folders.snap +49 -0
- data/spec/snapshots/namespace_map.snap +49 -0
- data/spec/snapshots/producers_with_key-no-nest.snap +49 -0
- data/spec/snapshots/producers_with_key.snap +49 -0
- data/spec/spec_helper.rb +61 -29
- data/spec/utils/db_poller_spec.rb +49 -39
- data/spec/utils/{db_producer_spec.rb → outbox_producer_spec.rb} +17 -184
- metadata +57 -67
- data/lib/deimos/batch_consumer.rb +0 -7
- data/lib/deimos/config/phobos_config.rb +0 -164
- data/lib/deimos/instrumentation.rb +0 -95
- data/lib/deimos/monkey_patches/phobos_cli.rb +0 -35
- data/lib/deimos/utils/inline_consumer.rb +0 -158
- data/lib/deimos/utils/lag_reporter.rb +0 -186
- data/lib/deimos/utils/schema_controller_mixin.rb +0 -129
- data/spec/config/configuration_spec.rb +0 -329
- data/spec/kafka_listener_spec.rb +0 -55
- data/spec/phobos.bad_db.yml +0 -73
- data/spec/phobos.yml +0 -77
- data/spec/utils/inline_consumer_spec.rb +0 -31
- data/spec/utils/lag_reporter_spec.rb +0 -76
- data/spec/utils/platform_schema_validation_spec.rb +0 -0
- data/spec/utils/schema_controller_mixin_spec.rb +0 -84
- /data/lib/generators/deimos/{db_backend → outbox_backend}/templates/migration +0 -0
- /data/lib/generators/deimos/{db_backend → outbox_backend}/templates/rails3_migration +0 -0
@@ -11,6 +11,7 @@ module Deimos
|
|
11
11
|
# Base poller class for retrieving and publishing messages.
|
12
12
|
class Base
|
13
13
|
|
14
|
+
FATAL_CODES = %i(invalid_msg_size msg_size_too_large)
|
14
15
|
# @return [Integer]
|
15
16
|
BATCH_SIZE = 1000
|
16
17
|
|
@@ -58,14 +59,14 @@ module Deimos
|
|
58
59
|
if Deimos.config.producers.backend == :kafka_async
|
59
60
|
Deimos.config.producers.backend = :kafka
|
60
61
|
end
|
61
|
-
Deimos.
|
62
|
+
Deimos::Logging.log_info('Starting...')
|
62
63
|
@signal_to_stop = false
|
63
64
|
ActiveRecord::Base.connection.reconnect! unless ActiveRecord::Base.connection.open_transactions.positive?
|
64
65
|
|
65
66
|
retrieve_poll_info
|
66
67
|
loop do
|
67
68
|
if @signal_to_stop
|
68
|
-
Deimos.
|
69
|
+
Deimos::Logging.log_info('Shutting down')
|
69
70
|
break
|
70
71
|
end
|
71
72
|
process_updates if should_run?
|
@@ -95,7 +96,7 @@ module Deimos
|
|
95
96
|
# Stop the poll.
|
96
97
|
# @return [void]
|
97
98
|
def stop
|
98
|
-
Deimos.
|
99
|
+
Deimos::Logging.log_info('Received signal to stop')
|
99
100
|
@signal_to_stop = true
|
100
101
|
end
|
101
102
|
|
@@ -111,9 +112,9 @@ module Deimos
|
|
111
112
|
# @param span [Object]
|
112
113
|
# @return [Boolean]
|
113
114
|
def handle_message_too_large(exception, batch, status, span)
|
114
|
-
Deimos.
|
115
|
+
Deimos::Logging.log_error("Error publishing through DB Poller: #{exception.message}")
|
115
116
|
if @config.skip_too_large_messages
|
116
|
-
Deimos.
|
117
|
+
Deimos::Logging.log_error("Skipping messages #{batch.map(&:id).join(', ')} since they are too large")
|
117
118
|
Deimos.config.tracer&.set_error(span, exception)
|
118
119
|
status.batches_errored += 1
|
119
120
|
true
|
@@ -137,21 +138,22 @@ module Deimos
|
|
137
138
|
process_batch(batch)
|
138
139
|
Deimos.config.tracer&.finish(span)
|
139
140
|
status.batches_processed += 1
|
140
|
-
rescue
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
141
|
+
rescue WaterDrop::Errors::ProduceManyError => e
|
142
|
+
if FATAL_CODES.include?(e.cause.try(:code))
|
143
|
+
retry unless handle_message_too_large(e, batch, status, span)
|
144
|
+
else
|
145
|
+
Deimos::Logging.log_error("Error publishing through DB Poller: #{e.message}")
|
146
|
+
sleep(0.5)
|
147
|
+
retry
|
148
|
+
end
|
147
149
|
rescue StandardError => e
|
148
|
-
Deimos.
|
150
|
+
Deimos::Logging.log_error("Error publishing through DB poller: #{e.message}}")
|
149
151
|
if @config.retries.nil? || retries < @config.retries
|
150
152
|
retries += 1
|
151
153
|
sleep(0.5)
|
152
154
|
retry
|
153
155
|
else
|
154
|
-
Deimos.
|
156
|
+
Deimos::Logging.log_error('Retries exceeded, moving on to next batch')
|
155
157
|
Deimos.config.tracer&.set_error(span, e)
|
156
158
|
status.batches_errored += 1
|
157
159
|
return false
|
@@ -10,14 +10,14 @@ module Deimos
|
|
10
10
|
# Send messages for updated data.
|
11
11
|
# @return [void]
|
12
12
|
def process_updates
|
13
|
-
Deimos.
|
13
|
+
Deimos::Logging.log_info("Polling #{log_identifier}")
|
14
14
|
status = PollStatus.new(0, 0, 0)
|
15
15
|
first_batch = true
|
16
16
|
|
17
17
|
# poll_query gets all the relevant data from the database, as defined
|
18
18
|
# by the producer itself.
|
19
19
|
loop do
|
20
|
-
Deimos.
|
20
|
+
Deimos::Logging.log_debug("Polling #{log_identifier}, batch #{status.current_batch}")
|
21
21
|
batch = fetch_results.to_a
|
22
22
|
break if batch.empty?
|
23
23
|
|
@@ -29,7 +29,7 @@ module Deimos
|
|
29
29
|
# If there were no results at all, we update last_sent so that we still get a wait
|
30
30
|
# before the next poll.
|
31
31
|
@info.touch(:last_sent) if first_batch
|
32
|
-
Deimos.
|
32
|
+
Deimos::Logging.log_info("Poll #{log_identifier} complete (#{status.report}")
|
33
33
|
end
|
34
34
|
|
35
35
|
# @return [ActiveRecord::Relation]
|
@@ -28,14 +28,14 @@ module Deimos
|
|
28
28
|
def process_updates
|
29
29
|
time_from = @config.full_table ? Time.new(0) : @info.last_sent.in_time_zone
|
30
30
|
time_to = Time.zone.now - @config.delay_time
|
31
|
-
Deimos.
|
31
|
+
Deimos::Logging.log_info("Polling #{log_identifier} from #{time_from} to #{time_to}")
|
32
32
|
status = PollStatus.new(0, 0, 0)
|
33
33
|
first_batch = true
|
34
34
|
|
35
35
|
# poll_query gets all the relevant data from the database, as defined
|
36
36
|
# by the producer itself.
|
37
37
|
loop do
|
38
|
-
Deimos.
|
38
|
+
Deimos::Logging.log_debug("Polling #{log_identifier}, batch #{status.current_batch}")
|
39
39
|
batch = fetch_results(time_from, time_to).to_a
|
40
40
|
break if batch.empty?
|
41
41
|
|
@@ -47,14 +47,14 @@ module Deimos
|
|
47
47
|
# If there were no results at all, we update last_sent so that we still get a wait
|
48
48
|
# before the next poll.
|
49
49
|
@info.touch(:last_sent) if first_batch
|
50
|
-
Deimos.
|
50
|
+
Deimos::Logging.log_info("Poll #{log_identifier} complete at #{time_to} (#{status.report})")
|
51
51
|
end
|
52
52
|
|
53
53
|
# @param time_from [ActiveSupport::TimeWithZone]
|
54
54
|
# @param time_to [ActiveSupport::TimeWithZone]
|
55
55
|
# @return [ActiveRecord::Relation]
|
56
56
|
def fetch_results(time_from, time_to)
|
57
|
-
id = self.producer_classes.first.
|
57
|
+
id = self.producer_classes.first.record_class.primary_key
|
58
58
|
quoted_timestamp = ActiveRecord::Base.connection.quote_column_name(@config.timestamp_column)
|
59
59
|
quoted_id = ActiveRecord::Base.connection.quote_column_name(id)
|
60
60
|
@resource_class.poll_query(time_from: time_from,
|
@@ -45,7 +45,7 @@ module Deimos
|
|
45
45
|
# Reraise if all retries exhausted
|
46
46
|
raise if count <= 0
|
47
47
|
|
48
|
-
Deimos.
|
48
|
+
Deimos::Logging.log_warn(
|
49
49
|
message: 'Deadlock encountered when trying to execute query. '\
|
50
50
|
"Retrying. #{count} attempt(s) remaining",
|
51
51
|
tags: tags
|
@@ -4,8 +4,7 @@ module Deimos
|
|
4
4
|
module Utils
|
5
5
|
# Class which continually polls the kafka_messages table
|
6
6
|
# in the database and sends Kafka messages.
|
7
|
-
class
|
8
|
-
include Phobos::Producer
|
7
|
+
class OutboxProducer
|
9
8
|
attr_accessor :id, :current_topic
|
10
9
|
|
11
10
|
# @return [Integer]
|
@@ -14,17 +13,19 @@ module Deimos
|
|
14
13
|
DELETE_BATCH_SIZE = 10
|
15
14
|
# @return [Integer]
|
16
15
|
MAX_DELETE_ATTEMPTS = 3
|
16
|
+
# @return [Array<Symbol>]
|
17
|
+
FATAL_CODES = %i(invalid_msg_size msg_size_too_large)
|
17
18
|
|
18
19
|
# @param logger [Logger]
|
19
20
|
def initialize(logger=Logger.new(STDOUT))
|
20
21
|
@id = SecureRandom.uuid
|
21
22
|
@logger = logger
|
22
|
-
@logger.push_tags("
|
23
|
+
@logger.push_tags("OutboxProducer #{@id}") if @logger.respond_to?(:push_tags)
|
23
24
|
end
|
24
25
|
|
25
26
|
# @return [FigTree]
|
26
27
|
def config
|
27
|
-
Deimos.config.
|
28
|
+
Deimos.config.outbox
|
28
29
|
end
|
29
30
|
|
30
31
|
# Start the poll.
|
@@ -82,7 +83,6 @@ module Deimos
|
|
82
83
|
rescue StandardError => e
|
83
84
|
@logger.error("Error processing messages for topic #{@current_topic}: #{e.class.name}: #{e.message} #{e.backtrace.join("\n")}")
|
84
85
|
KafkaTopicInfo.register_error(@current_topic, @id)
|
85
|
-
shutdown_producer
|
86
86
|
end
|
87
87
|
|
88
88
|
# Process a single batch in a topic.
|
@@ -94,24 +94,23 @@ module Deimos
|
|
94
94
|
batch_size = messages.size
|
95
95
|
compacted_messages = compact_messages(messages)
|
96
96
|
log_messages(compacted_messages)
|
97
|
-
|
97
|
+
Karafka.monitor.instrument('deimos.outbox.produce', topic: @current_topic, messages: compacted_messages) do
|
98
98
|
begin
|
99
|
-
produce_messages(compacted_messages.map(&:
|
100
|
-
rescue
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
@logger.error(Deimos::KafkaMessage.decoded(messages))
|
105
|
-
rescue StandardError => logging_exception # rubocop:disable Naming/RescuedExceptionsVariableName
|
106
|
-
@logger.error("Large message details logging failure: #{logging_exception.message}")
|
107
|
-
ensure
|
99
|
+
produce_messages(compacted_messages.map(&:karafka_message))
|
100
|
+
rescue WaterDrop::Errors::ProduceManyError => e
|
101
|
+
if FATAL_CODES.include?(e.cause.try(:code))
|
102
|
+
@logger.error('Message batch too large, deleting...')
|
103
|
+
delete_messages(messages)
|
108
104
|
raise e
|
105
|
+
else
|
106
|
+
Deimos.log_error("Got error #{e.cause.class.name} when publishing #{batch_size} messages, retrying...")
|
107
|
+
retry
|
109
108
|
end
|
110
109
|
end
|
111
110
|
end
|
112
111
|
delete_messages(messages)
|
113
112
|
Deimos.config.metrics&.increment(
|
114
|
-
'
|
113
|
+
'outbox.process',
|
115
114
|
tags: %W(topic:#{@current_topic}),
|
116
115
|
by: messages.size
|
117
116
|
)
|
@@ -197,16 +196,6 @@ module Deimos
|
|
197
196
|
end
|
198
197
|
end
|
199
198
|
|
200
|
-
# Shut down the sync producer if we have to. Phobos will automatically
|
201
|
-
# create a new one. We should call this if the producer can be in a bad
|
202
|
-
# state and e.g. we need to clear the buffer.
|
203
|
-
# @return [void]
|
204
|
-
def shutdown_producer
|
205
|
-
if self.class.producer.respond_to?(:sync_producer_shutdown) # Phobos 1.8.3
|
206
|
-
self.class.producer.sync_producer_shutdown
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
199
|
# Produce messages in batches, reducing the size 1/10 if the batch is too
|
211
200
|
# large. Does not retry batches of messages that have already been sent.
|
212
201
|
# @param batch [Array<Hash>]
|
@@ -217,30 +206,10 @@ module Deimos
|
|
217
206
|
begin
|
218
207
|
batch[current_index..-1].in_groups_of(batch_size, false).each do |group|
|
219
208
|
@logger.debug("Publishing #{group.size} messages to #{@current_topic}")
|
220
|
-
producer.
|
221
|
-
Deimos.config.metrics&.increment(
|
222
|
-
'publish',
|
223
|
-
tags: %W(status:success topic:#{@current_topic}),
|
224
|
-
by: group.size
|
225
|
-
)
|
209
|
+
Karafka.producer.produce_many_sync(group)
|
226
210
|
current_index += group.size
|
227
211
|
@logger.info("Sent #{group.size} messages to #{@current_topic}")
|
228
212
|
end
|
229
|
-
rescue Kafka::BufferOverflow, Kafka::MessageSizeTooLarge,
|
230
|
-
Kafka::RecordListTooLarge => e
|
231
|
-
if batch_size == 1
|
232
|
-
shutdown_producer
|
233
|
-
raise
|
234
|
-
end
|
235
|
-
|
236
|
-
@logger.error("Got error #{e.class.name} when publishing #{batch.size} in groups of #{batch_size}, retrying...")
|
237
|
-
batch_size = if batch_size < 10
|
238
|
-
1
|
239
|
-
else
|
240
|
-
(batch_size / 10).to_i
|
241
|
-
end
|
242
|
-
shutdown_producer
|
243
|
-
retry
|
244
213
|
end
|
245
214
|
end
|
246
215
|
|
@@ -55,13 +55,6 @@ module Deimos
|
|
55
55
|
constants.join('::').safe_constantize
|
56
56
|
end
|
57
57
|
|
58
|
-
# @param config [Hash] Producer or Consumer config
|
59
|
-
# @return [Boolean]
|
60
|
-
def use?(config)
|
61
|
-
use_schema_classes = config[:use_schema_classes]
|
62
|
-
use_schema_classes.present? ? use_schema_classes : Deimos.config.schema.use_schema_classes
|
63
|
-
end
|
64
|
-
|
65
58
|
end
|
66
59
|
end
|
67
60
|
end
|
data/lib/deimos/version.rb
CHANGED
data/lib/deimos.rb
CHANGED
@@ -1,17 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'active_support'
|
4
|
+
require 'karafka'
|
4
5
|
|
5
|
-
require 'phobos'
|
6
6
|
require 'deimos/version'
|
7
|
+
require 'deimos/logging'
|
7
8
|
require 'deimos/config/configuration'
|
8
9
|
require 'deimos/producer'
|
9
10
|
require 'deimos/active_record_producer'
|
10
11
|
require 'deimos/active_record_consumer'
|
11
12
|
require 'deimos/consumer'
|
12
|
-
require 'deimos/batch_consumer'
|
13
|
-
require 'deimos/instrumentation'
|
14
|
-
require 'deimos/utils/lag_reporter'
|
15
13
|
|
16
14
|
require 'deimos/backends/base'
|
17
15
|
require 'deimos/backends/kafka'
|
@@ -23,27 +21,38 @@ require 'deimos/utils/schema_class'
|
|
23
21
|
require 'deimos/schema_class/enum'
|
24
22
|
require 'deimos/schema_class/record'
|
25
23
|
|
26
|
-
require 'deimos/
|
24
|
+
require 'deimos/ext/schema_route'
|
25
|
+
require 'deimos/ext/consumer_route'
|
26
|
+
require 'deimos/ext/producer_route'
|
27
|
+
require 'deimos/ext/producer_middleware'
|
28
|
+
require 'deimos/ext/routing_defaults'
|
27
29
|
|
28
30
|
require 'deimos/railtie' if defined?(Rails)
|
29
|
-
require 'deimos/utils/schema_controller_mixin' if defined?(ActionController)
|
30
31
|
|
31
32
|
if defined?(ActiveRecord)
|
32
33
|
require 'deimos/kafka_source'
|
33
34
|
require 'deimos/kafka_topic_info'
|
34
|
-
require 'deimos/backends/
|
35
|
+
require 'deimos/backends/outbox'
|
35
36
|
require 'sigurd'
|
36
|
-
require 'deimos/utils/
|
37
|
+
require 'deimos/utils/outbox_producer'
|
37
38
|
require 'deimos/utils/db_poller'
|
38
39
|
end
|
39
40
|
|
40
|
-
require 'deimos/utils/inline_consumer'
|
41
41
|
require 'yaml'
|
42
42
|
require 'erb'
|
43
43
|
|
44
44
|
# Parent module.
|
45
45
|
module Deimos
|
46
|
+
EVENT_TYPES = %w(
|
47
|
+
deimos.ar_consumer.consume_batch
|
48
|
+
deimos.encode_message
|
49
|
+
deimos.batch_consumption.invalid_records
|
50
|
+
deimos.batch_consumption.valid_records
|
51
|
+
deimos.outbox.produce
|
52
|
+
)
|
53
|
+
|
46
54
|
class << self
|
55
|
+
|
47
56
|
# @return [Class<Deimos::SchemaBackends::Base>]
|
48
57
|
def schema_backend_class
|
49
58
|
backend = Deimos.config.schema.backend.to_s
|
@@ -57,7 +66,7 @@ module Deimos
|
|
57
66
|
# @param namespace [String]
|
58
67
|
# @return [Deimos::SchemaBackends::Base]
|
59
68
|
def schema_backend(schema:, namespace:)
|
60
|
-
if
|
69
|
+
if config.schema.use_schema_classes
|
61
70
|
# Initialize an instance of the provided schema
|
62
71
|
# in the event the schema class is an override, the inherited
|
63
72
|
# schema and namespace will be applied
|
@@ -91,13 +100,26 @@ module Deimos
|
|
91
100
|
self.schema_backend(schema: schema, namespace: namespace).decode(payload)
|
92
101
|
end
|
93
102
|
|
103
|
+
# @param message [Hash] a Karafka message with keys :payload, :key and :topic
|
104
|
+
def decode_message(message)
|
105
|
+
topic = message[:topic]
|
106
|
+
if Deimos.config.producers.topic_prefix
|
107
|
+
topic = topic.sub(Deimos.config.producers.topic_prefix, '')
|
108
|
+
end
|
109
|
+
config = karafka_config_for(topic: topic)
|
110
|
+
message[:payload] = config.deserializers[:payload].decode_message_hash(message[:payload])
|
111
|
+
if message[:key] && config.deserializers[:key].respond_to?(:decode_message_hash)
|
112
|
+
message[:key] = config.deserializers[:key].decode_message_hash(message[:key])
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
94
116
|
# Start the DB producers to send Kafka messages.
|
95
117
|
# @param thread_count [Integer] the number of threads to start.
|
96
118
|
# @return [void]
|
97
|
-
def
|
119
|
+
def start_outbox_backend!(thread_count: 1)
|
98
120
|
Sigurd.exit_on_signal = true
|
99
|
-
if self.config.producers.backend != :
|
100
|
-
raise('Publish backend is not set to :
|
121
|
+
if self.config.producers.backend != :outbox
|
122
|
+
raise('Publish backend is not set to :outbox, exiting')
|
101
123
|
end
|
102
124
|
|
103
125
|
if thread_count.nil? || thread_count.zero?
|
@@ -105,25 +127,55 @@ module Deimos
|
|
105
127
|
end
|
106
128
|
|
107
129
|
producers = (1..thread_count).map do
|
108
|
-
Deimos::Utils::
|
109
|
-
new(self.config.
|
130
|
+
Deimos::Utils::OutboxProducer.
|
131
|
+
new(self.config.outbox.logger || Karafka.logger)
|
110
132
|
end
|
111
133
|
executor = Sigurd::Executor.new(producers,
|
112
134
|
sleep_seconds: 5,
|
113
|
-
logger:
|
135
|
+
logger: Karafka.logger)
|
114
136
|
signal_handler = Sigurd::SignalHandler.new(executor)
|
115
137
|
signal_handler.run!
|
116
138
|
end
|
117
|
-
end
|
118
|
-
end
|
119
139
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
140
|
+
def setup_karafka
|
141
|
+
Karafka.producer.middleware.append(Deimos::ProducerMiddleware)
|
142
|
+
EVENT_TYPES.each { |type| Karafka.monitor.notifications_bus.register_event(type) }
|
143
|
+
|
144
|
+
Karafka.producer.monitor.subscribe('error.occurred') do |event|
|
145
|
+
if event.payload.key?(:messages)
|
146
|
+
topic = event[:messages].first[:topic]
|
147
|
+
config = Deimos.karafka_config_for(topic: topic)
|
148
|
+
message = Deimos::Logging.messages_log_text(config&.payload_log, event[:messages])
|
149
|
+
Karafka.logger.error("Error producing messages: #{event[:error].message} #{message.to_json}")
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# @return [Array<Karafka::Routing::Topic]
|
155
|
+
def karafka_configs
|
156
|
+
Karafka::App.routes.flat_map(&:topics).flat_map(&:to_a)
|
157
|
+
end
|
158
|
+
|
159
|
+
# @param topic [String]
|
160
|
+
# @return [Karafka::Routing::Topic,nil]
|
161
|
+
def karafka_config_for(topic: nil, producer: nil)
|
162
|
+
if topic
|
163
|
+
karafka_configs.find { |t| t.name == topic}
|
164
|
+
elsif producer
|
165
|
+
karafka_configs.find { |t| t.producer_class == producer}
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# @param handler_class [Class]
|
170
|
+
# @return [String,nil]
|
171
|
+
def topic_for_consumer(handler_class)
|
172
|
+
Deimos.karafka_configs.each do |topic|
|
173
|
+
if topic.consumer == handler_class
|
174
|
+
return topic.name
|
175
|
+
end
|
176
|
+
end
|
177
|
+
nil
|
178
|
+
end
|
179
|
+
|
128
180
|
end
|
129
181
|
end
|
@@ -6,14 +6,14 @@ require 'rails/generators/active_record/migration'
|
|
6
6
|
module Deimos
|
7
7
|
module Generators
|
8
8
|
# Generate the database backend migration.
|
9
|
-
class
|
9
|
+
class OutboxBackendGenerator < Rails::Generators::Base
|
10
10
|
include Rails::Generators::Migration
|
11
11
|
if Rails.version < '4'
|
12
12
|
extend(ActiveRecord::Generators::Migration)
|
13
13
|
else
|
14
14
|
include ActiveRecord::Generators::Migration
|
15
15
|
end
|
16
|
-
source_root File.expand_path('
|
16
|
+
source_root File.expand_path('outbox_backend/templates', __dir__)
|
17
17
|
desc 'Add migrations for the database backend'
|
18
18
|
|
19
19
|
# @return [String]
|
@@ -38,10 +38,10 @@ module Deimos
|
|
38
38
|
def generate
|
39
39
|
if Rails.version < '4'
|
40
40
|
migration_template('rails3_migration',
|
41
|
-
"#{db_migrate_path}/
|
41
|
+
"#{db_migrate_path}/create_outbox_backend.rb")
|
42
42
|
else
|
43
43
|
migration_template('migration',
|
44
|
-
"#{db_migrate_path}/
|
44
|
+
"#{db_migrate_path}/create_outbox_backend.rb")
|
45
45
|
end
|
46
46
|
end
|
47
47
|
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'deimos'
|
4
|
+
require 'karafka'
|
5
|
+
|
6
|
+
Deimos.configure do
|
7
|
+
<%- deimos_configs.each do |group, settings| -%>
|
8
|
+
<%= group -%> do
|
9
|
+
<%- settings.each do |k, v| -%>
|
10
|
+
<%= k.to_s %> <%= v.inspect %>
|
11
|
+
<%- end -%>
|
12
|
+
end
|
13
|
+
<%- end -%>
|
14
|
+
<%- deimos_config.db_poller_objects.each do |poller| -%>
|
15
|
+
db_poller do
|
16
|
+
<%- poller.non_default_settings! -%>
|
17
|
+
<%- poller.to_h.each do |k, v| -%>
|
18
|
+
<%= k.to_s %> <%= v.inspect %>
|
19
|
+
<%- end -%>
|
20
|
+
<%- end -%>
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class KarafkaApp < Karafka::App
|
25
|
+
setup do |config|
|
26
|
+
<%- setup_configs.each do |k, v| -%>
|
27
|
+
config.<%= k %> = <%= v.inspect %>
|
28
|
+
<%- end -%>
|
29
|
+
config.kafka = {
|
30
|
+
<%- default_kafka_configs.compact.each do |k, v| -%>
|
31
|
+
"<%= k.to_s %>": <%= v.inspect %>,
|
32
|
+
<%- end -%>
|
33
|
+
}
|
34
|
+
# Recreate consumers with each batch. This will allow Rails code reload to work in the
|
35
|
+
# development mode. Otherwise Karafka process would not be aware of code changes
|
36
|
+
config.consumer_persistence = !Rails.env.development?
|
37
|
+
end
|
38
|
+
|
39
|
+
# Comment out this part if you are not using instrumentation and/or you are not
|
40
|
+
# interested in logging events for certain environments. Since instrumentation
|
41
|
+
# notifications add extra boilerplate, if you want to achieve max performance,
|
42
|
+
# listen to only what you really need for given environment.
|
43
|
+
Karafka.monitor.subscribe(Karafka::Instrumentation::LoggerListener.new)
|
44
|
+
# Karafka.monitor.subscribe(Karafka::Instrumentation::ProctitleListener.new)
|
45
|
+
|
46
|
+
# This logger prints the producer development info using the Karafka logger.
|
47
|
+
# It is similar to the consumer logger listener but producer oriented.
|
48
|
+
Karafka.producer.monitor.subscribe(
|
49
|
+
WaterDrop::Instrumentation::LoggerListener.new(
|
50
|
+
# Log producer operations using the Karafka logger
|
51
|
+
Karafka.logger,
|
52
|
+
# If you set this to true, logs will contain each message details
|
53
|
+
# Please note, that this can be extensive
|
54
|
+
log_messages: false
|
55
|
+
)
|
56
|
+
)
|
57
|
+
|
58
|
+
# You can subscribe to all consumer related errors and record/track then that way
|
59
|
+
#
|
60
|
+
# Karafka.monitor.subscribe 'error.occurred' do |event|
|
61
|
+
# type = event[:type]
|
62
|
+
# error = event[:error]
|
63
|
+
# details = (error.backtrace || []).join("\n")
|
64
|
+
# ErrorTracker.send_error(error, type, details)
|
65
|
+
# end
|
66
|
+
|
67
|
+
# You can subscribe to all producer related errors and record/track then that way
|
68
|
+
# Please note, that producer and consumer have their own notifications pipeline so you need to
|
69
|
+
# setup error tracking independently for each of them
|
70
|
+
#
|
71
|
+
# Karafka.producer.monitor.subscribe('error.occurred') do |event|
|
72
|
+
# type = event[:type]
|
73
|
+
# error = event[:error]
|
74
|
+
# details = (error.backtrace || []).join("\n")
|
75
|
+
# ErrorTracker.send_error(error, type, details)
|
76
|
+
# end
|
77
|
+
|
78
|
+
routes.draw do
|
79
|
+
defaults do
|
80
|
+
<%- default_configs.each do |k, v| -%>
|
81
|
+
<%= k.to_s %> <%= v.inspect %>
|
82
|
+
<%- end -%>
|
83
|
+
end
|
84
|
+
|
85
|
+
<%- producer_configs.each do |producer| -%>
|
86
|
+
topic "<%= producer[:topic] %>" do
|
87
|
+
<%- producer.except(:topic).each do |k, v| -%>
|
88
|
+
<%- if k.to_sym == :key_config -%>
|
89
|
+
<%= k.to_s %>(<%= v.inspect %>)
|
90
|
+
<%- else -%>
|
91
|
+
<%= k.to_s %> <%= v.inspect %>
|
92
|
+
<%- end -%>
|
93
|
+
<%- end -%>
|
94
|
+
end
|
95
|
+
<%- end -%>
|
96
|
+
|
97
|
+
<%- consumer_configs.each do |group_id, topics| -%>
|
98
|
+
<%- if consumer_configs.length > 1 -%>
|
99
|
+
consumer_group :<%= group_id %> do<%- end -%>
|
100
|
+
<%- topics.each do |consumer| %>
|
101
|
+
topic "<%= consumer[:topic] -%>" do
|
102
|
+
<%- if consumer[:kafka].present? -%>
|
103
|
+
kafka(
|
104
|
+
<%- consumer[:kafka].each do |k, v| -%>
|
105
|
+
"<%= k.to_s %>": <%= v.inspect %>,
|
106
|
+
<%- end -%>
|
107
|
+
)
|
108
|
+
<%- end -%>
|
109
|
+
<%- consumer.except(:topic, :kafka).each do |k, v| -%>
|
110
|
+
<%- if k.to_sym == :key_config -%>
|
111
|
+
<%= k.to_s %>(<%= v.inspect %>)
|
112
|
+
<%- else -%>
|
113
|
+
<%= k.to_s %> <%= v.inspect %>
|
114
|
+
<%- end -%>
|
115
|
+
<%- end -%>
|
116
|
+
end
|
117
|
+
<%- end -%>
|
118
|
+
<%- if consumer_configs.length > 1 -%>
|
119
|
+
end<%- end %>
|
120
|
+
<%- end -%>
|
121
|
+
|
122
|
+
# Uncomment this if you use Karafka with ActiveJob
|
123
|
+
# You need to define the topic per each queue name you use
|
124
|
+
# active_job_topic :default
|
125
|
+
# topic :example do
|
126
|
+
# Uncomment this if you want Karafka to manage your topics configuration
|
127
|
+
# Managing topics configuration via routing will allow you to ensure config consistency
|
128
|
+
# across multiple environments
|
129
|
+
#
|
130
|
+
# config(partitions: 2, 'cleanup.policy': 'compact')
|
131
|
+
# consumer ExampleConsumer
|
132
|
+
# end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
Deimos.setup_karafka
|
137
|
+
|
138
|
+
# Karafka now features a Web UI!
|
139
|
+
# Visit the setup documentation to get started and enhance your experience.
|
140
|
+
#
|
141
|
+
# https://karafka.io/docs/Web-UI-Getting-Started
|
142
|
+
|
143
|
+
# Karafka::Web.setup do |config|
|
144
|
+
# # You may want to set it per ENV. This value was randomly generated.
|
145
|
+
# config.ui.sessions.secret = '<%= SecureRandom.hex %>'
|
146
|
+
# end
|
147
|
+
|
148
|
+
# Karafka::Web.enable!
|
149
|
+
|