karafka 1.2.11

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 (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