karafka 2.2.14 → 2.3.0.alpha2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/workflows/ci.yml +38 -12
  4. data/.ruby-version +1 -1
  5. data/CHANGELOG.md +24 -0
  6. data/Gemfile.lock +16 -16
  7. data/README.md +0 -2
  8. data/SECURITY.md +23 -0
  9. data/bin/integrations +1 -1
  10. data/config/locales/errors.yml +7 -1
  11. data/config/locales/pro_errors.yml +22 -0
  12. data/docker-compose.yml +1 -1
  13. data/karafka.gemspec +2 -2
  14. data/lib/karafka/admin/acl.rb +287 -0
  15. data/lib/karafka/admin.rb +9 -13
  16. data/lib/karafka/app.rb +5 -3
  17. data/lib/karafka/base_consumer.rb +9 -1
  18. data/lib/karafka/cli/base.rb +1 -1
  19. data/lib/karafka/connection/client.rb +83 -76
  20. data/lib/karafka/connection/conductor.rb +28 -0
  21. data/lib/karafka/connection/listener.rb +159 -42
  22. data/lib/karafka/connection/listeners_batch.rb +5 -11
  23. data/lib/karafka/connection/manager.rb +72 -0
  24. data/lib/karafka/connection/messages_buffer.rb +12 -0
  25. data/lib/karafka/connection/proxy.rb +17 -0
  26. data/lib/karafka/connection/status.rb +75 -0
  27. data/lib/karafka/contracts/config.rb +14 -10
  28. data/lib/karafka/contracts/consumer_group.rb +9 -1
  29. data/lib/karafka/contracts/topic.rb +3 -1
  30. data/lib/karafka/errors.rb +17 -0
  31. data/lib/karafka/instrumentation/logger_listener.rb +3 -0
  32. data/lib/karafka/instrumentation/notifications.rb +13 -5
  33. data/lib/karafka/instrumentation/vendors/appsignal/metrics_listener.rb +31 -28
  34. data/lib/karafka/instrumentation/vendors/datadog/logger_listener.rb +20 -1
  35. data/lib/karafka/instrumentation/vendors/datadog/metrics_listener.rb +15 -12
  36. data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +39 -36
  37. data/lib/karafka/pro/base_consumer.rb +47 -0
  38. data/lib/karafka/pro/connection/manager.rb +269 -0
  39. data/lib/karafka/pro/connection/multiplexing/listener.rb +40 -0
  40. data/lib/karafka/pro/iterator/tpl_builder.rb +1 -1
  41. data/lib/karafka/pro/iterator.rb +1 -6
  42. data/lib/karafka/pro/loader.rb +14 -0
  43. data/lib/karafka/pro/processing/coordinator.rb +2 -1
  44. data/lib/karafka/pro/processing/executor.rb +37 -0
  45. data/lib/karafka/pro/processing/expansions_selector.rb +32 -0
  46. data/lib/karafka/pro/processing/jobs/periodic.rb +41 -0
  47. data/lib/karafka/pro/processing/jobs/periodic_non_blocking.rb +32 -0
  48. data/lib/karafka/pro/processing/jobs_builder.rb +14 -3
  49. data/lib/karafka/pro/processing/offset_metadata/consumer.rb +44 -0
  50. data/lib/karafka/pro/processing/offset_metadata/fetcher.rb +131 -0
  51. data/lib/karafka/pro/processing/offset_metadata/listener.rb +46 -0
  52. data/lib/karafka/pro/processing/schedulers/base.rb +39 -23
  53. data/lib/karafka/pro/processing/schedulers/default.rb +12 -14
  54. data/lib/karafka/pro/processing/strategies/default.rb +154 -1
  55. data/lib/karafka/pro/processing/strategies/dlq/default.rb +39 -0
  56. data/lib/karafka/pro/processing/strategies/vp/default.rb +65 -25
  57. data/lib/karafka/pro/processing/virtual_offset_manager.rb +41 -11
  58. data/lib/karafka/pro/routing/features/long_running_job/topic.rb +2 -0
  59. data/lib/karafka/pro/routing/features/multiplexing/config.rb +38 -0
  60. data/lib/karafka/pro/routing/features/multiplexing/contracts/topic.rb +114 -0
  61. data/lib/karafka/pro/routing/features/multiplexing/patches/contracts/consumer_group.rb +42 -0
  62. data/lib/karafka/pro/routing/features/multiplexing/proxy.rb +38 -0
  63. data/lib/karafka/pro/routing/features/multiplexing/subscription_group.rb +42 -0
  64. data/lib/karafka/pro/routing/features/multiplexing/subscription_groups_builder.rb +40 -0
  65. data/lib/karafka/pro/routing/features/multiplexing.rb +59 -0
  66. data/lib/karafka/pro/routing/features/non_blocking_job/topic.rb +32 -0
  67. data/lib/karafka/pro/routing/features/non_blocking_job.rb +37 -0
  68. data/lib/karafka/pro/routing/features/offset_metadata/config.rb +33 -0
  69. data/lib/karafka/pro/routing/features/offset_metadata/contracts/topic.rb +42 -0
  70. data/lib/karafka/pro/routing/features/offset_metadata/topic.rb +65 -0
  71. data/lib/karafka/pro/routing/features/offset_metadata.rb +40 -0
  72. data/lib/karafka/pro/routing/features/patterns/contracts/consumer_group.rb +4 -0
  73. data/lib/karafka/pro/routing/features/patterns/detector.rb +18 -10
  74. data/lib/karafka/pro/routing/features/periodic_job/config.rb +37 -0
  75. data/lib/karafka/pro/routing/features/periodic_job/contracts/topic.rb +44 -0
  76. data/lib/karafka/pro/routing/features/periodic_job/topic.rb +94 -0
  77. data/lib/karafka/pro/routing/features/periodic_job.rb +27 -0
  78. data/lib/karafka/pro/routing/features/virtual_partitions/config.rb +1 -0
  79. data/lib/karafka/pro/routing/features/virtual_partitions/contracts/topic.rb +1 -0
  80. data/lib/karafka/pro/routing/features/virtual_partitions/topic.rb +7 -2
  81. data/lib/karafka/process.rb +5 -3
  82. data/lib/karafka/processing/coordinator.rb +5 -1
  83. data/lib/karafka/processing/executor.rb +16 -10
  84. data/lib/karafka/processing/executors_buffer.rb +19 -4
  85. data/lib/karafka/processing/schedulers/default.rb +3 -2
  86. data/lib/karafka/processing/strategies/default.rb +6 -0
  87. data/lib/karafka/processing/strategies/dlq.rb +36 -0
  88. data/lib/karafka/routing/builder.rb +12 -2
  89. data/lib/karafka/routing/consumer_group.rb +5 -5
  90. data/lib/karafka/routing/features/base.rb +44 -8
  91. data/lib/karafka/routing/features/dead_letter_queue/config.rb +6 -1
  92. data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +1 -0
  93. data/lib/karafka/routing/features/dead_letter_queue/topic.rb +9 -2
  94. data/lib/karafka/routing/subscription_group.rb +2 -2
  95. data/lib/karafka/routing/subscription_groups_builder.rb +11 -2
  96. data/lib/karafka/routing/topic.rb +8 -10
  97. data/lib/karafka/runner.rb +13 -3
  98. data/lib/karafka/server.rb +5 -9
  99. data/lib/karafka/setup/config.rb +17 -0
  100. data/lib/karafka/status.rb +23 -14
  101. data/lib/karafka/templates/karafka.rb.erb +7 -0
  102. data/lib/karafka/time_trackers/partition_usage.rb +56 -0
  103. data/lib/karafka/version.rb +1 -1
  104. data.tar.gz.sig +0 -0
  105. metadata +42 -10
  106. metadata.gz.sig +0 -0
  107. data/lib/karafka/connection/consumer_group_coordinator.rb +0 -48
