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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.dockerignore +13 -0
  3. data/.env +1 -0
  4. data/.gitignore +16 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +26 -0
  7. data/.rubocop_common.yml +29 -0
  8. data/.rubocop_todo.yml +7 -0
  9. data/.rubosync.yml +2 -0
  10. data/.ruby-version +1 -0
  11. data/.travis.yml +37 -0
  12. data/CHANGELOG.md +170 -0
  13. data/Dockerfile +14 -0
  14. data/Gemfile +8 -0
  15. data/LICENSE.txt +176 -0
  16. data/README.md +699 -0
  17. data/Rakefile +8 -0
  18. data/bin/console +19 -0
  19. data/bin/phobos +10 -0
  20. data/bin/setup +8 -0
  21. data/config/phobos.yml.example +137 -0
  22. data/docker-compose.yml +28 -0
  23. data/examples/handler_saving_events_database.rb +51 -0
  24. data/examples/handler_using_async_producer.rb +17 -0
  25. data/examples/publishing_messages_without_consumer.rb +82 -0
  26. data/lib/phobos/actions/process_batch.rb +35 -0
  27. data/lib/phobos/actions/process_batch_inline.rb +61 -0
  28. data/lib/phobos/actions/process_message.rb +49 -0
  29. data/lib/phobos/batch_handler.rb +23 -0
  30. data/lib/phobos/batch_message.rb +21 -0
  31. data/lib/phobos/cli.rb +69 -0
  32. data/lib/phobos/cli/runner.rb +48 -0
  33. data/lib/phobos/cli/start.rb +71 -0
  34. data/lib/phobos/constants.rb +33 -0
  35. data/lib/phobos/deep_struct.rb +39 -0
  36. data/lib/phobos/echo_handler.rb +11 -0
  37. data/lib/phobos/errors.rb +6 -0
  38. data/lib/phobos/executor.rb +103 -0
  39. data/lib/phobos/handler.rb +23 -0
  40. data/lib/phobos/instrumentation.rb +25 -0
  41. data/lib/phobos/listener.rb +192 -0
  42. data/lib/phobos/log.rb +23 -0
  43. data/lib/phobos/processor.rb +67 -0
  44. data/lib/phobos/producer.rb +171 -0
  45. data/lib/phobos/test.rb +3 -0
  46. data/lib/phobos/test/helper.rb +29 -0
  47. data/lib/phobos/version.rb +5 -0
  48. data/lib/phobos_temp_fork.rb +175 -0
  49. data/logo.png +0 -0
  50. data/phobos.gemspec +69 -0
  51. data/phobos_boot.rb +31 -0
  52. data/utils/create-topic.sh +13 -0
  53. 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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phobos
4
+ class EchoHandler
5
+ include Phobos::Handler
6
+
7
+ def consume(message, metadata)
8
+ Phobos.logger.info { Hash(message: message).merge(metadata) }
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phobos
4
+ class Error < StandardError; end
5
+ class AbortError < Error; end
6
+ 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