karafka 1.0.1 → 1.4.14

Sign up to get free protection for your applications and to get access to all the features.
Files changed (121) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data/.coditsu/ci.yml +3 -0
  4. data/.console_irbrc +1 -3
  5. data/.diffend.yml +3 -0
  6. data/.github/ISSUE_TEMPLATE/bug_report.md +50 -0
  7. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  8. data/.github/workflows/ci.yml +76 -0
  9. data/.gitignore +1 -0
  10. data/.ruby-version +1 -1
  11. data/CHANGELOG.md +286 -16
  12. data/CODE_OF_CONDUCT.md +1 -1
  13. data/CONTRIBUTING.md +6 -7
  14. data/Gemfile +5 -2
  15. data/Gemfile.lock +100 -103
  16. data/README.md +54 -74
  17. data/bin/karafka +1 -1
  18. data/certs/mensfeld.pem +26 -0
  19. data/config/errors.yml +40 -5
  20. data/docker-compose.yml +17 -0
  21. data/karafka.gemspec +31 -15
  22. data/lib/karafka/app.rb +19 -18
  23. data/lib/karafka/assignment_strategies/round_robin.rb +13 -0
  24. data/lib/karafka/attributes_map.rb +17 -21
  25. data/lib/karafka/backends/inline.rb +2 -3
  26. data/lib/karafka/base_consumer.rb +57 -0
  27. data/lib/karafka/base_responder.rb +77 -31
  28. data/lib/karafka/cli/base.rb +4 -4
  29. data/lib/karafka/cli/console.rb +11 -9
  30. data/lib/karafka/cli/flow.rb +9 -7
  31. data/lib/karafka/cli/info.rb +5 -4
  32. data/lib/karafka/cli/install.rb +32 -8
  33. data/lib/karafka/cli/missingno.rb +19 -0
  34. data/lib/karafka/cli/server.rb +18 -16
  35. data/lib/karafka/cli.rb +10 -2
  36. data/lib/karafka/code_reloader.rb +67 -0
  37. data/lib/karafka/connection/{config_adapter.rb → api_adapter.rb} +71 -22
  38. data/lib/karafka/connection/batch_delegator.rb +55 -0
  39. data/lib/karafka/connection/builder.rb +23 -0
  40. data/lib/karafka/connection/client.rb +120 -0
  41. data/lib/karafka/connection/listener.rb +39 -26
  42. data/lib/karafka/connection/message_delegator.rb +36 -0
  43. data/lib/karafka/consumers/batch_metadata.rb +10 -0
  44. data/lib/karafka/consumers/callbacks.rb +71 -0
  45. data/lib/karafka/consumers/includer.rb +64 -0
  46. data/lib/karafka/consumers/responders.rb +24 -0
  47. data/lib/karafka/{controllers → consumers}/single_params.rb +3 -3
  48. data/lib/karafka/contracts/config.rb +21 -0
  49. data/lib/karafka/contracts/consumer_group.rb +211 -0
  50. data/lib/karafka/contracts/consumer_group_topic.rb +19 -0
  51. data/lib/karafka/contracts/responder_usage.rb +54 -0
  52. data/lib/karafka/contracts/server_cli_options.rb +31 -0
  53. data/lib/karafka/contracts.rb +10 -0
  54. data/lib/karafka/errors.rb +27 -12
  55. data/lib/karafka/fetcher.rb +15 -15
  56. data/lib/karafka/helpers/class_matcher.rb +20 -10
  57. data/lib/karafka/helpers/config_retriever.rb +3 -3
  58. data/lib/karafka/helpers/inflector.rb +26 -0
  59. data/lib/karafka/helpers/multi_delegator.rb +0 -1
  60. data/lib/karafka/instrumentation/logger.rb +54 -0
  61. data/lib/karafka/instrumentation/monitor.rb +70 -0
  62. data/lib/karafka/instrumentation/proctitle_listener.rb +36 -0
  63. data/lib/karafka/instrumentation/stdout_listener.rb +140 -0
  64. data/lib/karafka/params/batch_metadata.rb +26 -0
  65. data/lib/karafka/params/builders/batch_metadata.rb +30 -0
  66. data/lib/karafka/params/builders/params.rb +38 -0
  67. data/lib/karafka/params/builders/params_batch.rb +25 -0
  68. data/lib/karafka/params/metadata.rb +20 -0
  69. data/lib/karafka/params/params.rb +35 -107
  70. data/lib/karafka/params/params_batch.rb +38 -19
  71. data/lib/karafka/patches/ruby_kafka.rb +47 -0
  72. data/lib/karafka/persistence/client.rb +29 -0
  73. data/lib/karafka/persistence/consumers.rb +45 -0
  74. data/lib/karafka/persistence/topics.rb +48 -0
  75. data/lib/karafka/process.rb +6 -9
  76. data/lib/karafka/responders/builder.rb +15 -14
  77. data/lib/karafka/responders/topic.rb +14 -9
  78. data/lib/karafka/routing/builder.rb +38 -9
  79. data/lib/karafka/routing/consumer_group.rb +6 -4
  80. data/lib/karafka/routing/consumer_mapper.rb +10 -9
  81. data/lib/karafka/routing/proxy.rb +10 -1
  82. data/lib/karafka/routing/router.rb +1 -1
  83. data/lib/karafka/routing/topic.rb +8 -12
  84. data/lib/karafka/routing/topic_mapper.rb +16 -18
  85. data/lib/karafka/serialization/json/deserializer.rb +27 -0
  86. data/lib/karafka/serialization/json/serializer.rb +31 -0
  87. data/lib/karafka/server.rb +50 -39
  88. data/lib/karafka/setup/config.rb +138 -91
  89. data/lib/karafka/setup/configurators/water_drop.rb +21 -16
  90. data/lib/karafka/setup/dsl.rb +21 -0
  91. data/lib/karafka/status.rb +7 -3
  92. data/lib/karafka/templates/{application_controller.rb.example → application_consumer.rb.erb} +2 -2
  93. data/lib/karafka/templates/karafka.rb.erb +92 -0
  94. data/lib/karafka/version.rb +1 -1
  95. data/lib/karafka.rb +19 -15
  96. data.tar.gz.sig +0 -0
  97. metadata +119 -81
  98. metadata.gz.sig +5 -0
  99. data/.github/ISSUE_TEMPLATE.md +0 -2
  100. data/.travis.yml +0 -17
  101. data/Rakefile +0 -7
  102. data/lib/karafka/base_controller.rb +0 -117
  103. data/lib/karafka/connection/messages_consumer.rb +0 -106
  104. data/lib/karafka/connection/messages_processor.rb +0 -61
  105. data/lib/karafka/controllers/includer.rb +0 -51
  106. data/lib/karafka/controllers/responders.rb +0 -19
  107. data/lib/karafka/loader.rb +0 -29
  108. data/lib/karafka/logger.rb +0 -53
  109. data/lib/karafka/monitor.rb +0 -98
  110. data/lib/karafka/parsers/json.rb +0 -38
  111. data/lib/karafka/patches/dry_configurable.rb +0 -33
  112. data/lib/karafka/persistence/controller.rb +0 -23
  113. data/lib/karafka/schemas/config.rb +0 -31
  114. data/lib/karafka/schemas/consumer_group.rb +0 -64
  115. data/lib/karafka/schemas/consumer_group_topic.rb +0 -18
  116. data/lib/karafka/schemas/responder_usage.rb +0 -38
  117. data/lib/karafka/schemas/server_cli_options.rb +0 -43
  118. data/lib/karafka/setup/configurators/base.rb +0 -35
  119. data/lib/karafka/setup/configurators/celluloid.rb +0 -19
  120. data/lib/karafka/templates/karafka.rb.example +0 -41
  121. /data/lib/karafka/templates/{application_responder.rb.example → application_responder.rb.erb} +0 -0