@@ -27,6 +27,144 @@ module Karafka
27
27
  # Apply strategy for a non-feature based flow
28
28
  FEATURES = %i[].freeze
29
29
 
30
+ # Allows to set offset metadata that will be used with the upcoming marking as consumed
31
+ # as long as a different offset metadata was not used. After it was used either via
32
+ # `#mark_as_consumed` or `#mark_as_consumed!` it will be set back to `nil`. It is done
33
+ # that way to provide the end user with ability to influence metadata on the non-user
34
+ # initiated markings in complex flows.
35
+ #
36
+ # @param offset_metadata [String, nil] metadata we want to store with the upcoming
37
+ # marking as consumed
38
+ #
39
+ # @note Please be aware, that offset metadata set this way will be passed to any marking
40
+ # as consumed even if it was not user initiated. For example in the DLQ flow.
41
+ def store_offset_metadata(offset_metadata)
42
+ @_current_offset_metadata = offset_metadata
43
+ end
44
+
45
+ # Marks message as consumed in an async way.
46
+ #
47
+ # @param message [Messages::Message] last successfully processed message.
48
+ # @param offset_metadata [String, nil] offset metadata string or nil if nothing
49
+ # @return [Boolean] true if we were able to mark the offset, false otherwise.
50
+ # False indicates that we were not able and that we have lost the partition.
51
+ #
52
+ # @note We keep track of this offset in case we would mark as consumed and got error when
53
+ # processing another message. In case like this we do not pause on the message we've
54
+ # already processed but rather at the next one. This applies to both sync and async
55
+ # versions of this method.
56
+ def mark_as_consumed(message, offset_metadata = @_current_offset_metadata)
57
+ if @_in_transaction
58
+ mark_in_transaction(message, offset_metadata, true)
59
+ else
60
+ # seek offset can be nil only in case `#seek` was invoked with offset reset request
61
+ # In case like this we ignore marking
62
+ return true if coordinator.seek_offset.nil?
63
+ # Ignore earlier offsets than the one we already committed
64
+ return true if coordinator.seek_offset > message.offset
65
+ return false if revoked?
66
+ return revoked? unless client.mark_as_consumed(message, offset_metadata)
67
+
68
+ coordinator.seek_offset = message.offset + 1
69
+ end
70
+
71
+ true
72
+ ensure
73
+ @_current_offset_metadata = nil
74
+ end
75
+
76
+ # Marks message as consumed in a sync way.
77
+ #
78
+ # @param message [Messages::Message] last successfully processed message.
79
+ # @param offset_metadata [String, nil] offset metadata string or nil if nothing
80
+ # @return [Boolean] true if we were able to mark the offset, false otherwise.
81
+ # False indicates that we were not able and that we have lost the partition.
82
+ def mark_as_consumed!(message, offset_metadata = @_current_offset_metadata)
83
+ if @_in_transaction
84
+ mark_in_transaction(message, offset_metadata, false)
85
+ else
86
+ # seek offset can be nil only in case `#seek` was invoked with offset reset request
87
+ # In case like this we ignore marking
88
+ return true if coordinator.seek_offset.nil?
89
+ # Ignore earlier offsets than the one we already committed
90
+ return true if coordinator.seek_offset > message.offset
91
+ return false if revoked?
92
+
93
+ return revoked? unless client.mark_as_consumed!(message, offset_metadata)
94
+
95
+ coordinator.seek_offset = message.offset + 1
96
+ end
97
+
98
+ true
99
+ ensure
100
+ @_current_offset_metadata = nil
101
+ end
102
+
103
+ # Starts producer transaction, saves the transaction context for transactional marking
104
+ # and runs user code in this context
105
+ #
106
+ # Transactions on a consumer level differ from those initiated by the producer as they
107
+ # allow to mark offsets inside of the transaction. If the transaction is initialized
108
+ # only from the consumer, the offset will be stored in a regular fashion.
109
+ #
110
+ # @param block [Proc] code that we want to run in a transaction
111
+ def transaction(&block)
112
+ transaction_started = false
113
+
114
+ # Prevent from nested transactions. It would not make any sense
115
+ raise Errors::TransactionAlreadyInitializedError if @_in_transaction
116
+
117
+ transaction_started = true
118
+ @_transaction_marked = []
119
+ @_in_transaction = true
120
+
121
+ producer.transaction(&block)
122
+
123
+ @_in_transaction = false
124
+
125
+ # This offset is already stored in transaction but we set it here anyhow because we
126
+ # want to make sure our internal in-memory state is aligned with the transaction
127
+ #
128
+ # @note We never need to use the blocking `#mark_as_consumed!` here because the offset
129
+ # anyhow was already stored during the transaction
130
+ #
131
+ # @note In theory we could only keep reference to the most recent marking and reject
132
+ # others. We however do not do it for two reasons:
133
+ # - User may have non standard flow relying on some alternative order and we want to
134
+ # mimic this
135
+ # - Complex strategies like VPs can use this in VPs to mark in parallel without
136
+ # having to redefine the transactional flow completely
137
+ @_transaction_marked.each do |marking|
138
+ marking.pop ? mark_as_consumed(*marking) : mark_as_consumed!(*marking)
139
+ end
140
+ ensure
141
+ if transaction_started
142
+ @_transaction_marked.clear
143
+ @_in_transaction = false
144
+ end
145
+ end
146
+
147
+ # Stores the next offset for processing inside of the transaction and stores it in a
148
+ # local accumulator for post-transaction status update
149
+ #
150
+ # @param message [Messages::Message] message we want to commit inside of a transaction
151
+ # @param offset_metadata [String, nil] offset metadata or nil if none
152
+ # @param async [Boolean] should we mark in async or sync way (applicable only to post
153
+ # transaction state synchronization usage as within transaction it is always sync)
154
+ def mark_in_transaction(message, offset_metadata, async)
155
+ raise Errors::TransactionRequiredError unless @_in_transaction
156
+ raise Errors::AssignmentLostError if revoked?
157
+
158
+ producer.transaction_mark_as_consumed(
159
+ client,
160
+ message,
161
+ offset_metadata
162
+ )
163
+
164
+ @_transaction_marked ||= []
165
+ @_transaction_marked << [message, offset_metadata, async]
166
+ end
167
+
30
168
  # No actions needed for the standard flow here
