sbmt-kafka_consumer 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fe979c0a187c1aac9f2f8d571d99801e564912db57b86c8219b569fc3faef5c1
4
- data.tar.gz: af13a117d78170019ee3f0ae0930c5b65c0dd027efa39a46eb51f1a32998f47b
3
+ metadata.gz: 260ec80f2e17f9aef25d51a0eba28f43cb8ea524c3d2d956dea2fd5a240123f3
4
+ data.tar.gz: b302aac60dd45b5bb3da4af3e52e1a21f1fa5f4cd126837c68cbb50c20cc894a
5
5
  SHA512:
6
- metadata.gz: 7105c49b6c2ecf06769bb310c9a4bf836aa5cbad1b2ee994860b814b71059b8217610429dada55b408063597e32893b1efb404be11902d22693b37e4233ddb18
7
- data.tar.gz: 2a6195f0db6b8a0a7f9435e6b93745422a97f2238c073598155b0c2d81212891028a67db9029160735754189610a3ab4687c433044c67bf9d6d2b6802bdd1149
6
+ metadata.gz: db36d1846f7b91745464ccd2a1e11b97553760e43715aae41e4cf7c0671c51cf7e35efc6b14eab9ed572ccabd21b5cdd10a4bf97b6fbf898bf4ddc83c728b2ac
7
+ data.tar.gz: 631e701e0c5cc15e059faa00c6e5a55ef8d92f2ad45b0c4a8d7467150f668569f3f1796453b5e1596ab489b3dc32ac3d3ca6124431077c0ffd6d97158308adf7
data/.rubocop.yml CHANGED
@@ -19,6 +19,8 @@ AllCops:
19
19
  SuggestExtensions: false
20
20
  TargetRubyVersion: 2.7
21
21
  TargetRailsVersion: 5.2
22
+ Exclude:
23
+ - spec/internal/pkg/**/*
22
24
 
23
25
  RSpec/FilePath:
24
26
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -13,6 +13,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
13
13
 
14
14
  ### Fixed
15
15
 
16
+ ## [2.1.0] - 2024-05-13
17
+
18
+ ### Added
19
+
20
+ - Implemented method `export_batch` for processing messages in batches
21
+
22
+ ## [2.0.1] - 2024-05-08
23
+
24
+ ### Fixed
25
+
26
+ - Limit the Karafka version to less than `2.4` because they dropped the consumer group mapping
27
+
16
28
  ## [2.0.0] - 2024-01-30
17
29
 
