phobos 1.8.0 → 1.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,9 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thor'
2
4
  require 'phobos/cli/start'
3
5
 
4
6
  module Phobos
5
7
  module CLI
6
-
7
8
  def self.logger
8
9
  @logger ||= Logging.logger[self].tap do |l|
9
10
  l.appenders = [Logging.appenders.stdout]
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Phobos
2
4
  module CLI
3
5
  class Runner
4
-
5
- SIGNALS = %i( INT TERM QUIT ).freeze
6
+ SIGNALS = [:INT, :TERM, :QUIT].freeze
6
7
 
7
8
  def initialize
8
9
  @signal_queue = []
@@ -42,7 +43,6 @@ module Phobos
42
43
  writer.write_nonblock('.')
43
44
  signal_queue << signal
44
45
  end
45
-
46
46
  end
47
47
  end
48
48
  end
@@ -1,17 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'phobos/cli/runner'
2
4
 
3
5
  module Phobos
4
6
  module CLI
5
7
  class Start
6
8
  def initialize(options)
7
- unless options[:skip_config]
8
- @config_file = File.expand_path(options[:config])
9
- end
9
+ @config_file = File.expand_path(options[:config]) unless options[:skip_config]
10
10
  @boot_file = File.expand_path(options[:boot])
11
11
 
12
- if options[:listeners]
13
- @listeners_file = File.expand_path(options[:listeners])
14
- end
12
+ @listeners_file = File.expand_path(options[:listeners]) if options[:listeners]
15
13
  end
16
14
 
17
15
  def execute
@@ -22,9 +20,7 @@ module Phobos
22
20
  Phobos.configure(config_file)
23
21
  end
24
22
 
25
- if listeners_file
26
- Phobos.add_listeners(listeners_file)
27
- end
23
+ Phobos.add_listeners(listeners_file) if listeners_file
28
24
 
29
25
  validate_listeners!
30
26
 
@@ -36,37 +32,33 @@ module Phobos
36
32
  attr_reader :config_file, :boot_file, :listeners_file
37
33
 
38
34
  def validate_config_file!
39
- unless File.exist?(config_file)
40
- Phobos::CLI.logger.error { Hash(message: "Config file not found (#{config_file})") }
41
- exit(1)
42
- end
35
+ File.exist?(config_file) || error_exit("Config file not found (#{config_file})")
43
36
  end
44
37
 
45
38
  def validate_listeners!
46
39
  Phobos.config.listeners.each do |listener|
47
- handler_class = listener.handler
40
+ handler = listener.handler
48
41
 
49
- begin
50
- handler_class.constantize
51
- rescue NameError
52
- Phobos::CLI.logger.error { Hash(message: "Handler '#{handler_class}' not defined") }
53
- exit(1)
54
- end
42
+ Object.const_defined?(handler) || error_exit("Handler '#{handler}' not defined")
55
43
 
56
44
  delivery = listener.delivery
57
45
  if delivery.nil?
58
46
  Phobos::CLI.logger.warn do
59
- Hash(message: "Delivery option should be specified, defaulting to 'batch' - specify this option to silence this message")
47
+ Hash(message: "Delivery option should be specified, defaulting to 'batch'"\
48
+ ' - specify this option to silence this message')
60
49
  end
61
50
  elsif !Listener::DELIVERY_OPTS.include?(delivery)
62
- Phobos::CLI.logger.error do
63
- Hash(message: "Invalid delivery option '#{delivery}'. Please specify one of: #{Listener::DELIVERY_OPTS.join(', ')}")
64
- end
65
- exit(1)
51
+ error_exit("Invalid delivery option '#{delivery}'. Please specify one of: "\
52
+ "#{Listener::DELIVERY_OPTS.join(', ')}")
66
53
  end
67
54
  end
68
55
  end
69
56
 
57
+ def error_exit(msg)
58
+ Phobos::CLI.logger.error { Hash(message: msg) }
59
+ exit(1)
60
+ end
61
+
70
62
  def load_boot_file
71
63
  load(boot_file) if File.exist?(boot_file)