31
169
  def handle_before_schedule_consume
32
170
  Karafka.monitor.instrument('consumer.before_schedule_consume', caller: self)
@@ -87,7 +225,7 @@ module Karafka
87
225
  end
88
226
  end
89
227
 
90
- # Standard
228
+ # Standard flow for revocation
91
229
  def handle_revoked
92
230
  coordinator.on_revoked do
93
231
  resume
@@ -100,6 +238,21 @@ module Karafka
100
238
  revoked
101
239
  end
102
240
  end
241
+
242
+ # No action needed for the tick standard flow
243
+ def handle_before_schedule_tick
244
+ Karafka.monitor.instrument('consumer.before_schedule_tick', caller: self)
245
+
246
+ nil
247
+ end
248
+
249
+ # Runs the consumer `#tick` method with reporting
250
+ def handle_tick
251
+ Karafka.monitor.instrument('consumer.tick', caller: self)
252
+ Karafka.monitor.instrument('consumer.ticked', caller: self) do
253
+ tick
254
+ end
255
+ end
103
256
  end
104
257
  end
105
258
  end
@@ -26,6 +26,45 @@ module Karafka
26
26
  dead_letter_queue
27
27
  ].freeze
28
28
 
29
+ # Override of the standard `#mark_as_consumed` in order to handle the pause tracker
30
+ # reset in case DLQ is marked as fully independent. When DLQ is marked independent,
31
+ # any offset marking causes the pause count tracker to reset. This is useful when
32
+ # the error is not due to the collective batch operations state but due to intermediate
33
+ # "crawling" errors that move with it
34
+ #
35
+ # @see `Strategies::Default#mark_as_consumed` for more details
36
+ # @param message [Messages::Message]
37
+ # @param offset_metadata [String, nil]
38
+ def mark_as_consumed(message, offset_metadata = @_current_offset_metadata)
39
+ return super unless retrying?
40
+ return super unless topic.dead_letter_queue.independent?
41
+ return false unless super
42
+
43
+ coordinator.pause_tracker.reset
44
+
45
+ true
46
+ ensure
47
+ @_current_offset_metadata = nil
48
+ end
49
+
50
+ # Override of the standard `#mark_as_consumed!`. Resets the pause tracker count in case
51
+ # DLQ was configured with the `independent` flag.
52
+ #
53
+ # @see `Strategies::Default#mark_as_consumed!` for more details
54
+ # @param message [Messages::Message]
55
+ # @param offset_metadata [String, nil]
56
+ def mark_as_consumed!(message, offset_metadata = @_current_offset_metadata)
57
+ return super unless retrying?
58
+ return super unless topic.dead_letter_queue.independent?
59
+ return false unless super
60
+
61
+ coordinator.pause_tracker.reset
62
+
63
+ true
64
+ ensure
65
+ @_current_offset_metadata = nil
66
+ end
67
+
29
68
  # When we encounter non-recoverable message, we skip it and go on with our lives