@@ -7,7 +7,6 @@ module Karafka
7
7
  # Multidelegator is used to delegate calls to multiple targets
8
8
  class MultiDelegator
9
9
  # @param targets to which we want to delegate methods
10
- #
11
10
  def initialize(*targets)
12
11
  @targets = targets
13
12
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Instrumentation
5
+ # Default logger for Event Delegator
6
+ # @note It uses ::Logger features - providing basic logging
7
+ class Logger < ::Logger
8
+ # Map containing information about log level for given environment
9
+ ENV_MAP = {
10
+ 'production' => ::Logger::ERROR,
11
+ 'test' => ::Logger::ERROR,
12
+ 'development' => ::Logger::INFO,
13
+ 'debug' => ::Logger::DEBUG,
14
+ 'default' => ::Logger::INFO
15
+ }.freeze
16
+
17
+ private_constant :ENV_MAP
18
+
19
+ # Creates a new instance of logger ensuring that it has a place to write to
20
+ # @param _args Any arguments that we don't care about but that are needed in order to
21
+ # make this logger compatible with the default Ruby one
22
+ def initialize(*_args)
23
+ super(target)
24
+ self.level = ENV_MAP[Karafka.env] || ENV_MAP['default']
25
+ end
26
+
27
+ private
28
+
29
+ # @return [Karafka::Helpers::MultiDelegator] multi delegator instance
30
+ # to which we will be writing logs
31
+ # We use this approach to log stuff to file and to the $stdout at the same time
32
+ def target
33
+ Karafka::Helpers::MultiDelegator
34
+ .delegate(:write, :close)
35
+ .to(*[$stdout, file].compact)
36
+ end
37
+
38
+ # @return [Pathname] Path to a file to which we should log
39
+ def log_path
40
+ @log_path ||= Karafka::App.root.join("log/#{Karafka.env}.log")
41
+ end
42
+
43
+ # @return [File] file to which we want to write our logs
44
+ # @note File is being opened in append mode ('a')
45
+ def file
46
+ FileUtils.mkdir_p(File.dirname(log_path))
47
+
48
+ @file ||= File.open(log_path, 'a')
49
+ rescue Errno::EACCES, Errno::EROFS
50
+ nil
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ # Namespace for all the things related with Karafka instrumentation process
5
+ module Instrumentation
6
+ # Monitor is used to hookup external monitoring services to monitor how Karafka works
7
+ # It provides a standardized API for checking incoming messages/enqueueing etc
8
+ # Since it is a pub-sub based on dry-monitor, you can use as many subscribers/loggers at the
9
+ # same time, which means that you might have for example file logging and NewRelic at the same
10
+ # time
11
+ # @note This class acts as a singleton because we are only permitted to have single monitor
12
+ # per running process (just as logger)
13
+ class Monitor < Dry::Monitor::Notifications
14
+ # List of events that we support in the system and to which a monitor client can hook up
15
+ # @note The non-error once support timestamp benchmarking
16
+ # @note Depending on Karafka extensions and additional engines, this might not be the
17
+ # complete list of all the events. Please use the #available_events on fully loaded
18
+ # Karafka system to determine all of the events you can use.
19
+ # Last 4 events are from WaterDrop but for convenience we use the same monitor for the
20
+ # whole karafka ecosystem
21
+ BASE_EVENTS = %w[
22
+ params.params.deserialize
23
+ params.params.deserialize.error
24
+ connection.listener.before_fetch_loop
25
+ connection.listener.fetch_loop
26
+ connection.listener.fetch_loop.error
27
+ connection.client.fetch_loop.error
28
+ connection.batch_delegator.call
29
+ connection.message_delegator.call
30
+ fetcher.call.error
31
+ backends.inline.process
32
+ process.notice_signal
33
+ consumers.responders.respond_with
34
+ async_producer.call.error
35
+ async_producer.call.retry
36
+ sync_producer.call.error
37
+ sync_producer.call.retry
38
+ app.initializing
39
+ app.initialized
40
+ app.running
41
+ app.stopping
42
+ app.stopping.error
43
+ app.stopped
44
+ ].freeze
45
+
46
+ private_constant :BASE_EVENTS
47
+
48
+ # @return [Karafka::Instrumentation::Monitor] monitor instance for system instrumentation
49
+ def initialize
50
+ super(:karafka)
51
+ BASE_EVENTS.each(&method(:register_event))
52
+ end
53
+
54
+ # Allows us to subscribe to events with a code that will be yielded upon events
55
+ # @param event_name_or_listener [String, Object] name of the event we want to subscribe to
56
+ # or a listener if we decide to go with object listener
57
+ def subscribe(event_name_or_listener)
58
+ return super unless event_name_or_listener.is_a?(String)
59
+ return super if available_events.include?(event_name_or_listener)
60
+
61
+ raise Errors::UnregisteredMonitorEventError, event_name_or_listener
62
+ end
63
+
64
+ # @return [Array<String>] names of available events to which we can subscribe
65
+ def available_events
66
+ __bus__.events.keys
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Instrumentation
5
+ # Listener that sets a proc title with a nice descriptive value
6
+ class ProctitleListener
7
+ # Updates proc title to an initializing one
8
+ # @param _event [Dry::Events::Event] event details including payload
9
+ def on_app_initializing(_event)
10
+ setproctitle('initializing')
11
+ end
12
+
13
+ # Updates proc title to a running one
14
+ # @param _event [Dry::Events::Event] event details including payload
15
+ def on_app_running(_event)
16
+ setproctitle('running')
17
+ end
18
+
19
+ # Updates proc title to a stopping one
20
+ # @param _event [Dry::Events::Event] event details including payload
21
+ def on_app_stopping(_event)
22
+ setproctitle('stopping')
23
+ end
24
+
25
+ private
26
+
27
+ # Sets a proper proc title with our constant prefix
28
+ # @param status [String] any status we want to set
29
+ def setproctitle(status)
30
+ ::Process.setproctitle(
31
+ "karafka #{Karafka::App.config.client_id} (#{status})"
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Instrumentation
5
+ # Default listener that hooks up to our instrumentation and uses its events for logging
6
+ # It can be removed/replaced or anything without any harm to the Karafka app flow
7
+ class StdoutListener
8
+ # Log levels that we use in this particular listener
9
+ USED_LOG_LEVELS = %i[
10
+ debug
11
+ info
12
+ error
13
+ fatal
14
+ ].freeze
15
+
16
+ # Logs details about incoming batches and with which consumer we will consume them
17
+ # @param event [Dry::Events::Event] event details including payload
18
+ def on_connection_batch_delegator_call(event)
19
+ consumer = event[:consumer]
20
+ topic = consumer.topic.name
21
+ kafka_messages = event[:kafka_batch].messages
22
+ info(
23
+ <<~MSG.chomp.tr("\n", ' ')
24
+ #{kafka_messages.count} messages
25
+ on #{topic} topic
26
+ delegated to #{consumer.class}
27
+ MSG
28
+ )
29
+ end
30
+
31
+ # Logs details about incoming message and with which consumer we will consume it
32
+ # @param event [Dry::Events::Event] event details including payload
33
+ def on_connection_message_delegator_call(event)
34
+ consumer = event[:consumer]
35
+ topic = consumer.topic.name
36
+ info "1 message on #{topic} topic delegated to #{consumer.class}"
37
+ end
38
+
39
+ # Logs details about each received message value deserialization
40
+ # @param event [Dry::Events::Event] event details including payload
41
+ def on_params_params_deserialize(event)
42
+ # Keep in mind, that a caller here is a param object not a controller,
43
+ # so it returns a topic as a string, not a routing topic
44
+ debug(
45
+ <<~MSG.chomp.tr("\n", ' ')
46
+ Params deserialization for #{event[:caller].metadata.topic} topic
47
+ successful in #{event[:time]} ms
48
+ MSG
49
+ )
50
+ end
51
+
52
+ # Logs unsuccessful deserialization attempts of incoming data
53
+ # @param event [Dry::Events::Event] event details including payload
54
+ def on_params_params_deserialize_error(event)
55
+ topic = event[:caller].metadata.topic
56
+ error = event[:error]
57
+ error "Params deserialization error for #{topic} topic: #{error}"
58
+ end
59
+
60
+ # Logs errors that occurred in a listener fetch loop
61
+ # @param event [Dry::Events::Event] event details including payload
62
+ # @note It's an error as we can recover from it not a fatal
63
+ def on_connection_listener_fetch_loop_error(event)
64
+ error "Listener fetch loop error: #{event[:error]}"
65
+ end
66
+
67
+ # Logs errors that are related to the connection itself
68
+ # @param event [Dry::Events::Event] event details including payload
69
+ # @note Karafka will attempt to reconnect, so an error not a fatal
70
+ def on_connection_client_fetch_loop_error(event)
71
+ error "Client fetch loop error: #{event[:error]}"
72
+ end
73
+
74
+ # Logs info about crashed fetcher
75
+ # @param event [Dry::Events::Event] event details including payload
76
+ # @note If this happens, Karafka will shutdown as it means a critical error
77
+ # in one of the threads
78
+ def on_fetcher_call_error(event)
79
+ fatal "Fetcher crash due to an error: #{event[:error]}"
80
+ end
81
+
82
+ # Logs info about processing of a certain dataset with an inline backend
83
+ # @param event [Dry::Events::Event] event details including payload
84
+ def on_backends_inline_process(event)
85
+ count = event[:caller].send(:params_batch).to_a.size
86
+ topic = event[:caller].topic.name
87
+ time = event[:time]
88
+ info "Inline processing of topic #{topic} with #{count} messages took #{time} ms"
89
+ end
90
+
91
+ # Logs info about system signals that Karafka received
92
+ # @param event [Dry::Events::Event] event details including payload
93
+ def on_process_notice_signal(event)
94
+ info "Received #{event[:signal]} system signal"
95
+ end
96
+
97
+ # Logs info about responder usage withing a controller flow
98
+ # @param event [Dry::Events::Event] event details including payload
99
+ def on_consumers_responders_respond_with(event)
100
+ calling = event[:caller]
101
+ responder = calling.topic.responder
102
+ data = event[:data]
103
+ info "Responded from #{calling.class} using #{responder} with following data #{data}"
104
+ end
105
+
106
+ # Logs info that we're initializing Karafka framework components
107
+ # @param _event [Dry::Events::Event] event details including payload
108
+ def on_app_initializing(_event)
109
+ info "Initializing Karafka framework #{::Process.pid}"
110
+ end
111
+
112
+ # Logs info that we're running Karafka app
113
+ # @param _event [Dry::Events::Event] event details including payload
114
+ def on_app_running(_event)
115
+ info "Running Karafka server #{::Process.pid}"
116
+ end
117
+
118
+ # Logs info that we're going to stop the Karafka server
119
+ # @param _event [Dry::Events::Event] event details including payload
120
+ def on_app_stopping(_event)
121
+ # We use a separate thread as logging can't be called from trap context
122
+ Thread.new { info "Stopping Karafka server #{::Process.pid}" }
123
+ end
124
+
125
+ # Logs an error that Karafka was unable to stop the server gracefully and it had to do a
126
+ # forced exit
127
+ # @param _event [Dry::Events::Event] event details including payload
128
+ def on_app_stopping_error(_event)
129
+ # We use a separate thread as logging can't be called from trap context
130
+ Thread.new { error "Forceful Karafka server #{::Process.pid} stop" }
131
+ end
132
+
133
+ USED_LOG_LEVELS.each do |log_level|
134
+ define_method log_level do |*args|
135
+ Karafka.logger.send(log_level, *args)
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Params
5
+ # Simple batch metadata object that stores all non-message information received from Kafka
6
+ # cluster while fetching the data
7
+ # @note This metadata object refers to per batch metadata, not `#params.metadata`
8
+ BatchMetadata = Struct.new(
9
+ :batch_size,
10
+ :first_offset,
11
+ :highwater_mark_offset,
12
+ :unknown_last_offset,
13
+ :last_offset,
14
+ :offset_lag,
15
+ :deserializer,
16
+ :partition,
17
+ :topic,
18
+ keyword_init: true
19
+ ) do
20
+ # @return [Boolean] is the last offset known or unknown
21
+ def unknown_last_offset?
22
+ unknown_last_offset
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Params
5
+ module Builders
6
+ # Builder for creating batch metadata object based on the batch informations
7
+ module BatchMetadata
8
+ class << self
9
+ # Creates metadata based on the kafka batch data
10
+ # @param kafka_batch [Kafka::FetchedBatch] kafka batch details
11
+ # @param topic [Karafka::Routing::Topic] topic for which we've fetched the batch
12
+ # @return [Karafka::Params::BatchMetadata] batch metadata object
13
+ def from_kafka_batch(kafka_batch, topic)
14
+ Karafka::Params::BatchMetadata.new(
15
+ batch_size: kafka_batch.messages.count,
16
+ first_offset: kafka_batch.first_offset,
17
+ highwater_mark_offset: kafka_batch.highwater_mark_offset,
18
+ unknown_last_offset: kafka_batch.unknown_last_offset?,
19
+ last_offset: kafka_batch.last_offset,
20
+ offset_lag: kafka_batch.offset_lag,
21
+ deserializer: topic.deserializer,
22
+ partition: kafka_batch.partition,
23
+ topic: topic.name
24
+ ).freeze
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Params
5
+ # Due to the fact, that we create params related objects in couple contexts / places
6
+ # plus backends can build up them their own way we have this namespace.
7
+ # It allows to isolate actual params objects from their building process that can be
8
+ # context dependent.
9
+ module Builders
10
+ # Builder for params
11
+ module Params
12
+ class << self
13
+ # @param kafka_message [Kafka::FetchedMessage] message fetched from Kafka
14
+ # @param topic [Karafka::Routing::Topic] topic for which this message was fetched
15
+ # @return [Karafka::Params::Params] params object with payload and message metadata
16
+ def from_kafka_message(kafka_message, topic)
17
+ metadata = Karafka::Params::Metadata.new(
18
+ create_time: kafka_message.create_time,
19
+ headers: kafka_message.headers || {},
20
+ is_control_record: kafka_message.is_control_record,
21
+ key: kafka_message.key,
22
+ offset: kafka_message.offset,
23
+ deserializer: topic.deserializer,
24
+ partition: kafka_message.partition,
25
+ receive_time: Time.now,
26
+ topic: topic.name
27
+ ).freeze
28
+
29
+ Karafka::Params::Params.new(
30
+ kafka_message.value,
31
+ metadata
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Params
5
+ module Builders
6
+ # Builder for creating params batch instances
7
+ module ParamsBatch
8
+ class << self
9
+ # Creates params batch with params inside based on the incoming messages
10
+ # and the topic from which it comes
11
+ # @param kafka_messages [Array<Kafka::FetchedMessage>] raw fetched messages
12
+ # @param topic [Karafka::Routing::Topic] topic for which we're received messages
13
+ # @return [Karafka::Params::ParamsBatch<Karafka::Params::Params>] batch with params
14
+ def from_kafka_messages(kafka_messages, topic)
15
+ params_array = kafka_messages.map do |message|
16
+ Karafka::Params::Builders::Params.from_kafka_message(message, topic)
17
+ end
18
+
19
+ Karafka::Params::ParamsBatch.new(params_array).freeze
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Params
5
+ # Single message / params metadata details that can be accessed without the need for the
6
+ # payload deserialization
7
+ Metadata = Struct.new(
8
+ :create_time,
9
+ :headers,
10
+ :is_control_record,
11
+ :key,
12
+ :offset,
13
+ :deserializer,
14
+ :partition,
15
+ :receive_time,
16
+ :topic,
17
+ keyword_init: true
18
+ )
19
+ end
20
+ end
@@ -3,123 +3,51 @@
3
3
  module Karafka
4
4
  # Params namespace encapsulating all the logic that is directly related to params handling
5
5
  module Params
6
- # Class-wrapper for hash with indifferent access with additional lazy loading feature
7
6
  # It provides lazy loading not only until the first usage, but also allows us to skip
8
- # using parser until we execute our logic. That way we can operate with
9
- # heavy-parsing data without slowing down the whole application.
10
- class Params < HashWithIndifferentAccess
11
- # Kafka::FetchedMessage attributes that we want to use inside of params
12
- KAFKA_MESSAGE_ATTRIBUTES = %i[
13
- value
14
- partition
15
- offset
16
- key
17
- ].freeze
18
-
19
- # Params attributes that should be available via a method call invocation for Kafka
20
- # client compatibility.
21
- # Kafka passes internally Kafka::FetchedMessage object and the ruby-kafka consumer
22
- # uses those fields via method calls, so in order to be able to pass there our params
23
- # objects, have to have same api.
24
- PARAMS_METHOD_ATTRIBUTES = %i[
25
- topic
26
- partition
27
- offset
28
- key
29
- ].freeze
30
-
31
- class << self
32
- # We allow building instances only via the #build method
33
-
34
- # @param message [Kafka::FetchedMessage, Hash] message that we get out of Kafka
35
- # in case of building params inside main Karafka process in
36
- # Karafka::Connection::Consumer, or a hash when we retrieve data that is already parsed
37
- # @param parser [Class] parser class that we will use to unparse data
38
- # @return [Karafka::Params::Params] Karafka params object not yet used parser for
39
- # retrieving data that we've got from Kafka
40
- # @example Build params instance from a hash
41
- # Karafka::Params::Params.build({ key: 'value' }) #=> params object
42
- # @example Build params instance from a Kafka::FetchedMessage object
43
- # Karafka::Params::Params.build(message) #=> params object
44
- def build(message, parser)
45
- # Hash case happens inside backends that interchange data
46
- if message.is_a?(Hash)
47
- new(parser: parser).send(:merge!, message)
48
- else
49
- # This happens inside Kafka::FetchedMessagesProcessor
50
- new(
51
- parser: parser,
52
- parsed: false,
53
- received_at: Time.now
54
- ).tap do |instance|
55
- KAFKA_MESSAGE_ATTRIBUTES.each do |attribute|
56
- instance[attribute] = message.send(attribute)
57
- end
58
-
59
- # When we get raw messages, they might have a topic, that was modified by a
60
- # topic mapper. We need to "reverse" this change and map back to the non-modified
61
- # format, so our internal flow is not corrupted with the mapping
62
- instance[:topic] = Karafka::App.config.topic_mapper.incoming(message.topic)
63
- end
64
- end
65
- end
66
-
67
- # Defines a method call accessor to a particular hash field.
68
- # @note Won't work for complex key names that contain spaces, etc
69
- # @param key [Symbol] name of a field that we want to retrieve with a method call
70
- # @example
71
- # key_attr_reader :example
72
- # params.example #=> 'my example value'
73
- def key_attr_reader(key)
74
- define_method key do
75
- self[key]
76
- end
77
- end
7
+ # using deserializer until we execute our logic. That way we can operate with
8
+ # heavy-deserialization data without slowing down the whole application.
9
+ class Params
10
+ extend Forwardable
11
+
12
+ attr_reader :raw_payload, :metadata
13
+
14
+ def_delegators :metadata, *Metadata.members
15
+
16
+ # @param raw_payload [Object] incoming payload before deserialization
17
+ # @param metadata [Karafka::Params::Metadata] message metadata object
18
+ def initialize(raw_payload, metadata)
19
+ @raw_payload = raw_payload
20
+ @metadata = metadata
21
+ @deserialized = false
22
+ @payload = nil
78
23
  end
79
24
 
80
- # @return [Karafka::Params::Params] this will trigger parser execution. If we decide to
81
- # retrieve data, parser will be executed to parse data. Output of parsing will be merged
82
- # to the current object. This object will be also marked as already parsed, so we won't
83
- # parse it again.
84
- def retrieve!
85
- return self if self[:parsed]
25
+ # @return [Object] lazy-deserialized data (deserialized upon first request)
26
+ def payload
27
+ return @payload if deserialized?
86
28
 
87
- merge!(parse(delete(:value)))
29
+ @payload = deserialize
30
+ # We mark deserialization as successful after deserialization, as in case of an error
31
+ # this won't be falsely set to true
32
+ @deserialized = true
33
+ @payload
88
34
  end
89
35
 
90
- PARAMS_METHOD_ATTRIBUTES.each(&method(:key_attr_reader))
36
+ # @return [Boolean] did given params payload were deserialized already
37
+ def deserialized?
38
+ @deserialized
39
+ end
91
40
 
92
41
  private
93
42
 
94
- # Overwritten merge! method - it behaves differently for keys that are the same in our hash
95
- # and in a other_hash - it will not replace keys that are the same in our hash
96
- # and in the other one
97
- # @param other_hash [Hash, HashWithIndifferentAccess] hash that we want to merge into current
98
- # @return [Karafka::Params::Params] our parameters hash with merged values
99
- # @example Merge with hash without same keys
100
- # new(a: 1, b: 2).merge!(c: 3) #=> { a: 1, b: 2, c: 3 }
101
- # @example Merge with hash with same keys (symbol based)
102
- # new(a: 1).merge!(a: 2) #=> { a: 1 }
103
- # @example Merge with hash with same keys (string based)
104
- # new(a: 1).merge!('a' => 2) #=> { a: 1 }
105
- # @example Merge with hash with same keys (current string based)
106
- # new('a' => 1).merge!(a: 2) #=> { a: 1 }
107
- def merge!(other_hash)
108
- super(other_hash) { |_key, base_value, _new_value| base_value }
109
- end
110
-
111
- # @param value [String] Raw data that we want to parse using controller's parser
112
- # @note If something goes wrong, it will return raw data in a hash with a message key
113
- # @return [Hash] parsed data or a hash with message key containing raw data if something
114
- # went wrong during parsing
115
- def parse(value)
116
- self[:parser].parse(value)
117
- # We catch both of them, because for default JSON - we use JSON parser directly
118
- rescue ::Karafka::Errors::ParserError => e
119
- Karafka.monitor.notice_error(self.class, e)
43
+ # @return [Object] tries de-serializes data
44
+ def deserialize
45
+ Karafka.monitor.instrument('params.params.deserialize', caller: self) do
46
+ metadata.deserializer.call(self)
47
+ end
48
+ rescue ::StandardError => e
49
+ Karafka.monitor.instrument('params.params.deserialize.error', caller: self, error: e)
120
50
  raise e
121
- ensure
122
- self[:parsed] = true
123
51
  end
124
52
  end
125
53
  end