72
64
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phobos
4
+ module Constants
5
+ LOG_DATE_PATTERN = '%Y-%m-%dT%H:%M:%S:%L%zZ'
6
+
7
+ KAFKA_CONSUMER_OPTS = [
8
+ :session_timeout,
9
+ :offset_commit_interval,
10
+ :offset_commit_threshold,
11
+ :heartbeat_interval,
12
+ :offset_retention_time
13
+ ].freeze
14
+
15
+ LISTENER_OPTS = [
16
+ :handler,
17
+ :group_id,
18
+ :topic,
19
+ :min_bytes,
20
+ :max_wait_time,
21
+ :force_encoding,
22
+ :start_from_beginning,
23
+ :max_bytes_per_partition,
24
+ :backoff,
25
+ :delivery,
26
+ :session_timeout,
27
+ :offset_commit_interval,
28
+ :offset_commit_threshold,
29
+ :heartbeat_interval,
30
+ :offset_retention_time
31
+ ].freeze
32
+ end
33
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Please use this with at least the same consideration as you would when using OpenStruct.
2
4
  # Right now we only use this to parse our internal configuration files. It is not meant to
3
5
  # be used on incoming data.
@@ -5,36 +7,33 @@ module Phobos
5
7
  class DeepStruct < OpenStruct
6
8
  # Based on
7
9
  # https://docs.omniref.com/ruby/2.3.0/files/lib/ostruct.rb#line=88
8
- def initialize(hash=nil)
10
+ def initialize(hash = nil)
9
11
  @table = {}
10
12
  @hash_table = {}
11
13
 
12
- if hash
13
- hash.each_pair do |k, v|
14
- k = k.to_sym
15
- @table[k] = to_deep_struct(v)
16
- @hash_table[k] = v
17
- end
14
+ hash&.each_pair do |key, value|
15
+ key = key.to_sym
16
+ @table[key] = to_deep_struct(value)
17
+ @hash_table[key] = value
18
18
  end
19
19
  end
20
20
 
21
21
  def to_h
22
22
  @hash_table.dup
23
23
  end
24
- alias_method :to_hash, :to_h
24
+ alias to_hash to_h
25
25
 
26
26
  private
27
27
 
28
- def to_deep_struct(v)
29
- case v
28
+ def to_deep_struct(value)
29
+ case value
30
30
  when Hash
31
- self.class.new(v)
31
+ self.class.new(value)
32
32
  when Enumerable
33
- v.map { |el| to_deep_struct(el) }
33
+ value.map { |el| to_deep_struct(el) }
34
34
  else
35
- v
35
+ value
36
36
  end
37
37
  end
38
- protected :to_deep_struct
39
38
  end
40
39
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Phobos
2
4
  class EchoHandler
3
5
  include Phobos::Handler
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Phobos
2
4
  class Error < StandardError; end
3
5
  class AbortError < Error; end
@@ -1,23 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Phobos
2
4
  class Executor
3
5
  include Phobos::Instrumentation
4
- LISTENER_OPTS = %i(
5
- handler
6
- group_id
7
- topic
8
- min_bytes
9
- max_wait_time
10
- force_encoding
11
- start_from_beginning
12
- max_bytes_per_partition
13
- backoff
14
- delivery
15
- session_timeout
16
- offset_commit_interval
17
- offset_commit_threshold
18
- heartbeat_interval
19
- offset_retention_time
20
- ).freeze
6
+ include Phobos::Log
21
7
 
22
8
  def initialize
23
9
  @threads = Concurrent::Array.new
@@ -26,7 +12,7 @@ module Phobos
26
12
  listener_configs = config.to_hash.deep_symbolize_keys
27
13
  max_concurrency = listener_configs[:max_concurrency] || 1
28
14
  Array.new(max_concurrency).map do
29
- configs = listener_configs.select { |k| LISTENER_OPTS.include?(k) }
15
+ configs = listener_configs.select { |k| Constants::LISTENER_OPTS.include?(k) }
30
16
  Phobos::Listener.new(configs.merge(handler: handler_class))
31
17
  end
32
18
  end
@@ -51,10 +37,17 @@ module Phobos
51
37
 
52
38
  def stop
53
39
  return if @signal_to_stop
40
+
54
41
  instrument('executor.stop') do
55
42
  @signal_to_stop = true
56
43
  @listeners.each(&:stop)
57
- @threads.select(&:alive?).each { |thread| thread.wakeup rescue nil }
44
+ @threads.select(&:alive?).each do |thread|
45
+ begin
46
+ thread.wakeup
47
+ rescue StandardError
48
+ nil
49
+ end
50
+ end
58
51
  @thread_pool&.shutdown
59
52
  @thread_pool&.wait_for_termination
60
53
  Phobos.logger.info { Hash(message: 'Executor stopped') }
@@ -63,44 +56,48 @@ module Phobos
63
56
 
64
57
  private
65
58
 