30
69
  def handle_after_consume
31
70
  coordinator.on_finished do |last_group_message|
@@ -29,40 +29,80 @@ module Karafka
29
29
  ].freeze
30
30
 
31
31
  # @param message [Karafka::Messages::Message] marks message as consumed
32
+ # @param offset_metadata [String, nil]
32
33
  # @note This virtual offset management uses a regular default marking API underneath.
33
34
  # We do not alter the "real" marking API, as VPs are just one of many cases we want
34
35
  # to support and we do not want to impact them with collective offsets management
35
- def mark_as_consumed(message)
36
- return super if collapsed?
37
-
38
- manager = coordinator.virtual_offset_manager
39
-
40
- coordinator.synchronize do
41
- manager.mark(message)
42
- # If this is last marking on a finished flow, we can use the original
43
- # last message and in order to do so, we need to mark all previous messages as
44
- # consumed as otherwise the computed offset could be different
45
- # We mark until our offset just in case of a DLQ flow or similar, where we do not
46
- # want to mark all but until the expected location
47
- manager.mark_until(message) if coordinator.finished?
48
-
49
- return revoked? unless manager.markable?
50
-
51
- manager.markable? ? super(manager.markable) : revoked?
36
+ def mark_as_consumed(message, offset_metadata = @_current_offset_metadata)
37
+ if @_in_transaction && !collapsed?
38
+ mark_in_transaction(message, offset_metadata, true)
39
+ elsif collapsed?
40
+ super
41
+ else
42
+ manager = coordinator.virtual_offset_manager
43
+
44
+ coordinator.synchronize do
45
+ manager.mark(message, offset_metadata)
46
+ # If this is last marking on a finished flow, we can use the original
47
+ # last message and in order to do so, we need to mark all previous messages as
48
+ # consumed as otherwise the computed offset could be different
49
+ # We mark until our offset just in case of a DLQ flow or similar, where we do not
50
+ # want to mark all but until the expected location
51
+ manager.mark_until(message, offset_metadata) if coordinator.finished?
52
+
53
+ return revoked? unless manager.markable?
54
+
55
+ manager.markable? ? super(*manager.markable) : revoked?
56
+ end
52
57
  end
