phobos_temp_fork 0.0.1
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 +7 -0
- data/.dockerignore +13 -0
- data/.env +1 -0
- data/.gitignore +16 -0
- data/.rspec +3 -0
- data/.rubocop.yml +26 -0
- data/.rubocop_common.yml +29 -0
- data/.rubocop_todo.yml +7 -0
- data/.rubosync.yml +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +37 -0
- data/CHANGELOG.md +170 -0
- data/Dockerfile +14 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +176 -0
- data/README.md +699 -0
- data/Rakefile +8 -0
- data/bin/console +19 -0
- data/bin/phobos +10 -0
- data/bin/setup +8 -0
- data/config/phobos.yml.example +137 -0
- data/docker-compose.yml +28 -0
- data/examples/handler_saving_events_database.rb +51 -0
- data/examples/handler_using_async_producer.rb +17 -0
- data/examples/publishing_messages_without_consumer.rb +82 -0
- data/lib/phobos/actions/process_batch.rb +35 -0
- data/lib/phobos/actions/process_batch_inline.rb +61 -0
- data/lib/phobos/actions/process_message.rb +49 -0
- data/lib/phobos/batch_handler.rb +23 -0
- data/lib/phobos/batch_message.rb +21 -0
- data/lib/phobos/cli.rb +69 -0
- data/lib/phobos/cli/runner.rb +48 -0
- data/lib/phobos/cli/start.rb +71 -0
- data/lib/phobos/constants.rb +33 -0
- data/lib/phobos/deep_struct.rb +39 -0
- data/lib/phobos/echo_handler.rb +11 -0
- data/lib/phobos/errors.rb +6 -0
- data/lib/phobos/executor.rb +103 -0
- data/lib/phobos/handler.rb +23 -0
- data/lib/phobos/instrumentation.rb +25 -0
- data/lib/phobos/listener.rb +192 -0
- data/lib/phobos/log.rb +23 -0
- data/lib/phobos/processor.rb +67 -0
- data/lib/phobos/producer.rb +171 -0
- data/lib/phobos/test.rb +3 -0
- data/lib/phobos/test/helper.rb +29 -0
- data/lib/phobos/version.rb +5 -0
- data/lib/phobos_temp_fork.rb +175 -0
- data/logo.png +0 -0
- data/phobos.gemspec +69 -0
- data/phobos_boot.rb +31 -0
- data/utils/create-topic.sh +13 -0
- metadata +308 -0
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'phobos/processor'
|
4
|
+
|
5
|
+
module Phobos
|
6
|
+
module Actions
|
7
|
+
class ProcessMessage
|
8
|
+
include Phobos::Processor
|
9
|
+
|
10
|
+
attr_reader :metadata
|
11
|
+
|
12
|
+
def initialize(listener:, message:, listener_metadata:)
|
13
|
+
@listener = listener
|
14
|
+
@message = message
|
15
|
+
@metadata = listener_metadata.merge(
|
16
|
+
key: message.key,
|
17
|
+
partition: message.partition,
|
18
|
+
offset: message.offset,
|
19
|
+
retry_count: 0,
|
20
|
+
headers: message.headers
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def execute
|
25
|
+
payload = force_encoding(@message.value)
|
26
|
+
|
27
|
+
begin
|
28
|
+
process_message(payload)
|
29
|
+
rescue StandardError => e
|
30
|
+
handle_error(e, 'listener.retry_handler_error',
|
31
|
+
"error processing message, waiting #{backoff_interval}s")
|
32
|
+
retry
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def process_message(payload)
|
39
|
+
instrument('listener.process_message', @metadata) do
|
40
|
+
handler = @listener.handler_class.new
|
41
|
+
|
42
|
+
handler.around_consume(payload, @metadata) do |around_payload, around_metadata|
|
43
|
+
handler.consume(around_payload, around_metadata)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Phobos
|
4
|
+
module BatchHandler
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
def consume_batch(_payloads, _metadata)
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
|
13
|
+
def around_consume_batch(payloads, metadata)
|
14
|
+
yield payloads, metadata
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
def start(kafka_client); end
|
19
|
+
|
20
|
+
def stop; end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Phobos
|
4
|
+
class BatchMessage
|
5
|
+
attr_accessor :key, :partition, :offset, :payload, :headers
|
6
|
+
|
7
|
+
def initialize(key:, partition:, offset:, payload:, headers:)
|
8
|
+
@key = key
|
9
|
+
@partition = partition
|
10
|
+
@offset = offset
|
11
|
+
@payload = payload
|
12
|
+
@headers = headers
|
13
|
+
end
|
14
|
+
|
15
|
+
def ==(other)
|
16
|
+
[:key, :partition, :offset, :payload, :headers].all? do |s|
|
17
|
+
public_send(s) == other.public_send(s)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/phobos/cli.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
require 'phobos/cli/start'
|
5
|
+
|
6
|
+
module Phobos
|
7
|
+
module CLI
|
8
|
+
def self.logger
|
9
|
+
@logger ||= Logging.logger[self].tap do |l|
|
10
|
+
l.appenders = [Logging.appenders.stdout]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Commands < Thor
|
15
|
+
include Thor::Actions
|
16
|
+
|
17
|
+
map '-v' => :version
|
18
|
+
map '--version' => :version
|
19
|
+
|
20
|
+
desc 'version', 'Outputs the version number. Can be used with: phobos -v or phobos --version'
|
21
|
+
def version
|
22
|
+
puts Phobos::VERSION
|
23
|
+
end
|
24
|
+
|
25
|
+
desc 'init', 'Initialize your project with Phobos'
|
26
|
+
def init
|
27
|
+
copy_file 'config/phobos.yml.example', 'config/phobos.yml'
|
28
|
+
create_file 'phobos_boot.rb' do
|
29
|
+
<<~EXAMPLE
|
30
|
+
# Use this file to load your code
|
31
|
+
puts <<~ART
|
32
|
+
______ _ _
|
33
|
+
| ___ \\\\ | | |
|
34
|
+
| |_/ / |__ ___ | |__ ___ ___
|
35
|
+
| __/| '_ \\\\ / _ \\\\| '_ \\\\ / _ \\\\/ __|
|
36
|
+
| | | | | | (_) | |_) | (_) \\\\__ \\\\
|
37
|
+
\\\\_| |_| |_|\\\\___/|_.__/ \\\\___/|___/
|
38
|
+
ART
|
39
|
+
puts "\nphobos_boot.rb - find this file at \#{File.expand_path(__FILE__)}\n\n"
|
40
|
+
EXAMPLE
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
desc 'start', 'Starts Phobos'
|
45
|
+
method_option :config,
|
46
|
+
aliases: ['-c'],
|
47
|
+
default: 'config/phobos.yml',
|
48
|
+
banner: 'Configuration file'
|
49
|
+
method_option :boot,
|
50
|
+
aliases: ['-b'],
|
51
|
+
banner: 'File path to load application specific code',
|
52
|
+
default: 'phobos_boot.rb'
|
53
|
+
method_option :listeners,
|
54
|
+
aliases: ['-l'],
|
55
|
+
banner: 'Separate listeners config file (optional)'
|
56
|
+
method_option :skip_config,
|
57
|
+
default: false,
|
58
|
+
type: :boolean,
|
59
|
+
banner: 'Skip config file'
|
60
|
+
def start
|
61
|
+
Phobos::CLI::Start.new(options).execute
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.source_root
|
65
|
+
File.expand_path(File.join(File.dirname(__FILE__), '../..'))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Phobos
|
4
|
+
module CLI
|
5
|
+
class Runner
|
6
|
+
SIGNALS = [:INT, :TERM, :QUIT].freeze
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@signal_queue = []
|
10
|
+
@reader, @writer = IO.pipe
|
11
|
+
@executor = Phobos::Executor.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def run!
|
15
|
+
setup_signals
|
16
|
+
executor.start
|
17
|
+
|
18
|
+
loop do
|
19
|
+
case signal_queue.pop
|
20
|
+
when *SIGNALS
|
21
|
+
executor.stop
|
22
|
+
break
|
23
|
+
else
|
24
|
+
ready = IO.select([reader, writer])
|
25
|
+
|
26
|
+
# drain the self-pipe so it won't be returned again next time
|
27
|
+
reader.read_nonblock(1) if ready[0].include?(reader)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :reader, :writer, :signal_queue, :executor
|
35
|
+
|
36
|
+
def setup_signals
|
37
|
+
SIGNALS.each do |signal|
|
38
|
+
Signal.trap(signal) { unblock(signal) }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def unblock(signal)
|
43
|
+
writer.write_nonblock('.')
|
44
|
+
signal_queue << signal
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'phobos/cli/runner'
|
4
|
+
|
5
|
+
module Phobos
|
6
|
+
module CLI
|
7
|
+
class Start
|
8
|
+
def initialize(options)
|
9
|
+
@config_file = File.expand_path(options[:config]) unless options[:skip_config]
|
10
|
+
@boot_file = File.expand_path(options[:boot])
|
11
|
+
|
12
|
+
@listeners_file = File.expand_path(options[:listeners]) if options[:listeners]
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute
|
16
|
+
load_boot_file
|
17
|
+
|
18
|
+
if config_file
|
19
|
+
validate_config_file!
|
20
|
+
Phobos.configure(config_file)
|
21
|
+
end
|
22
|
+
|
23
|
+
Phobos.add_listeners(listeners_file) if listeners_file
|
24
|
+
|
25
|
+
validate_listeners!
|
26
|
+
|
27
|
+
Phobos::CLI::Runner.new.run!
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :config_file, :boot_file, :listeners_file
|
33
|
+
|
34
|
+
def validate_config_file!
|
35
|
+
File.exist?(config_file) || error_exit("Config file not found (#{config_file})")
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate_listeners! # rubocop:disable Metrics/MethodLength
|
39
|
+
Phobos.config.listeners.each do |listener|
|
40
|
+
handler = listener.handler
|
41
|
+
|
42
|
+
begin
|
43
|
+
handler.constantize
|
44
|
+
rescue NameError
|
45
|
+
error_exit("Handler '#{handler}' not defined")
|
46
|
+
end
|
47
|
+
|
48
|
+
delivery = listener.delivery
|
49
|
+
if delivery.nil?
|
50
|
+
Phobos::CLI.logger.warn do
|
51
|
+
Hash(message: "Delivery option should be specified, defaulting to 'batch'"\
|
52
|
+
' - specify this option to silence this message')
|
53
|
+
end
|
54
|
+
elsif !Listener::DELIVERY_OPTS.include?(delivery)
|
55
|
+
error_exit("Invalid delivery option '#{delivery}'. Please specify one of: "\
|
56
|
+
"#{Listener::DELIVERY_OPTS.join(', ')}")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def error_exit(msg)
|
62
|
+
Phobos::CLI.logger.error { Hash(message: msg) }
|
63
|
+
exit(1)
|
64
|
+
end
|
65
|
+
|
66
|
+
def load_boot_file
|
67
|
+
load(boot_file) if File.exist?(boot_file)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
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
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Please use this with at least the same consideration as you would when using OpenStruct.
|
4
|
+
# Right now we only use this to parse our internal configuration files. It is not meant to
|
5
|
+
# be used on incoming data.
|
6
|
+
module Phobos
|
7
|
+
class DeepStruct < OpenStruct
|
8
|
+
# Based on
|
9
|
+
# https://docs.omniref.com/ruby/2.3.0/files/lib/ostruct.rb#line=88
|
10
|
+
def initialize(hash = nil)
|
11
|
+
super
|
12
|
+
@hash_table = {}
|
13
|
+
|
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
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_h
|
22
|
+
@hash_table.dup
|
23
|
+
end
|
24
|
+
alias to_hash to_h
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def to_deep_struct(value)
|
29
|
+
case value
|
30
|
+
when Hash
|
31
|
+
self.class.new(value)
|
32
|
+
when Enumerable
|
33
|
+
value.map { |el| to_deep_struct(el) }
|
34
|
+
else
|
35
|
+
value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Phobos
|
4
|
+
class Executor
|
5
|
+
include Phobos::Instrumentation
|
6
|
+
include Phobos::Log
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@threads = Concurrent::Array.new
|
10
|
+
@listeners = Phobos.config.listeners.flat_map do |config|
|
11
|
+
handler_class = config.handler.constantize
|
12
|
+
listener_configs = config.to_hash.deep_symbolize_keys
|
13
|
+
max_concurrency = listener_configs[:max_concurrency] || 1
|
14
|
+
Array.new(max_concurrency).map do
|
15
|
+
configs = listener_configs.select { |k| Constants::LISTENER_OPTS.include?(k) }
|
16
|
+
Phobos::Listener.new(**configs.merge(handler: handler_class))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def start
|
22
|
+
@signal_to_stop = false
|
23
|
+
@threads.clear
|
24
|
+
@thread_pool = Concurrent::FixedThreadPool.new(@listeners.size)
|
25
|
+
|
26
|
+
@listeners.each do |listener|
|
27
|
+
@thread_pool.post do
|
28
|
+
thread = Thread.current
|
29
|
+
thread.abort_on_exception = true
|
30
|
+
@threads << thread
|
31
|
+
run_listener(listener)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
def stop
|
39
|
+
return if @signal_to_stop
|
40
|
+
|
41
|
+
instrument('executor.stop') do
|
42
|
+
@signal_to_stop = true
|
43
|
+
@listeners.each(&:stop)
|
44
|
+
@threads.select(&:alive?).each do |thread|
|
45
|
+
begin
|
46
|
+
thread.wakeup
|
47
|
+
rescue StandardError
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
@thread_pool&.shutdown
|
52
|
+
@thread_pool&.wait_for_termination
|
53
|
+
Phobos.logger.info { Hash(message: 'Executor stopped') }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def error_metadata(exception)
|
60
|
+
{
|
61
|
+
exception_class: exception.class.name,
|
62
|
+
exception_message: exception.message,
|
63
|
+
backtrace: exception.backtrace
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
# rubocop:disable Lint/RescueException
|
68
|
+
def run_listener(listener)
|
69
|
+
retry_count = 0
|
70
|
+
|
71
|
+
begin
|
72
|
+
listener.start
|
73
|
+
rescue Exception => e
|
74
|
+
handle_crashed_listener(listener, e, retry_count)
|
75
|
+
retry_count += 1
|
76
|
+
retry unless @signal_to_stop
|
77
|
+
end
|
78
|
+
rescue Exception => e
|
79
|
+
log_error("Failed to run listener (#{e.message})", error_metadata(e))
|
80
|
+
raise e
|
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
|
102
|
+
end
|
103
|
+
end
|