66
- def error_metadata(e)
59
+ def error_metadata(exception)
67
60
  {
68
- exception_class: e.class.name,
69
- exception_message: e.message,
70
- backtrace: e.backtrace
61
+ exception_class: exception.class.name,
62
+ exception_message: exception.message,
63
+ backtrace: exception.backtrace
71
64
  }
72
65
  end
73
66
 
67
+ # rubocop:disable Lint/RescueException
74
68
  def run_listener(listener)
75
69
  retry_count = 0
76
- backoff = listener.create_exponential_backoff
77
70
 
78
71
  begin
79
72
  listener.start
80
73
  rescue Exception => e
81
- #
82
- # When "listener#start" is interrupted it's safe to assume that the consumer
83
- # and the kafka client were properly stopped, it's safe to call start
84
- # again
85
- #
86
- interval = backoff.interval_at(retry_count).round(2)
87
- metadata = {
88
- listener_id: listener.id,
89
- retry_count: retry_count,
90
- waiting_time: interval
91
- }.merge(error_metadata(e))
92
-
93
- instrument('executor.retry_listener_error', metadata) do
94
- Phobos.logger.error { Hash(message: "Listener crashed, waiting #{interval}s (#{e.message})").merge(metadata)}
95
- sleep interval
96
- end
97
-
74
+ handle_crashed_listener(listener, e, retry_count)
98
75
  retry_count += 1
99
76
  retry unless @signal_to_stop
100
77
  end
101
78
  rescue Exception => e
102
- Phobos.logger.error { Hash(message: "Failed to run listener (#{e.message})").merge(error_metadata(e)) }
79
+ log_error("Failed to run listener (#{e.message})", error_metadata(e))
103
80
  raise e
104
81
  end
82
+ # rubocop:enable Lint/RescueException
83
+
84
+ # When "listener#start" is interrupted it's safe to assume that the consumer
85
+ # and the kafka client were properly stopped, it's safe to call start
86
+ # again
87
+ def handle_crashed_listener(listener, error, retry_count)
88
+ backoff = listener.create_exponential_backoff
89
+ interval = backoff.interval_at(retry_count).round(2)
90
+
91
+ metadata = {
92
+ listener_id: listener.id,
93
+ retry_count: retry_count,
94
+ waiting_time: interval
95
+ }.merge(error_metadata(error))
96
+
97
+ instrument('executor.retry_listener_error', metadata) do
98
+ log_error("Listener crashed, waiting #{interval}s (#{error.message})", metadata)
99
+ sleep interval
100
+ end
101
+ end
105
102
  end
106
103
  end
@@ -1,27 +1,27 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Phobos
2
4
  module Handler
3
5
  def self.included(base)
4
6
  base.extend(ClassMethods)
5
7
  end
6
8
 
7
- def before_consume(payload, metadata)
9
+ def before_consume(payload, _metadata)
8
10
  payload
9
11
  end
10
12
 
11
- def consume(payload, metadata)
13
+ def consume(_payload, _metadata)
12
14
  raise NotImplementedError
13
15
  end
14
16
 
15
- def around_consume(payload, metadata)
17
+ def around_consume(_payload, _metadata)
16
18
  yield
17
19
  end
18
20
 
19
21
  module ClassMethods
20
- def start(kafka_client)
21
- end
22
+ def start(kafka_client); end
22
23
 
23
- def stop
24
- end
24
+ def stop; end
25
25
  end
26
26
  end
27
27
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/notifications'
2
4
 
3
5
  module Phobos
@@ -15,8 +17,8 @@ module Phobos
15
17
  end
16
18
 
17
19
  def instrument(event, extra = {})
18
- ActiveSupport::Notifications.instrument("#{NAMESPACE}.#{event}", extra) do |extra|
19
- yield(extra) if block_given?
20
+ ActiveSupport::Notifications.instrument("#{NAMESPACE}.#{event}", extra) do |args|
21
+ yield(args) if block_given?
20
22
  end
21
23
  end
22
24
  end
@@ -1,30 +1,24 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Phobos
4
+ # rubocop:disable Metrics/ParameterLists, Metrics/ClassLength
2
5
  class Listener
3
6
  include Phobos::Instrumentation
7
+ include Phobos::Log
4
8
 
5
- KAFKA_CONSUMER_OPTS = %i(
6
- session_timeout
7
- offset_commit_interval
8
- offset_commit_threshold
9
- heartbeat_interval
10
- offset_retention_time
11
- ).freeze
12
-
13
- DEFAULT_MAX_BYTES_PER_PARTITION = 1048576 # 1 MB
9
+ DEFAULT_MAX_BYTES_PER_PARTITION = 1_048_576 # 1 MB
14
10
  DELIVERY_OPTS = %w[batch message].freeze