58
+ ensure
59
+ @_current_offset_metadata = nil
53
60
  end
54
61
 
55
62
  # @param message [Karafka::Messages::Message] blocking marks message as consumed
56
- def mark_as_consumed!(message)
57
- return super if collapsed?
63
+ # @param offset_metadata [String, nil]
64
+ def mark_as_consumed!(message, offset_metadata = @_current_offset_metadata)
65
+ if @_in_transaction && !collapsed?
66
+ mark_in_transaction(message, offset_metadata, false)
67
+ elsif collapsed?
68
+ super
69
+ else
70
+ manager = coordinator.virtual_offset_manager
71
+
72
+ coordinator.synchronize do
73
+ manager.mark(message, offset_metadata)
74
+ manager.mark_until(message, offset_metadata) if coordinator.finished?
75
+ manager.markable? ? super(*manager.markable) : revoked?
76
+ end
77
+ end
78
+ ensure
79
+ @_current_offset_metadata = nil
80
+ end
58
81
 
59
- manager = coordinator.virtual_offset_manager
82
+ # Stores the next offset for processing inside of the transaction when collapsed and
83
+ # accumulates marking as consumed in the local buffer.
84
+ #
85
+ # Due to nature of VPs we cannot provide full EOS support but we can simulate it,
86
+ # making sure that no offset are stored unless transaction is finished. We do it by
87
+ # accumulating the post-transaction marking requests and after it is successfully done
88
+ # we mark each as consumed. This effectively on errors "rollbacks" the state and
89
+ # prevents offset storage.
90
+ #
91
+ # Since the EOS here is "weak", we do not have to worry about the race-conditions and
92
+ # we do not have to have any mutexes.
93
+ #
94
+ # @param message [Messages::Message] message we want to commit inside of a transaction
95
+ # @param offset_metadata [String, nil] offset metadata or nil if none
96
+ # @param async [Boolean] should we mark in async or sync way (applicable only to post
97
+ # transaction state synchronization usage as within transaction it is always sync)
98
+ def mark_in_transaction(message, offset_metadata, async)
99
+ raise Errors::TransactionRequiredError unless @_in_transaction
100
+ # Prevent from attempts of offset storage when we no longer own the assignment
101
+ raise Errors::AssignmentLostError if revoked?
60
102
 
