karafka 1.2.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/.coditsu.yml +3 -0
  3. data/.console_irbrc +13 -0
  4. data/.gitignore +68 -0
  5. data/.rspec +1 -0
  6. data/.ruby-gemset +1 -0
  7. data/.ruby-version +1 -0
  8. data/.travis.yml +49 -0
  9. data/CHANGELOG.md +458 -0
  10. data/CODE_OF_CONDUCT.md +46 -0
  11. data/CONTRIBUTING.md +41 -0
  12. data/Gemfile +15 -0
  13. data/Gemfile.lock +126 -0
  14. data/MIT-LICENCE +18 -0
  15. data/README.md +102 -0
  16. data/bin/karafka +19 -0
  17. data/config/errors.yml +6 -0
  18. data/karafka.gemspec +42 -0
  19. data/lib/karafka.rb +79 -0
  20. data/lib/karafka/app.rb +45 -0
  21. data/lib/karafka/attributes_map.rb +69 -0
  22. data/lib/karafka/backends/inline.rb +16 -0
  23. data/lib/karafka/base_consumer.rb +68 -0
  24. data/lib/karafka/base_responder.rb +208 -0
  25. data/lib/karafka/callbacks.rb +30 -0
  26. data/lib/karafka/callbacks/config.rb +22 -0
  27. data/lib/karafka/callbacks/dsl.rb +16 -0
  28. data/lib/karafka/cli.rb +54 -0
  29. data/lib/karafka/cli/base.rb +78 -0
  30. data/lib/karafka/cli/console.rb +29 -0
  31. data/lib/karafka/cli/flow.rb +46 -0
  32. data/lib/karafka/cli/info.rb +29 -0
  33. data/lib/karafka/cli/install.rb +42 -0
  34. data/lib/karafka/cli/server.rb +66 -0
  35. data/lib/karafka/connection/api_adapter.rb +148 -0
  36. data/lib/karafka/connection/builder.rb +16 -0
  37. data/lib/karafka/connection/client.rb +107 -0
  38. data/lib/karafka/connection/delegator.rb +46 -0
  39. data/lib/karafka/connection/listener.rb +60 -0
  40. data/lib/karafka/consumers/callbacks.rb +54 -0
  41. data/lib/karafka/consumers/includer.rb +51 -0
  42. data/lib/karafka/consumers/responders.rb +24 -0
  43. data/lib/karafka/consumers/single_params.rb +15 -0
  44. data/lib/karafka/errors.rb +50 -0
  45. data/lib/karafka/fetcher.rb +44 -0
  46. data/lib/karafka/helpers/class_matcher.rb +78 -0
  47. data/lib/karafka/helpers/config_retriever.rb +46 -0
  48. data/lib/karafka/helpers/multi_delegator.rb +33 -0
  49. data/lib/karafka/instrumentation/listener.rb +112 -0
  50. data/lib/karafka/instrumentation/logger.rb +55 -0
  51. data/lib/karafka/instrumentation/monitor.rb +64 -0
  52. data/lib/karafka/loader.rb +28 -0
  53. data/lib/karafka/params/dsl.rb +158 -0
  54. data/lib/karafka/params/params_batch.rb +46 -0
  55. data/lib/karafka/parsers/json.rb +38 -0
  56. data/lib/karafka/patches/dry_configurable.rb +33 -0
  57. data/lib/karafka/patches/ruby_kafka.rb +34 -0
  58. data/lib/karafka/persistence/client.rb +25 -0
  59. data/lib/karafka/persistence/consumer.rb +38 -0
  60. data/lib/karafka/persistence/topic.rb +29 -0
  61. data/lib/karafka/process.rb +62 -0
  62. data/lib/karafka/responders/builder.rb +36 -0
  63. data/lib/karafka/responders/topic.rb +57 -0
  64. data/lib/karafka/routing/builder.rb +61 -0
  65. data/lib/karafka/routing/consumer_group.rb +61 -0
  66. data/lib/karafka/routing/consumer_mapper.rb +34 -0
  67. data/lib/karafka/routing/proxy.rb +37 -0
  68. data/lib/karafka/routing/router.rb +29 -0
  69. data/lib/karafka/routing/topic.rb +60 -0
  70. data/lib/karafka/routing/topic_mapper.rb +55 -0
  71. data/lib/karafka/schemas/config.rb +24 -0
  72. data/lib/karafka/schemas/consumer_group.rb +78 -0
  73. data/lib/karafka/schemas/consumer_group_topic.rb +18 -0
  74. data/lib/karafka/schemas/responder_usage.rb +39 -0
  75. data/lib/karafka/schemas/server_cli_options.rb +43 -0
  76. data/lib/karafka/server.rb +85 -0
  77. data/lib/karafka/setup/config.rb +193 -0
  78. data/lib/karafka/setup/configurators/base.rb +29 -0
  79. data/lib/karafka/setup/configurators/params.rb +25 -0
  80. data/lib/karafka/setup/configurators/water_drop.rb +32 -0
  81. data/lib/karafka/setup/dsl.rb +22 -0
  82. data/lib/karafka/status.rb +25 -0
  83. data/lib/karafka/templates/application_consumer.rb.example +6 -0
  84. data/lib/karafka/templates/application_responder.rb.example +11 -0
  85. data/lib/karafka/templates/karafka.rb.example +54 -0
  86. data/lib/karafka/version.rb +7 -0
  87. data/log/.gitkeep +0 -0
  88. metadata +303 -0
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Callbacks
5
+ # App level dsl to define callbacks
6
+ module Dsl
7
+ Callbacks::TYPES.each do |callback_type|
8
+ # Allows us to define a block, that will be executed for a given moment
9
+ # @param [Block] block that should be executed after the initialization process
10
+ define_method callback_type do |&block|
11
+ config.callbacks.send(callback_type).push block
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ # Karafka framework Cli
5
+ # If you want to add/modify command that belongs to CLI, please review all commands
6
+ # available in cli/ directory inside Karafka source code.
7
+ #
8
+ # @note Whole Cli is built using Thor
9
+ # @see https://github.com/erikhuda/thor
10
+ class Cli < Thor
11
+ package_name 'Karafka'
12
+
13
+ class << self
14
+ # Loads all Cli commands into Thor framework
15
+ # This method should be executed before we run Karafka::Cli.start, otherwise we won't
16
+ # have any Cli commands available
17
+ def prepare
18
+ cli_commands.each do |action|
19
+ action.bind_to(self)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ # @return [Array<Class>] Array with Cli action classes that can be used as commands
26
+ def cli_commands
27
+ constants
28
+ .map! { |object| const_get(object) }
29
+ .keep_if do |object|
30
+ object.instance_of?(Class) && (object < Cli::Base)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ # This is kinda trick - since we don't have a autoload and other magic stuff
38
+ # like Rails does, so instead this method allows us to replace currently running
39
+ # console with a new one via Kernel.exec. It will start console with new code loaded
40
+ # Yes we know that it is not turbofast, however it is turbo convinient and small
41
+ #
42
+ # Also - the KARAFKA_CONSOLE is used to detect that we're executing the irb session
43
+ # so this method is only available when the Karafka console is running
44
+ #
45
+ # We skip this because this should exist and be only valid in the console
46
+ # :nocov:
47
+ if ENV['KARAFKA_CONSOLE']
48
+ # Reloads Karafka irb console session
49
+ def reload!
50
+ puts "Reloading...\n"
51
+ Kernel.exec Karafka::Cli::Console.command
52
+ end
53
+ end
54
+ # :nocov:
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ class Cli < Thor
5
+ # Base class for all the command that we want to define
6
+ # This base class provides a nicer interface to Thor and allows to easier separate single
7
+ # independent commands
8
+ # In order to define a new command you need to:
9
+ # - specify its desc
10
+ # - implement call method
11
+ #
12
+ # @example Create a dummy command
13
+ # class Dummy < Base
14
+ # self.desc = 'Dummy command'
15
+ #
16
+ # def call
17
+ # puts 'I'm doing nothing!
18
+ # end
19
+ # end
20
+ class Base
21
+ include Thor::Shell
22
+
23
+ # We can use it to call other cli methods via this object
24
+ attr_reader :cli
25
+
26
+ # @param cli [Karafka::Cli] current Karafka Cli instance
27
+ def initialize(cli)
28
+ @cli = cli
29
+ end
30
+
31
+ # This method should implement proper cli action
32
+ def call
33
+ raise NotImplementedError, 'Implement this in a subclass'
34
+ end
35
+
36
+ class << self
37
+ # Allows to set options for Thor cli
38
+ # @see https://github.com/erikhuda/thor
39
+ # @param option Single option details
40
+ def option(*option)
41
+ @options ||= []
42
+ @options << option
43
+ end
44
+
45
+ # Allows to set description of a given cli command
46
+ # @param desc [String] Description of a given cli command
47
+ def desc(desc)
48
+ @desc ||= desc
49
+ end
50
+
51
+ # This method will bind a given Cli command into Karafka Cli
52
+ # This method is a wrapper to way Thor defines its commands
53
+ # @param cli_class [Karafka::Cli] Karafka cli_class
54
+ def bind_to(cli_class)
55
+ cli_class.desc name, @desc
56
+
57
+ (@options || []).each { |option| cli_class.option(*option) }
58
+
59
+ context = self
60
+
61
+ cli_class.send :define_method, name do |*args|
62
+ context.new(self).call(*args)
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ # @return [String] downcased current class name that we use to define name for
69
+ # given Cli command
70
+ # @example for Karafka::Cli::Install
71
+ # name #=> 'install'
72
+ def name
73
+ to_s.split('::').last.downcase
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ # Karafka framework Cli
5
+ class Cli < Thor
6
+ # Console Karafka Cli action
7
+ class Console < Base
8
+ desc 'Start the Karafka console (short-cut alias: "c")'
9
+ option aliases: 'c'
10
+
11
+ # @return [String] Console executing command
12
+ # @example
13
+ # Karafka::Cli::Console.command #=> 'KARAFKA_CONSOLE=true bundle exec irb...'
14
+ def self.command
15
+ envs = [
16
+ "IRBRC='#{Karafka.gem_root}/.console_irbrc'",
17
+ 'KARAFKA_CONSOLE=true'
18
+ ]
19
+ "#{envs.join(' ')} bundle exec irb"
20
+ end
21
+
22
+ # Start the Karafka console
23
+ def call
24
+ cli.info
25
+ exec self.class.command
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ # Karafka framework Cli
5
+ class Cli < Thor
6
+ # Description of topics flow (incoming/outgoing)
7
+ class Flow < Base
8
+ desc 'Print application data flow (incoming => outgoing)'
9
+
10
+ # Print out all defined routes in alphabetical order
11
+ def call
12
+ topics.each do |topic|
13
+ any_topics = !topic.responder&.topics.nil?
14
+
15
+ if any_topics
16
+ puts "#{topic.name} =>"
17
+
18
+ topic.responder.topics.each_value do |responder_topic|
19
+ features = []
20
+ features << (responder_topic.required? ? 'always' : 'conditionally')
21
+ features << (responder_topic.multiple_usage? ? 'one or more' : 'exactly once')
22
+
23
+ print responder_topic.name, "(#{features.join(', ')})"
24
+ end
25
+ else
26
+ puts "#{topic.name} => (nothing)"
27
+ end
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ # @return [Array<Karafka::Routing::Topic>] all topics sorted in alphabetical order
34
+ def topics
35
+ Karafka::App.consumer_groups.map(&:topics).flatten.sort_by(&:name)
36
+ end
37
+
38
+ # Prints a given value with label in a nice way
39
+ # @param label [String] label describing value
40
+ # @param value [String] value that should be printed
41
+ def print(label, value)
42
+ printf "%-25s %s\n", " - #{label}:", value
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ # Karafka framework Cli
5
+ class Cli < Thor
6
+ # Info Karafka Cli action
7
+ class Info < Base
8
+ desc 'Print configuration details and other options of your application'
9
+
10
+ # Print configuration details and other options of your application
11
+ def call
12
+ config = Karafka::App.config
13
+
14
+ info = [
15
+ "Karafka framework version: #{Karafka::VERSION}",
16
+ "Application client id: #{config.client_id}",
17
+ "Backend: #{config.backend}",
18
+ "Batch fetching: #{config.batch_fetching}",
19
+ "Batch consuming: #{config.batch_consuming}",
20
+ "Boot file: #{Karafka.boot_file}",
21
+ "Environment: #{Karafka.env}",
22
+ "Kafka seed brokers: #{config.kafka.seed_brokers}"
23
+ ]
24
+
25
+ puts(info.join("\n"))
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ # Karafka framework Cli
5
+ class Cli < Thor
6
+ # Install Karafka Cli action
7
+ class Install < Base
8
+ desc 'Install all required things for Karafka application in current directory'
9
+
10
+ # Directories created by default
11
+ INSTALL_DIRS = %w[
12
+ app/consumers
13
+ app/responders
14
+ config
15
+ log
16
+ tmp/pids
17
+ ].freeze
18
+
19
+ # Where should we map proper files from templates
20
+ INSTALL_FILES_MAP = {
21
+ 'karafka.rb.example' => Karafka.boot_file.basename,
22
+ 'application_consumer.rb.example' => 'app/consumers/application_consumer.rb',
23
+ 'application_responder.rb.example' => 'app/responders/application_responder.rb'
24
+ }.freeze
25
+
26
+ # Install all required things for Karafka application in current directory
27
+ def call
28
+ INSTALL_DIRS.each do |dir|
29
+ FileUtils.mkdir_p Karafka.root.join(dir)
30
+ end
31
+
32
+ INSTALL_FILES_MAP.each do |source, target|
33
+ target = Karafka.root.join(target)
34
+ next if File.exist?(target)
35
+
36
+ source = Karafka.core_root.join("templates/#{source}")
37
+ FileUtils.cp_r(source, target)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ # Karafka framework Cli
5
+ class Cli < Thor
6
+ # Server Karafka Cli action
7
+ class Server < Base
8
+ desc 'Start the Karafka server (short-cut alias: "s")'
9
+ option aliases: 's'
10
+ option :daemon, default: false, type: :boolean, aliases: :d
11
+ option :pid, default: 'tmp/pids/karafka', type: :string, aliases: :p
12
+ option :consumer_groups, type: :array, default: nil, aliases: :g
13
+
14
+ # Start the Karafka server
15
+ def call
16
+ validate!
17
+
18
+ puts 'Starting Karafka server'
19
+ cli.info
20
+
21
+ if cli.options[:daemon]
22
+ FileUtils.mkdir_p File.dirname(cli.options[:pid])
23
+ daemonize
24
+ end
25
+
26
+ # We assign active topics on a server level, as only server is expected to listen on
27
+ # part of the topics
28
+ Karafka::Server.consumer_groups = cli.options[:consumer_groups]
29
+
30
+ # Remove pidfile on stop, just before the server instance is going to be GCed
31
+ # We want to delay the moment in which the pidfile is removed as much as we can,
32
+ # so instead of removing it after the server stops running, we rely on the gc moment
33
+ # when this object gets removed (it is a bit later), so it is closer to the actual
34
+ # system process end. We do that, so monitoring and deployment tools that rely on pids
35
+ # won't alarm or start new system process up until the current one is finished
36
+ ObjectSpace.define_finalizer(self, proc { send(:clean) })
37
+
38
+ Karafka::Server.run
39
+ end
40
+
41
+ private
42
+
43
+ # Checks the server cli configuration
44
+ # options validations in terms of app setup (topics, pid existence, etc)
45
+ def validate!
46
+ result = Schemas::ServerCliOptions.call(cli.options)
47
+ return if result.success?
48
+ raise Errors::InvalidConfiguration, result.errors
49
+ end
50
+
51
+ # Detaches current process into background and writes its pidfile
52
+ def daemonize
53
+ ::Process.daemon(true)
54
+ File.open(
55
+ cli.options[:pid],
56
+ 'w'
57
+ ) { |file| file.write(::Process.pid) }
58
+ end
59
+
60
+ # Removes a pidfile (if exist)
61
+ def clean
62
+ FileUtils.rm_f(cli.options[:pid]) if cli.options[:pid]
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ # Namespace for all the things related to Kafka connection
5
+ module Connection
6
+ # Mapper used to convert our internal settings into ruby-kafka settings based on their
7
+ # API requirements.
8
+ # Since ruby-kafka has more and more options and there are few "levels" on which
9
+ # we have to apply them (despite the fact, that in Karafka you configure all of it
10
+ # in one place), we have to remap it into what ruby-kafka driver requires
11
+ # @note The good thing about Kafka.new method is that it ignores all options that
12
+ # do nothing. So we don't have to worry about injecting our internal settings
13
+ # into the client and breaking stuff
14
+ module ApiAdapter
15
+ class << self
16
+ # Builds all the configuration settings for Kafka.new method
17
+ # @return [Array<Hash>] Array with all the client arguments including hash with all
18
+ # the settings required by Kafka.new method
19
+ # @note We return array, so we can inject any arguments we want, in case of changes in the
20
+ # raw driver
21
+ def client
22
+ # This one is a default that takes all the settings except special
23
+ # cases defined in the map
24
+ settings = {
25
+ logger: ::Karafka.logger,
26
+ client_id: ::Karafka::App.config.client_id
27
+ }
28
+
29
+ kafka_configs.each do |setting_name, setting_value|
30
+ # All options for config adapter should be ignored as we're just interested
31
+ # in what is left, as we want to pass all the options that are "typical"
32
+ # and not listed in the api_adapter special cases mapping. All the values
33
+ # from the api_adapter mapping go somewhere else, not to the client directly
34
+ next if AttributesMap.api_adapter.values.flatten.include?(setting_name)
35
+
36
+ settings[setting_name] = setting_value
37
+ end
38
+
39
+ settings_hash = sanitize(settings)
40
+
41
+ # Normalization for the way Kafka::Client accepts arguments from 0.5.3
42
+ [settings_hash.delete(:seed_brokers), settings_hash]
43
+ end
44
+
45
+ # Builds all the configuration settings for kafka#consumer method
46
+ # @param consumer_group [Karafka::Routing::ConsumerGroup] consumer group details
47
+ # @return [Array<Hash>] array with all the consumer arguments including hash with all
48
+ # the settings required by Kafka#consumer
49
+ def consumer(consumer_group)
50
+ settings = { group_id: consumer_group.id }
51
+ settings = fetch_for(:consumer, consumer_group, settings)
52
+ [sanitize(settings)]
53
+ end
54
+
55
+ # Builds all the configuration settings for kafka consumer consume_each_batch and
56
+ # consume_each_message methods
57
+ # @param consumer_group [Karafka::Routing::ConsumerGroup] consumer group details
58
+ # @return [Array<Hash>] Array with all the arguments required by consuming method
59
+ # including hash with all the settings required by
60
+ # Kafka::Consumer#consume_each_message and Kafka::Consumer#consume_each_batch method
61
+ def consumption(consumer_group)
62
+ [
63
+ sanitize(
64
+ fetch_for(
65
+ :consumption,
66
+ consumer_group,
67
+ automatically_mark_as_processed: consumer_group.automatically_mark_as_consumed
68
+ )
69
+ )
70
+ ]
71
+ end
72
+
73
+ # Builds all the configuration settings for kafka consumer#subscribe method
74
+ # @param topic [Karafka::Routing::Topic] topic that holds details for a given subscription
75
+ # @return [Hash] hash with all the settings required by kafka consumer#subscribe method
76
+ def subscribe(topic)
77
+ settings = fetch_for(:subscribe, topic)
78
+ [Karafka::App.config.topic_mapper.outgoing(topic.name), sanitize(settings)]
79
+ end
80
+
81
+ # Builds all the configuration settings required by kafka consumer#pause method
82
+ # @param topic [String] topic that we want to pause
83
+ # @param partition [Integer] number partition that we want to pause
84
+ # @param consumer_group [Karafka::Routing::ConsumerGroup] consumer group details
85
+ # @return [Array] array with all the details required to pause kafka consumer
86
+ def pause(topic, partition, consumer_group)
87
+ [
88
+ Karafka::App.config.topic_mapper.outgoing(topic),
89
+ partition,
90
+ { timeout: consumer_group.pause_timeout }
91
+ ]
92
+ end
93
+
94
+ # Remaps topic details taking the topic mapper feature into consideration.
95
+ # @param params [Karafka::Params::Params] params instance
96
+ # @return [Array] array with all the details needed by ruby-kafka to mark message
97
+ # as processed
98
+ # @note When default empty topic mapper is used, no need for any conversion as the
99
+ # internal and external format are exactly the same
100
+ def mark_message_as_processed(params)
101
+ # Majority of non heroku users don't use custom topic mappers. No need to change
102
+ # anything when it is a default mapper that does not change anything
103
+ return [params] if Karafka::App.config.topic_mapper == Karafka::Routing::TopicMapper
104
+
105
+ # @note We don't use tap as it is around 13% slower than non-dup version
106
+ dupped = params.dup
107
+ dupped['topic'] = Karafka::App.config.topic_mapper.outgoing(params.topic)
108
+ [dupped]
109
+ end
110
+
111
+ private
112
+
113
+ # Fetches proper settings for a given map namespace
114
+ # @param namespace_key [Symbol] namespace from attributes map config adapter hash
115
+ # @param route_layer [Object] route topic or consumer group
116
+ # @param preexisting_settings [Hash] hash with some preexisting settings that might have
117
+ # been loaded in a different way
118
+ def fetch_for(namespace_key, route_layer, preexisting_settings = {})
119
+ kafka_configs.each_key do |setting_name|
120
+ # Ignore settings that are not related to our namespace
121
+ next unless AttributesMap.api_adapter[namespace_key].include?(setting_name)
122
+ # Ignore settings that are already initialized
123
+ # In case they are in preexisting settings fetched differently
124
+ next if preexisting_settings.key?(setting_name)
125
+ # Fetch all the settings from a given layer object. Objects can handle the fallback
126
+ # to the kafka settings, so
127
+ preexisting_settings[setting_name] = route_layer.send(setting_name)
128
+ end
129
+
130
+ preexisting_settings
131
+ end
132
+
133
+ # Removes nil containing keys from the final settings so it can use Kafkas driver
134
+ # defaults for those
135
+ # @param settings [Hash] settings that may contain nil values
136
+ # @return [Hash] settings without nil using keys (non of karafka options should be nil)
137
+ def sanitize(settings)
138
+ settings.reject { |_key, value| value.nil? }
139
+ end
140
+
141
+ # @return [Hash] Kafka config details as a hash
142
+ def kafka_configs
143
+ ::Karafka::App.config.kafka.to_h
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end