15
11
 
16
12
  attr_reader :group_id, :topic, :id
17
13
  attr_reader :handler_class, :encoding
18
14
 
19
- def initialize(handler:, group_id:, topic:, min_bytes: nil,
20
- max_wait_time: nil, force_encoding: nil,
21
- start_from_beginning: true, backoff: nil,
22
- delivery: 'batch',
23
- max_bytes_per_partition: DEFAULT_MAX_BYTES_PER_PARTITION,
15
+ # rubocop:disable Metrics/MethodLength
16
+ def initialize(handler:, group_id:, topic:, min_bytes: nil, max_wait_time: nil,
17
+ force_encoding: nil, start_from_beginning: true, backoff: nil,
18
+ delivery: 'batch', max_bytes_per_partition: DEFAULT_MAX_BYTES_PER_PARTITION,
24
19
  session_timeout: nil, offset_commit_interval: nil,
25
20
  heartbeat_interval: nil, offset_commit_threshold: nil,
26
- offset_retention_time: nil
27
- )
21
+ offset_retention_time: nil)
28
22
  @id = SecureRandom.hex[0...6]
29
23
  @handler_class = handler
30
24
  @group_id = group_id
@@ -32,14 +26,11 @@ module Phobos
32
26
  @backoff = backoff
33
27
  @delivery = delivery.to_s
34
28
  @subscribe_opts = {
35
- start_from_beginning: start_from_beginning,
36
- max_bytes_per_partition: max_bytes_per_partition
29
+ start_from_beginning: start_from_beginning, max_bytes_per_partition: max_bytes_per_partition
37
30
  }
38
31
  @kafka_consumer_opts = compact(
39
- session_timeout: session_timeout,
40
- offset_commit_interval: offset_commit_interval,
41
- heartbeat_interval: heartbeat_interval,
42
- offset_retention_time: offset_retention_time,
32
+ session_timeout: session_timeout, offset_retention_time: offset_retention_time,
33
+ offset_commit_interval: offset_commit_interval, heartbeat_interval: heartbeat_interval,
43
34
  offset_commit_threshold: offset_commit_threshold
44
35
  )
45
36
  @encoding = Encoding.const_get(force_encoding.to_sym) if force_encoding
@@ -47,9 +38,59 @@ module Phobos
47
38
  @kafka_client = Phobos.create_kafka_client
48
39
  @producer_enabled = @handler_class.ancestors.include?(Phobos::Producer)
49
40
  end
41
+ # rubocop:enable Metrics/MethodLength
50
42
 
51
43
  def start
52
44
  @signal_to_stop = false
45
+
46
+ start_listener
47
+
48
+ begin
49
+ start_consumer_loop
50
+ rescue Kafka::ProcessingError, Phobos::AbortError
51
+ # Abort is an exception to prevent the consumer from committing the offset.
52
+ # Since "listener" had a message being retried while "stop" was called
53
+ # it's wise to not commit the batch offset to avoid data loss. This will
54
+ # cause some messages to be reprocessed
55
+ instrument('listener.retry_aborted', listener_metadata) do
56
+ log_info('Retry loop aborted, listener is shutting down', listener_metadata)
57
+ end
58
+ end
59
+ ensure
60
+ stop_listener
61
+ end
62
+
63
+ def stop
64
+ return if should_stop?
65
+
66
+ instrument('listener.stopping', listener_metadata) do
67
+ log_info('Listener stopping', listener_metadata)
68
+ @consumer&.stop
69
+ @signal_to_stop = true
70
+ end
71
+ end
72
+
73
+ def create_exponential_backoff
74
+ Phobos.create_exponential_backoff(@backoff)
75
+ end
76
+
77
+ def should_stop?
78
+ @signal_to_stop == true
79
+ end
80
+
81
+ def send_heartbeat_if_necessary
82
+ raise Phobos::AbortError if should_stop?
83
+
84
+ @consumer&.send_heartbeat_if_necessary
85
+ end
86
+
87
+ private
88
+
89
+ def listener_metadata
90
+ { listener_id: id, group_id: group_id, topic: topic, handler: handler_class.to_s }
91
+ end
92
+
93
+ def start_listener
53
94
  instrument('listener.start', listener_metadata) do
54
95
  @consumer = create_kafka_consumer
55
96
  @consumer.subscribe(topic, @subscribe_opts)
@@ -58,25 +99,14 @@ module Phobos
58
99
  # since "start" blocks a thread might be used to call it