61
- coordinator.synchronize do
62
- manager.mark(message)
63
- manager.mark_until(message) if coordinator.finished?
64
- manager.markable? ? super(manager.markable) : revoked?
65
- end
103
+ return super if collapsed?
104
+
105
+ @_transaction_marked << [message, offset_metadata, async]
66
106
  end
67
107
 
68
108
  # @return [Boolean] is the virtual processing collapsed in the context of given
@@ -30,22 +30,29 @@ module Karafka
30
30
 
31
31
  # @param topic [String]
32
32
  # @param partition [Integer]
33
+ # @param offset_metadata_strategy [Symbol] what metadata should we select. That is, should
34
+ # we use the most recent or one picked from the offset that is going to be committed
33
35
  #
34
36
  # @note We need topic and partition because we use a seek message (virtual) for real offset
35
37
  # management. We could keep real message reference but this can be memory consuming
36
38
  # and not worth it.
37
- def initialize(topic, partition)
39
+ def initialize(topic, partition, offset_metadata_strategy)
38
40
  @topic = topic
39
41
  @partition = partition
40
42
  @groups = []
41
43
  @marked = {}
44
+ @offsets_metadata = {}
42
45
  @real_offset = -1
46
+ @offset_metadata_strategy = offset_metadata_strategy
47
+ @current_offset_metadata = nil
43
48
  end
44
49
 
45
50
  # Clears the manager for a next collective operation
46
51
  def clear
47
52
  @groups.clear
48
- @marked = {}
53
+ @offsets_metadata.clear
54
+ @current_offset_metadata = nil
55
+ @marked.clear
49
56
  @real_offset = -1
50
57
  end
51
58
 
@@ -65,9 +72,14 @@ module Karafka
65
72
  # and we can refresh our real offset representation based on that as it might have changed
66
73
  # to a newer real offset.
67
74
  # @param message [Karafka::Messages::Message] message coming from VP we want to mark
68
- def mark(message)
75
+ # @param offset_metadata [String, nil] offset metadata. `nil` if none
76
+ def mark(message, offset_metadata)
69
77
  offset = message.offset
70
78
 
79
+ # Store metadata when we materialize the most stable offset
80
+ @offsets_metadata[offset] = offset_metadata
81
+ @current_offset_metadata = offset_metadata
82
+
71
83
  group = @groups.find { |reg_group| reg_group.include?(offset) }
72
84
 
73
85
  # This case can happen when someone uses MoM and wants to mark message from a previous
@@ -81,6 +93,9 @@ module Karafka
81
93
 
82
94
  # Mark all previous messages from the same group also as virtually consumed
83
95
  group[0..position].each do |markable_offset|
96
+ # Set previous messages metadata offset as the offset of higher one for overwrites
97
+ # unless a different metadata were set explicitely
98
+ @offsets_metadata[markable_offset] ||= offset_metadata
84
99
  @marked[markable_offset] = true
85
100
  end
