karafka 2.2.14 → 2.3.0.alpha2

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