deimos-ruby 1.7.0.pre.beta1 → 1.8.0.pre.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/Gemfile.lock +8 -2
- data/README.md +69 -15
- data/deimos-ruby.gemspec +2 -0
- data/docs/ARCHITECTURE.md +144 -0
- data/docs/CONFIGURATION.md +4 -0
- data/lib/deimos.rb +6 -6
- data/lib/deimos/active_record_consume/batch_consumption.rb +159 -0
- data/lib/deimos/active_record_consume/batch_slicer.rb +27 -0
- data/lib/deimos/active_record_consume/message_consumption.rb +58 -0
- data/lib/deimos/active_record_consume/schema_model_converter.rb +52 -0
- data/lib/deimos/active_record_consumer.rb +33 -75
- data/lib/deimos/batch_consumer.rb +2 -142
- data/lib/deimos/config/configuration.rb +8 -10
- data/lib/deimos/consume/batch_consumption.rb +148 -0
- data/lib/deimos/consume/message_consumption.rb +93 -0
- data/lib/deimos/consumer.rb +79 -72
- data/lib/deimos/kafka_message.rb +1 -1
- data/lib/deimos/message.rb +6 -1
- data/lib/deimos/utils/db_poller.rb +6 -6
- data/lib/deimos/utils/db_producer.rb +6 -2
- data/lib/deimos/utils/deadlock_retry.rb +68 -0
- data/lib/deimos/utils/lag_reporter.rb +19 -26
- data/lib/deimos/version.rb +1 -1
- 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 +3 -11
- data/spec/batch_consumer_spec.rb +23 -7
- data/spec/config/configuration_spec.rb +4 -0
- data/spec/consumer_spec.rb +6 -6
- data/spec/deimos_spec.rb +57 -49
- data/spec/handlers/my_batch_consumer.rb +6 -1
- data/spec/handlers/my_consumer.rb +6 -1
- data/spec/message_spec.rb +19 -0
- data/spec/schemas/com/my-namespace/MySchemaCompound-key.avsc +18 -0
- data/spec/schemas/com/my-namespace/Wibble.avsc +43 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/utils/db_poller_spec.rb +2 -2
- data/spec/utils/deadlock_retry_spec.rb +74 -0
- data/spec/utils/lag_reporter_spec.rb +29 -22
- metadata +57 -16
- data/lib/deimos/base_consumer.rb +0 -100
- data/lib/deimos/utils/executor.rb +0 -124
- data/lib/deimos/utils/platform_schema_validation.rb +0 -0
- data/lib/deimos/utils/signal_handler.rb +0 -68
- data/spec/utils/executor_spec.rb +0 -53
- data/spec/utils/signal_handler_spec.rb +0 -16
data/lib/deimos/base_consumer.rb
DELETED
@@ -1,100 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Deimos
|
4
|
-
# Shared methods for Kafka Consumers
|
5
|
-
class BaseConsumer
|
6
|
-
include SharedConfig
|
7
|
-
|
8
|
-
class << self
|
9
|
-
# @return [Deimos::SchemaBackends::Base]
|
10
|
-
def decoder
|
11
|
-
@decoder ||= Deimos.schema_backend(schema: config[:schema],
|
12
|
-
namespace: config[:namespace])
|
13
|
-
end
|
14
|
-
|
15
|
-
# @return [Deimos::SchemaBackends::Base]
|
16
|
-
def key_decoder
|
17
|
-
@key_decoder ||= Deimos.schema_backend(schema: config[:key_schema],
|
18
|
-
namespace: config[:namespace])
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
# Helper method to decode an encoded key.
|
23
|
-
# @param key [String]
|
24
|
-
# @return [Object] the decoded key.
|
25
|
-
def decode_key(key)
|
26
|
-
return nil if key.nil?
|
27
|
-
|
28
|
-
config = self.class.config
|
29
|
-
unless config[:key_configured]
|
30
|
-
raise 'No key config given - if you are not decoding keys, please use '\
|
31
|
-
'`key_config plain: true`'
|
32
|
-
end
|
33
|
-
|
34
|
-
if config[:key_field]
|
35
|
-
self.class.decoder.decode_key(key, config[:key_field])
|
36
|
-
elsif config[:key_schema]
|
37
|
-
self.class.key_decoder.decode(key, schema: config[:key_schema])
|
38
|
-
else # no encoding
|
39
|
-
key
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
protected
|
44
|
-
|
45
|
-
def _with_span
|
46
|
-
@span = Deimos.config.tracer&.start(
|
47
|
-
'deimos-consumer',
|
48
|
-
resource: self.class.name.gsub('::', '-')
|
49
|
-
)
|
50
|
-
yield
|
51
|
-
ensure
|
52
|
-
Deimos.config.tracer&.finish(@span)
|
53
|
-
end
|
54
|
-
|
55
|
-
def _report_time_delayed(payload, metadata)
|
56
|
-
return if payload.nil? || payload['timestamp'].blank?
|
57
|
-
|
58
|
-
begin
|
59
|
-
time_delayed = Time.now.in_time_zone - payload['timestamp'].to_datetime
|
60
|
-
rescue ArgumentError
|
61
|
-
Deimos.config.logger.info(
|
62
|
-
message: "Error parsing timestamp! #{payload['timestamp']}"
|
63
|
-
)
|
64
|
-
return
|
65
|
-
end
|
66
|
-
Deimos.config.metrics&.histogram('handler', time_delayed, tags: %W(
|
67
|
-
time:time_delayed
|
68
|
-
topic:#{metadata[:topic]}
|
69
|
-
))
|
70
|
-
end
|
71
|
-
|
72
|
-
# Overrideable method to determine if a given error should be considered
|
73
|
-
# "fatal" and always be reraised.
|
74
|
-
# @param error [Exception]
|
75
|
-
# @param payload [Hash]
|
76
|
-
# @param metadata [Hash]
|
77
|
-
# @return [Boolean]
|
78
|
-
def fatal_error?(_error, _payload, _metadata)
|
79
|
-
false
|
80
|
-
end
|
81
|
-
|
82
|
-
# @param exception [Exception]
|
83
|
-
# @param payload [Hash]
|
84
|
-
# @param metadata [Hash]
|
85
|
-
def _handle_error(exception, payload, metadata)
|
86
|
-
Deimos.config.tracer&.set_error(@span, exception)
|
87
|
-
|
88
|
-
raise if Deimos.config.consumers.reraise_errors ||
|
89
|
-
Deimos.config.consumers.fatal_error&.call(exception, payload, metadata) ||
|
90
|
-
fatal_error?(exception, payload, metadata)
|
91
|
-
end
|
92
|
-
|
93
|
-
# @param _time_taken [Float]
|
94
|
-
# @param _payload [Hash]
|
95
|
-
# @param _metadata [Hash]
|
96
|
-
def _handle_success(_time_taken, _payload, _metadata)
|
97
|
-
raise NotImplementedError
|
98
|
-
end
|
99
|
-
end
|
100
|
-
end
|
@@ -1,124 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# rubocop:disable Lint/RescueException
|
4
|
-
module Deimos
|
5
|
-
module Utils
|
6
|
-
# Mostly copied from Phobos::Executor. We should DRY this up by putting in a
|
7
|
-
# PR to make it more generic. Might even make sense to move to a separate
|
8
|
-
# gem.
|
9
|
-
class Executor
|
10
|
-
# @return [Array<#start, #stop, #id>]
|
11
|
-
attr_accessor :runners
|
12
|
-
|
13
|
-
# @param runners [Array<#start, #stop, #id>] A list of objects that can be
|
14
|
-
# started or stopped.
|
15
|
-
# @param logger [Logger]
|
16
|
-
# @param sleep_seconds [Integer] Use a fixed time to sleep between
|
17
|
-
# failed runs instead of using an exponential backoff.
|
18
|
-
def initialize(runners, sleep_seconds: nil, logger: Logger.new(STDOUT))
|
19
|
-
@threads = Concurrent::Array.new
|
20
|
-
@runners = runners
|
21
|
-
@logger = logger
|
22
|
-
@sleep_seconds = sleep_seconds
|
23
|
-
end
|
24
|
-
|
25
|
-
# Start the executor.
|
26
|
-
def start
|
27
|
-
@logger.info('Starting executor')
|
28
|
-
@signal_to_stop = false
|
29
|
-
@threads.clear
|
30
|
-
@thread_pool = Concurrent::FixedThreadPool.new(@runners.size)
|
31
|
-
|
32
|
-
@runners.each do |runner|
|
33
|
-
@thread_pool.post do
|
34
|
-
thread = Thread.current
|
35
|
-
thread.abort_on_exception = true
|
36
|
-
@threads << thread
|
37
|
-
run_object(runner)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
true
|
42
|
-
end
|
43
|
-
|
44
|
-
# Stop the executor.
|
45
|
-
def stop
|
46
|
-
return if @signal_to_stop
|
47
|
-
|
48
|
-
@logger.info('Stopping executor')
|
49
|
-
@signal_to_stop = true
|
50
|
-
@runners.each(&:stop)
|
51
|
-
@threads.select(&:alive?).each do |thread|
|
52
|
-
begin
|
53
|
-
thread.wakeup
|
54
|
-
rescue StandardError
|
55
|
-
nil
|
56
|
-
end
|
57
|
-
end
|
58
|
-
@thread_pool&.shutdown
|
59
|
-
@thread_pool&.wait_for_termination
|
60
|
-
@logger.info('Executor stopped')
|
61
|
-
end
|
62
|
-
|
63
|
-
private
|
64
|
-
|
65
|
-
# @param exception [Throwable]
|
66
|
-
# @return [Hash]
|
67
|
-
def error_metadata(exception)
|
68
|
-
{
|
69
|
-
exception_class: exception.class.name,
|
70
|
-
exception_message: exception.message,
|
71
|
-
backtrace: exception.backtrace
|
72
|
-
}
|
73
|
-
end
|
74
|
-
|
75
|
-
def run_object(runner)
|
76
|
-
retry_count = 0
|
77
|
-
|
78
|
-
begin
|
79
|
-
@logger.info("Running #{runner.id}")
|
80
|
-
runner.start
|
81
|
-
retry_count = 0 # success - reset retry count
|
82
|
-
rescue Exception => e
|
83
|
-
handle_crashed_runner(runner, e, retry_count)
|
84
|
-
retry_count += 1
|
85
|
-
retry unless @signal_to_stop
|
86
|
-
end
|
87
|
-
rescue Exception => e
|
88
|
-
@logger.error("Failed to run executor (#{e.message}) #{error_metadata(e)}")
|
89
|
-
raise e
|
90
|
-
end
|
91
|
-
|
92
|
-
# @return [ExponentialBackoff]
|
93
|
-
def create_exponential_backoff
|
94
|
-
min = 1
|
95
|
-
max = 60
|
96
|
-
ExponentialBackoff.new(min, max).tap do |backoff|
|
97
|
-
backoff.randomize_factor = rand
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
# When "runner#start" is interrupted / crashes we assume it's
|
102
|
-
# safe to be called again
|
103
|
-
def handle_crashed_runner(runner, error, retry_count)
|
104
|
-
interval = if @sleep_seconds
|
105
|
-
@sleep_seconds
|
106
|
-
else
|
107
|
-
backoff = create_exponential_backoff
|
108
|
-
backoff.interval_at(retry_count).round(2)
|
109
|
-
end
|
110
|
-
|
111
|
-
metadata = {
|
112
|
-
listener_id: runner.id,
|
113
|
-
retry_count: retry_count,
|
114
|
-
waiting_time: interval
|
115
|
-
}.merge(error_metadata(error))
|
116
|
-
|
117
|
-
@logger.error("Runner crashed, waiting #{interval}s (#{error.message}) #{metadata}")
|
118
|
-
sleep(interval)
|
119
|
-
end
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
# rubocop:enable Lint/RescueException
|
File without changes
|
@@ -1,68 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Deimos
|
4
|
-
module Utils
|
5
|
-
# Mostly copied free-form from Phobos::Cli::Runner. We should add a PR to
|
6
|
-
# basically replace that implementation with this one to make it more generic.
|
7
|
-
class SignalHandler
|
8
|
-
SIGNALS = %i(INT TERM QUIT).freeze
|
9
|
-
|
10
|
-
# Takes any object that responds to the `start` and `stop` methods.
|
11
|
-
# @param runner[#start, #stop]
|
12
|
-
def initialize(runner)
|
13
|
-
@signal_queue = []
|
14
|
-
@reader, @writer = IO.pipe
|
15
|
-
@runner = runner
|
16
|
-
end
|
17
|
-
|
18
|
-
# Run the runner.
|
19
|
-
def run!
|
20
|
-
setup_signals
|
21
|
-
@runner.start
|
22
|
-
|
23
|
-
loop do
|
24
|
-
case signal_queue.pop
|
25
|
-
when *SIGNALS
|
26
|
-
@runner.stop
|
27
|
-
break
|
28
|
-
else
|
29
|
-
ready = IO.select([reader, writer])
|
30
|
-
|
31
|
-
# drain the self-pipe so it won't be returned again next time
|
32
|
-
reader.read_nonblock(1) if ready[0].include?(reader)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
private
|
38
|
-
|
39
|
-
attr_reader :reader, :writer, :signal_queue, :executor
|
40
|
-
|
41
|
-
# https://stackoverflow.com/questions/29568298/run-code-when-signal-is-sent-but-do-not-trap-the-signal-in-ruby
|
42
|
-
def prepend_handler(signal)
|
43
|
-
previous = Signal.trap(signal) do
|
44
|
-
previous = -> { raise SignalException, signal } unless previous.respond_to?(:call)
|
45
|
-
yield
|
46
|
-
previous.call
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
# Trap signals using the self-pipe trick.
|
51
|
-
def setup_signals
|
52
|
-
at_exit { @runner&.stop }
|
53
|
-
SIGNALS.each do |signal|
|
54
|
-
prepend_handler(signal) do
|
55
|
-
unblock(signal)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
# Save the signal to the queue and continue on.
|
61
|
-
# @param signal [Symbol]
|
62
|
-
def unblock(signal)
|
63
|
-
writer.write_nonblock('.')
|
64
|
-
signal_queue << signal
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
data/spec/utils/executor_spec.rb
DELETED
@@ -1,53 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'spec_helper'
|
4
|
-
|
5
|
-
RSpec.describe Deimos::Utils::Executor do
|
6
|
-
|
7
|
-
let(:executor) { described_class.new(runners) }
|
8
|
-
let(:runners) { (1..2).map { |i| TestRunners::TestRunner.new(i) } }
|
9
|
-
|
10
|
-
it 'starts and stops configured runners' do
|
11
|
-
runners.each do |r|
|
12
|
-
expect(r.started).to be_falsey
|
13
|
-
expect(r.stopped).to be_falsey
|
14
|
-
end
|
15
|
-
executor.start
|
16
|
-
wait_for do
|
17
|
-
runners.each do |r|
|
18
|
-
expect(r.started).to be_truthy
|
19
|
-
expect(r.stopped).to be_falsey
|
20
|
-
end
|
21
|
-
executor.stop
|
22
|
-
runners.each do |r|
|
23
|
-
expect(r.started).to be_truthy
|
24
|
-
expect(r.stopped).to be_truthy
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
it 'sleeps X seconds' do
|
30
|
-
executor = described_class.new(runners, sleep_seconds: 5)
|
31
|
-
allow(executor).to receive(:handle_crashed_runner).and_call_original
|
32
|
-
expect(executor).to receive(:sleep).with(5).twice
|
33
|
-
runners.each { |r| r.should_error = true }
|
34
|
-
executor.start
|
35
|
-
wait_for do
|
36
|
-
runners.each { |r| expect(r.started).to be_truthy }
|
37
|
-
executor.stop
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
it 'reconnects crashed runners' do
|
42
|
-
allow(executor).to receive(:handle_crashed_runner).and_call_original
|
43
|
-
runners.each { |r| r.should_error = true }
|
44
|
-
executor.start
|
45
|
-
wait_for do
|
46
|
-
expect(executor).to have_received(:handle_crashed_runner).with(runners[0], anything, 0).once
|
47
|
-
expect(executor).to have_received(:handle_crashed_runner).with(runners[1], anything, 0).once
|
48
|
-
runners.each { |r| expect(r.started).to be_truthy }
|
49
|
-
executor.stop
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
end
|
@@ -1,16 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
RSpec.describe Deimos::Utils::SignalHandler do
|
4
|
-
describe '#run!' do
|
5
|
-
|
6
|
-
it 'starts and stops the runner' do
|
7
|
-
runner = TestRunners::TestRunner.new
|
8
|
-
expect(runner).to receive(:start)
|
9
|
-
expect(runner).to receive(:stop)
|
10
|
-
|
11
|
-
signal_handler = described_class.new(runner)
|
12
|
-
signal_handler.send(:unblock, described_class::SIGNALS.first)
|
13
|
-
signal_handler.run!
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|