86
101
 
@@ -91,13 +106,15 @@ module Karafka
91
106
  # Mark all from all groups including the `message`.
92
107
  # Useful when operating in a collapsed state for marking
93
108
  # @param message [Karafka::Messages::Message]
94
- def mark_until(message)
95
- mark(message)
109
+ # @param offset_metadata [String, nil]
110
+ def mark_until(message, offset_metadata)
111
+ mark(message, offset_metadata)
96
112
 
97
113
  @groups.each do |group|
98
114
  group.each do |offset|
99
115
  next if offset > message.offset
100
116
 
117
+ @offsets_metadata[offset] = offset_metadata
101
118
  @marked[offset] = true
102
119
  end
103
120
  end
@@ -116,15 +133,28 @@ module Karafka
116
133
  !@real_offset.negative?
117
134
  end
118
135
 
119
- # @return [Messages::Seek] markable message for real offset marking
136
+ # @return [Array<Messages::Seek, String>] markable message for real offset marking and
137
+ # its associated metadata
120
138
  def markable
121
139
  raise Errors::InvalidRealOffsetUsageError unless markable?
122
140
 
123
- Messages::Seek.new(
124
- @topic,
125
- @partition,
126
- @real_offset
127
- )
141
+ offset_metadata = case @offset_metadata_strategy
142
+ when :exact
143
+ @offsets_metadata.fetch(@real_offset)
144
+ when :current
145
+ @current_offset_metadata
146
+ else
147
+ raise Errors::UnsupportedCaseError, @offset_metadata_strategy
148
+ end
149
+
150
+ [
151
+ Messages::Seek.new(
152
+ @topic,
153
+ @partition,
154
+ @real_offset
155
+ ),
156
+ offset_metadata
157
+ ]
128
158
  end
129
159
 
130
160
  private
@@ -23,6 +23,8 @@ module Karafka
23
23
  @long_running_job ||= Config.new(active: active)
24
24
  end
25
25
 
26
+ alias long_running long_running_job
27
+
26
28
  # @return [Boolean] is a given job on a topic a long-running one
27
29
  def long_running_job?