18
30
  ### Changed
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  [![Gem Version](https://badge.fury.io/rb/sbmt-kafka_consumer.svg)](https://badge.fury.io/rb/sbmt-kafka_consumer)
2
- [![Build Status](https://github.com/SberMarket-Tech/sbmt-kafka_consumer/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/SberMarket-Tech/sbmt-outbox/actions?query=branch%3Amaster)
2
+ [![Build Status](https://github.com/SberMarket-Tech/sbmt-kafka_consumer/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/SberMarket-Tech/sbmt-kafka_consumer/actions?query=branch%3Amaster)
3
3
 
4
4
  # Sbmt-KafkaConsumer
5
5
 
@@ -19,6 +19,10 @@ And then execute:
19
19
  bundle install
20
20
  ```
21
21
 
22
+ ## Demo
23
+
24
+ Learn how to use this gem and how it works with Ruby on Rails at here https://github.com/SberMarket-Tech/outbox-example-apps
25
+
22
26
  ## Auto configuration
23
27
 
24
28
  We recommend going through the configuration and file creation process using the following Rails generators. Each generator can be run by using the `--help` option to learn more about the available arguments.
@@ -227,6 +231,22 @@ require_relative "config/environment"
227
231
  some-extra-configuration
228
232
  ```
229
233
 
234
+ ### `Export batch`
235
+
236
+ To process messages in batches, you need to add the `export_batch` method in the consumer
237
+
238
+ ```ruby
239
+ # app/consumers/some_consumer.rb
240
+ class SomeConsumer < Sbmt::KafkaConsumer::BaseConsumer
241
+ def export_batch(messages)
242
+ # some code
243
+ end
244
+ end
245
+ ```
246
+ __CAUTION__:
247
+ - ⚠️ Inbox does not support batch insertion.
248
+ - ⚠️ If you want to use this feature, you need to process the stack atomically (eg: insert it into clickhouse in one request).
249
+
230
250
  ## CLI
231
251
 
232
252
  Run the following command to execute a server
@@ -255,6 +275,7 @@ Also pay attention to the number of processes of the server:
255
275
 
256
276
  To test your consumer with Rspec, please use [this shared context](./lib/sbmt/kafka_consumer/testing/shared_contexts/with_sbmt_karafka_consumer.rb)
257
277
 
278
+ ### for payload
258
279
  ```ruby
259
280
  require "sbmt/kafka_consumer/testing"
260
281
 
@@ -268,6 +289,20 @@ RSpec.describe OrderCreatedConsumer do
268
289
  end
269
290
  ```
270
291
 
292
+ ### for payloads
293
+ ```ruby
294
+ require "sbmt/kafka_consumer/testing"
295
+
296
+ RSpec.describe OrderCreatedConsumer do
297
+ include_context "with sbmt karafka consumer"
298
+
299
+ it "works" do
300
+ publish_to_sbmt_karafka_batch(payloads, deserializer: deserializer)
301
+ expect { consume_with_sbmt_karafka }.to change(Order, :count).by(1)
302
+ end
303
+ end
304
+ ```
305
+
271
306
  ## Development
272
307
 
273
308
  1. Prepare environment
@@ -17,12 +17,26 @@ module Sbmt
17
17
 
18
18
  def consume
19
19
  ::Rails.application.executor.wrap do
20
- messages.each do |message|
21
- with_instrumentation(message) { do_consume(message) }
20
+ if export_batch?
21
+ with_batch_instrumentation(messages) do
22
+ export_batch(messages)
23
+ mark_as_consumed!(messages.last)
24
+ end
25
+ else
26
+ messages.each do |message|
27
+ with_instrumentation(message) { do_consume(message) }
28
+ end
22
29
  end
23
30
  end
24
31
  end
25
32
 
33
+ def export_batch?
34
+ if @export_batch_memoized.nil?
35
+ @export_batch_memoized = respond_to?(:export_batch)
36
+ end
37
+ @export_batch_memoized
38
+ end
39
+
26
40
  private
27
41
 
28
42
  def with_instrumentation(message)
@@ -53,6 +67,25 @@ module Sbmt
53
67
  end
54
68
  end
55
69
 
70
+ def with_batch_instrumentation(messages)
71
+ @trace_id = SecureRandom.base58
72
+
73
+ logger.tagged(
74
+ trace_id: trace_id,
75
+ first_offset: messages.first.metadata.offset,
76
+ last_offset: messages.last.metadata.offset
77
+ ) do
78
+ ::Sbmt::KafkaConsumer.monitor.instrument(
79
+ "consumer.consumed_batch",
80
+ caller: self,
81
+ messages: messages,
82
+ trace_id: trace_id
83
+ ) do
84
+ yield
85
+ end
86
+ end
87
+ end
88
+
56
89
  def do_consume(message)
57
90
  log_message(message) if log_payload?
58
91
 
@@ -9,6 +9,7 @@ module Sbmt
9
9
  SBMT_KAFKA_CONSUMER_EVENTS = %w[
10
10
  consumer.consumed_one
11
11
  consumer.inbox.consumed_one
12
+ consumer.consumed_batch
12
13
  ].freeze
13
14
 
14
15
  def initialize
@@ -20,6 +20,7 @@ module Sbmt
20
20
 
21
21
  def trace(&block)
22
22
  return handle_consumed_one(&block) if @event_id == "consumer.consumed_one"
23
+ return handle_consumed_batch(&block) if @event_id == "consumer.consumed_batch"
23
24
  return handle_inbox_consumed_one(&block) if @event_id == "consumer.inbox.consumed_one"
24
25
  return handle_error(&block) if @event_id == "error.occurred"
25
26
 
@@ -43,6 +44,23 @@ module Sbmt
43
44
  end
44
45
  end
45
46
 
47
+ def handle_consumed_batch
48
+ return yield unless enabled?
49
+
50
+ consumer = @payload[:caller]
51
+ messages = @payload[:messages]
52
+
53
+ links = messages.filter_map do |m|
54
+ parent_context = ::OpenTelemetry.propagation.extract(m.headers, getter: ::OpenTelemetry::Context::Propagation.text_map_getter)
55
+ span_context = ::OpenTelemetry::Trace.current_span(parent_context).context
56
+ ::OpenTelemetry::Trace::Link.new(span_context) if span_context.valid?
57
+ end
58
+
59
+ tracer.in_span("consume batch", links: links, attributes: batch_attrs(consumer, messages), kind: :consumer) do
60
+ yield
61
+ end
62
+ end
63
+
46
64
  def handle_inbox_consumed_one
47
65
  return yield unless enabled?
48
66
 
@@ -92,6 +110,19 @@ module Sbmt
92
110
  attributes.compact
93
111
  end
94
112
 
113
+ def batch_attrs(consumer, messages)
114
+ message = messages.first
115
+ {
116
+ "messaging.system" => "kafka",
117
+ "messaging.destination" => message.topic,
118
+ "messaging.destination_kind" => "topic",
119
+ "messaging.kafka.consumer_group" => consumer.topic.consumer_group.id,
120
+ "messaging.batch_size" => messages.count,
121
+ "messaging.first_offset" => messages.first.offset,
122
+ "messaging.last_offset" => messages.last.offset
123
+ }.compact
124
+ end
125
+
95
126
  def extract_message_key(key)
96
127
  # skip encode if already valid utf8
97
128
  return key if key.nil? || (key.encoding == Encoding::UTF_8 && key.valid_encoding?)
@@ -9,34 +9,48 @@ module Sbmt
9
9
  class SentryTracer < ::Sbmt::KafkaConsumer::Instrumentation::Tracer
10
10
  CONSUMER_ERROR_TYPES = %w[
11
11
  consumer.base.consume_one
12
+ consumer.base.consumed_batch
12
13
  consumer.inbox.consume_one
13
14
  ].freeze
14
15
 
15
16
  def trace(&block)
16
17
  return handle_consumed_one(&block) if @event_id == "consumer.consumed_one"
18
+ return handle_consumed_batch(&block) if @event_id == "consumer.consumed_batch"
17
19
  return handle_error(&block) if @event_id == "error.occurred"
18
20
 
19
21
  yield
20
22
  end
21
23
 
22
24
  def handle_consumed_one
23
- return yield unless ::Sentry.initialized?
24
-
25
- consumer = @payload[:caller]
26
- message = @payload[:message]
27
- trace_id = @payload[:trace_id]
28
-
29
- scope, transaction = start_transaction(trace_id, consumer, message)
30
-
31
- begin
25
+ message = {
26
+ trace_id: @payload[:trace_id],
27
+ topic: @payload[:message].topic,
28
+ offset: @payload[:message].offset
29
+ }
30
+
31
+ with_sentry_transaction(
32
+ @payload[:caller],
33
+ message
34
+ ) do
32
35
  yield
33
- rescue
34
- finish_transaction(transaction, 500)
35
- raise
36
36
  end
37
+ end
37
38
 
38
- finish_transaction(transaction, 200)
39
- scope.clear
39
+ def handle_consumed_batch
40
+ message_first = @payload[:messages].first
41
+ message = {
42
+ trace_id: @payload[:trace_id],
43
+ topic: message_first.topic,
44
+ first_offset: message_first.offset,
45
+ last_offset: @payload[:messages].last.offset
46
+ }
47
+
48
+ with_sentry_transaction(
49
+ @payload[:caller],
50
+ message
51
+ ) do
52
+ yield
53
+ end
40
54
  end
41
55
 
42
56
  def handle_error
@@ -64,9 +78,9 @@ module Sbmt
64
78
 
65
79
  private
66
80
 
67
- def start_transaction(trace_id, consumer, message)
81
+ def start_transaction(consumer, message)
68
82
  scope = ::Sentry.get_current_scope
69
- scope.set_tags(trace_id: trace_id, topic: message.topic, offset: message.offset)
83
+ scope.set_tags(message)
70
84
  scope.set_transaction_name("Sbmt/KafkaConsumer/#{consumer.class.name}")
71
85
 
72
86
  transaction = ::Sentry.start_transaction(name: scope.transaction_name, op: "kafka-consumer")
@@ -97,6 +111,22 @@ module Sbmt
97
111
  # so in that case we return raw_payload
98
112
  message.raw_payload
99
113
  end
114
+
115
+ def with_sentry_transaction(consumer, message)
116
+ return yield unless ::Sentry.initialized?
117
+
118
+ scope, transaction = start_transaction(consumer, message)
119
+
120
+ begin
121
+ yield
122
+ rescue
123
+ finish_transaction(transaction, 500)
124
+ raise
125
+ end
126
+
127
+ finish_transaction(transaction, 200)
128
+ scope.clear
129
+ end
100
130
  end
101
131
  end
102
132
  end
@@ -28,15 +28,14 @@ RSpec.shared_context "with sbmt karafka consumer" do
28
28
 
29
29
  def publish_to_sbmt_karafka(raw_payload, opts = {})
30
30
  message = Karafka::Messages::Message.new(raw_payload, Karafka::Messages::Metadata.new(metadata_defaults.merge(opts)))
31
- consumer.messages = Karafka::Messages::Messages.new(
32
- [message],
33
- Karafka::Messages::BatchMetadata.new(
34
- topic: test_topic.name,
35
- partition: 0,
36
- processed_at: Time.zone.now,
37
- created_at: Time.zone.now
38
- )
39
- )
31
+ consumer.messages = consumer_messages([message])
32
+ end
33
+
34
+ def publish_to_sbmt_karafka_batch(raw_payloads, opts = {})
35
+ messages = raw_payloads.map do |p|
36
+ Karafka::Messages::Message.new(p, Karafka::Messages::Metadata.new(metadata_defaults.merge(opts)))
37
+ end
38
+ consumer.messages = consumer_messages(messages)
40
39
  end
41
40
 
42
41
  # @return [Hash] message default options
@@ -58,4 +57,18 @@ RSpec.shared_context "with sbmt karafka consumer" do
58
57
  instance.singleton_class.include Karafka::Processing::Strategies::Default
59
58
  instance
60
59
  end
60
+
61
+ private
62
+
63
+ def consumer_messages(messages)
64
+ Karafka::Messages::Messages.new(
65
+ messages,
66
+ Karafka::Messages::BatchMetadata.new(
67
+ topic: test_topic.name,
68
+ partition: 0,
69
+ processed_at: Time.zone.now,
70
+ created_at: Time.zone.now
71
+ )
72
+ )
73
+ end
61
74
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sbmt
4
4
  module KafkaConsumer
5
- VERSION = "2.0.0"
5
+ VERSION = "2.1.0"
6
6
  end
7
7
  end
@@ -33,7 +33,7 @@ Gem::Specification.new do |spec|
33
33
 
34
34
  spec.add_dependency "rails", ">= 5.2"
35
35
  spec.add_dependency "zeitwerk", "~> 2.3"
36
- spec.add_dependency "karafka", "~> 2.2"
36
+ spec.add_dependency "karafka", "~> 2.2", "< 2.4" # [Breaking] Drop the concept of consumer group mapping.
37
37
  spec.add_dependency "yabeda", ">= 0.11"
38
38
  spec.add_dependency "anyway_config", ">= 2.4.0"
39
39
  spec.add_dependency "thor"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sbmt-kafka_consumer
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sbermarket Ruby-Platform Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-02-27 00:00:00.000000000 Z
11
+ date: 2024-06-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -45,6 +45,9 @@ dependencies:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
47
  version: '2.2'
48
+ - - "<"
49
+ - !ruby/object:Gem::Version
50
+ version: '2.4'
48
51
  type: :runtime
49
52
  prerelease: false
50
53
  version_requirements: !ruby/object:Gem::Requirement
@@ -52,6 +55,9 @@ dependencies:
52
55
  - - "~>"
53
56
  - !ruby/object:Gem::Version
54
57
  version: '2.2'
58
+ - - "<"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.4'
55
61
  - !ruby/object:Gem::Dependency
56
62
  name: yabeda
57
63
  requirement: !ruby/object:Gem::Requirement
@@ -564,7 +570,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
564
570
  - !ruby/object:Gem::Version
565
571
  version: '0'
566
572
  requirements: []
567
- rubygems_version: 3.4.10
573
+ rubygems_version: 3.5.11
568
574
  signing_key:
569
575
  specification_version: 4
570
576
  summary: Ruby gem for consuming Kafka messages