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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/Gemfile.lock +8 -2
  4. data/README.md +69 -15
  5. data/deimos-ruby.gemspec +2 -0
  6. data/docs/ARCHITECTURE.md +144 -0
  7. data/docs/CONFIGURATION.md +4 -0
  8. data/lib/deimos.rb +6 -6
  9. data/lib/deimos/active_record_consume/batch_consumption.rb +159 -0
  10. data/lib/deimos/active_record_consume/batch_slicer.rb +27 -0
  11. data/lib/deimos/active_record_consume/message_consumption.rb +58 -0
  12. data/lib/deimos/active_record_consume/schema_model_converter.rb +52 -0
  13. data/lib/deimos/active_record_consumer.rb +33 -75
  14. data/lib/deimos/batch_consumer.rb +2 -142
  15. data/lib/deimos/config/configuration.rb +8 -10
  16. data/lib/deimos/consume/batch_consumption.rb +148 -0
  17. data/lib/deimos/consume/message_consumption.rb +93 -0
  18. data/lib/deimos/consumer.rb +79 -72
  19. data/lib/deimos/kafka_message.rb +1 -1
  20. data/lib/deimos/message.rb +6 -1
  21. data/lib/deimos/utils/db_poller.rb +6 -6
  22. data/lib/deimos/utils/db_producer.rb +6 -2
  23. data/lib/deimos/utils/deadlock_retry.rb +68 -0
  24. data/lib/deimos/utils/lag_reporter.rb +19 -26
  25. data/lib/deimos/version.rb +1 -1
  26. data/spec/active_record_batch_consumer_spec.rb +481 -0
  27. data/spec/active_record_consume/batch_slicer_spec.rb +42 -0
  28. data/spec/active_record_consume/schema_model_converter_spec.rb +105 -0
  29. data/spec/active_record_consumer_spec.rb +3 -11
  30. data/spec/batch_consumer_spec.rb +23 -7
  31. data/spec/config/configuration_spec.rb +4 -0
  32. data/spec/consumer_spec.rb +6 -6
  33. data/spec/deimos_spec.rb +57 -49
  34. data/spec/handlers/my_batch_consumer.rb +6 -1
  35. data/spec/handlers/my_consumer.rb +6 -1
  36. data/spec/message_spec.rb +19 -0
  37. data/spec/schemas/com/my-namespace/MySchemaCompound-key.avsc +18 -0
  38. data/spec/schemas/com/my-namespace/Wibble.avsc +43 -0
  39. data/spec/spec_helper.rb +17 -0
  40. data/spec/utils/db_poller_spec.rb +2 -2
  41. data/spec/utils/deadlock_retry_spec.rb +74 -0
  42. data/spec/utils/lag_reporter_spec.rb +29 -22
  43. metadata +57 -16
  44. data/lib/deimos/base_consumer.rb +0 -100
  45. data/lib/deimos/utils/executor.rb +0 -124
  46. data/lib/deimos/utils/platform_schema_validation.rb +0 -0
  47. data/lib/deimos/utils/signal_handler.rb +0 -68
  48. data/spec/utils/executor_spec.rb +0 -53
  49. data/spec/utils/signal_handler_spec.rb +0 -16
@@ -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
@@ -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
@@ -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