59
100
  @handler_class.producer.configure_kafka_client(@kafka_client) if @producer_enabled
60
101
 
61
- instrument('listener.start_handler', listener_metadata) { @handler_class.start(@kafka_client) }
62
- Phobos.logger.info { Hash(message: 'Listener started').merge(listener_metadata) }
63
- end
64
-
65
- begin
66
- @delivery == 'batch' ? consume_each_batch : consume_each_message
67
-
68
- # Abort is an exception to prevent the consumer from committing the offset.
69
- # Since "listener" had a message being retried while "stop" was called
70
- # it's wise to not commit the batch offset to avoid data loss. This will
71
- # cause some messages to be reprocessed
72
- #
73
- rescue Kafka::ProcessingError, Phobos::AbortError
74
- instrument('listener.retry_aborted', listener_metadata) do
75
- Phobos.logger.info({ message: 'Retry loop aborted, listener is shutting down' }.merge(listener_metadata))
102
+ instrument('listener.start_handler', listener_metadata) do
103
+ @handler_class.start(@kafka_client)
76
104
  end
105
+ log_info('Listener started', listener_metadata)
77
106
  end
107
+ end
78
108
 
79
- ensure
109
+ def stop_listener
80
110
  instrument('listener.stop', listener_metadata) do
81
111
  instrument('listener.stop_handler', listener_metadata) { @handler_class.stop }
82
112
 
@@ -88,12 +118,14 @@ module Phobos
88
118
  end
89
119
 
90
120
  @kafka_client.close
91
- if should_stop?
92
- Phobos.logger.info { Hash(message: 'Listener stopped').merge(listener_metadata) }
93
- end
121
+ log_info('Listener stopped', listener_metadata) if should_stop?
94
122
  end
95
123
  end
96
124
 
125
+ def start_consumer_loop
126
+ @delivery == 'batch' ? consume_each_batch : consume_each_message
127
+ end
128
+
97
129
  def consume_each_batch
98
130
  @consumer.each_batch(@message_processing_opts) do |batch|
99
131
  batch_processor = Phobos::Actions::ProcessBatch.new(
@@ -103,8 +135,8 @@ module Phobos
103
135
  )
104
136
 
105
137
  batch_processor.execute
106
- Phobos.logger.debug { Hash(message: 'Committed offset').merge(batch_processor.metadata) }
107
- return if should_stop?
138
+ log_debug('Committed offset', batch_processor.metadata)
139
+ return nil if should_stop?
108
140
  end
109
141
  end
110
142
 
@@ -117,41 +149,15 @@ module Phobos
117
149
  )
118
150
 
119
151
  message_processor.execute
120
- Phobos.logger.debug { Hash(message: 'Committed offset').merge(message_processor.metadata) }
121
- return if should_stop?
122
- end
123
- end
124
-
125
- def stop
126
- return if should_stop?
127
- instrument('listener.stopping', listener_metadata) do
128
- Phobos.logger.info { Hash(message: 'Listener stopping').merge(listener_metadata) }
129
- @consumer&.stop
130
- @signal_to_stop = true
152
+ log_debug('Committed offset', message_processor.metadata)
153
+ return nil if should_stop?
131
154
  end
132
155
  end
133
156
 
134
- def create_exponential_backoff
135
- Phobos.create_exponential_backoff(@backoff)
136
- end
137
-
138
- def should_stop?
139
- @signal_to_stop == true
140
- end
141
-
142
- def send_heartbeat_if_necessary
143
- raise Phobos::AbortError if should_stop?
144
- @consumer&.send_heartbeat_if_necessary
145
- end
146
-
147
- private
148
-
149
- def listener_metadata
150
- { listener_id: id, group_id: group_id, topic: topic, handler: handler_class.to_s }
151
- end
152
-
153
157
  def create_kafka_consumer
154
- configs = Phobos.config.consumer_hash.select { |k| KAFKA_CONSUMER_OPTS.include?(k) }
158
+ configs = Phobos.config.consumer_hash.select do |k|
159
+ Constants::KAFKA_CONSUMER_OPTS.include?(k)
160
+ end
155
161
  configs.merge!(@kafka_consumer_opts)
156
162
  @kafka_client.consumer({ group_id: group_id }.merge(configs))
157
163
  end
@@ -160,4 +166,5 @@ module Phobos
160
166
  hash.delete_if { |_, v| v.nil? }
161
167
  end
162
168
  end
169
+ # rubocop:enable Metrics/ParameterLists, Metrics/ClassLength
163
170
  end