28
30
  long_running_job.active?
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This Karafka component is a Pro component under a commercial license.
4
+ # This Karafka component is NOT licensed under LGPL.
5
+ #
6
+ # All of the commercial components are present in the lib/karafka/pro directory of this
7
+ # repository and their usage requires commercial license agreement.
8
+ #
9
+ # Karafka has also commercial-friendly license, commercial support and commercial components.
10
+ #
11
+ # By sending a pull request to the pro components, you are agreeing to transfer the copyright of
12
+ # your code to Maciej Mensfeld.
13
+
14
+ module Karafka
15
+ module Pro
16
+ module Routing
17
+ module Features
18
+ class Multiplexing < Base
19
+ # Multiplexing configuration
20
+ Config = Struct.new(
21
+ :active,
22
+ :min,
23
+ :max,
24
+ :boot,
25
+ keyword_init: true
26
+ ) do
27
+ alias_method :active?, :active
28
+
29
+ # @return [Boolean] true if we are allowed to upscale and downscale
30
+ def dynamic?
31
+ min < max
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This Karafka component is a Pro component under a commercial license.
4
+ # This Karafka component is NOT licensed under LGPL.
5
+ #
6
+ # All of the commercial components are present in the lib/karafka/pro directory of this
7
+ # repository and their usage requires commercial license agreement.
8
+ #
9
+ # Karafka has also commercial-friendly license, commercial support and commercial components.
10
+ #
11
+ # By sending a pull request to the pro components, you are agreeing to transfer the copyright of
12
+ # your code to Maciej Mensfeld.
13
+
14
+ module Karafka
15
+ module Pro
16
+ module Routing
17
+ module Features
18
+ class Multiplexing < Base
19
+ # Namespace for multiplexing feature contracts
20
+ module Contracts
21
+ # Validates the subscription group multiplexing setup
22
+ # We validate it on the topic level as subscription groups are not built during the
23
+ # routing as they are pre-run dynamically built.
24
+ #
25
+ # multiplexing attributes are optional since multiplexing may not be enabled
26
+ class Topic < Karafka::Contracts::Base
27
+ configure do |config|
28
+ config.error_messages = YAML.safe_load(
29
+ File.read(
30
+ File.join(Karafka.gem_root, 'config', 'locales', 'pro_errors.yml')
31
+ )
32
+ ).fetch('en').fetch('validations').fetch('topic')
33
+ end
34
+
35
+ nested(:subscription_group_details) do
36
+ optional(:multiplexing_min) { |val| val.is_a?(Integer) && val >= 1 }
37
+ optional(:multiplexing_max) { |val| val.is_a?(Integer) && val >= 1 }
38
+ optional(:multiplexing_boot) { |val| val.is_a?(Integer) && val >= 1 }
39
+ end
40
+
41
+ # Makes sure min is not more than max
42
+ virtual do |data, errors|
43
+ next unless errors.empty?
44
+ next unless min(data)
45
+ next unless max(data)
46
+
47
+ min = min(data)
48
+ max = max(data)
49
+
50
+ next if min <= max
51
+
52
+ [[%w[subscription_group_details], :multiplexing_min_max_mismatch]]
53
+ end
54
+
55
+ # Makes sure, that boot is between min and max
56
+ virtual do |data, errors|
57
+ next unless errors.empty?
58
+ next unless min(data)
59
+ next unless max(data)
60
+ next unless boot(data)
61
+
62
+ min = min(data)
63
+ max = max(data)
64
+ boot = boot(data)
65
+
66
+ next if boot >= min && boot <= max
67
+
68
+ [[%w[subscription_group_details], :multiplexing_boot_mismatch]]
69
+ end
70
+
71
+ # Makes sure, that boot is equal to min and max when not in dynamic mode
72
+ virtual do |data, errors|
73
+ next unless errors.empty?
74
+ next unless min(data)
75
+ next unless max(data)
76
+ next unless boot(data)
77
+
78
+ min = min(data)
79
+ max = max(data)
80
+ boot = boot(data)
81
+
82
+ # In dynamic mode there are other rules to check boot
83
+ next if min != max
84
+ next if boot == min
85
+
86
+ [[%w[subscription_group_details], :multiplexing_boot_not_dynamic]]
87
+ end
88
+
89
+ class << self
90
+ # @param data [Hash] topic details
91
+ # @return [Integer, false] min or false if missing
92
+ def min(data)
93
+ data[:subscription_group_details].fetch(:multiplexing_min, false)
94
+ end
95
+
96
+ # @param data [Hash] topic details
97
+ # @return [Integer, false] max or false if missing
98
+ def max(data)
99
+ data[:subscription_group_details].fetch(:multiplexing_max, false)
100
+ end
101
+
102
+ # @param data [Hash] topic details
103
+ # @return [Integer, false] boot or false if missing
104
+ def boot(data)
105
+ data[:subscription_group_details].fetch(:multiplexing_boot, false)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This Karafka component is a Pro component under a commercial license.
4
+ # This Karafka component is NOT licensed under LGPL.
5
+ #
6
+ # All of the commercial components are present in the lib/karafka/pro directory of this
7
+ # repository and their usage requires commercial license agreement.
8
+ #
9
+ # Karafka has also commercial-friendly license, commercial support and commercial components.
10
+ #
11
+ # By sending a pull request to the pro components, you are agreeing to transfer the copyright of
12
+ # your code to Maciej Mensfeld.
13
+
14
+ module Karafka
15
+ module Pro
16
+ module Routing
17
+ module Features
18
+ class Multiplexing < Base
19
+ # Patches to Karafka OSS
20
+ module Patches
21
+ # Contracts patches
22
+ module Contracts
23
+ # Consumer group contract patches
24
+ module ConsumerGroup
25
+ # Redefines the setup allowing for multiple sgs as long as with different names
26
+ #
27
+ # @param topic [Hash] topic config hash
28
+ # @return [Array] topic unique key for validators
29
+ def topic_unique_key(topic)
30
+ [
31
+ topic[:name],
32
+ topic[:subscription_group_details]
33
+ ]
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end