karafka 2.4.9 → 2.4.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +9 -0
- data/Gemfile.lock +3 -3
- data/config/locales/errors.yml +1 -0
- data/config/locales/pro_errors.yml +17 -0
- data/lib/karafka/base_consumer.rb +23 -0
- data/lib/karafka/contracts/consumer_group.rb +17 -0
- data/lib/karafka/instrumentation/logger_listener.rb +3 -0
- data/lib/karafka/instrumentation/notifications.rb +3 -0
- data/lib/karafka/instrumentation/vendors/appsignal/client.rb +32 -11
- data/lib/karafka/instrumentation/vendors/appsignal/errors_listener.rb +1 -1
- data/lib/karafka/messages/message.rb +6 -0
- data/lib/karafka/pro/loader.rb +2 -1
- data/lib/karafka/pro/routing/features/recurring_tasks/builder.rb +9 -8
- data/lib/karafka/pro/routing/features/scheduled_messages/builder.rb +131 -0
- data/lib/karafka/pro/routing/features/scheduled_messages/config.rb +28 -0
- data/lib/karafka/pro/routing/features/scheduled_messages/contracts/topic.rb +40 -0
- data/lib/karafka/pro/routing/features/scheduled_messages/proxy.rb +27 -0
- data/lib/karafka/pro/routing/features/scheduled_messages/topic.rb +44 -0
- data/lib/karafka/pro/routing/features/scheduled_messages.rb +24 -0
- data/lib/karafka/pro/scheduled_messages/consumer.rb +185 -0
- data/lib/karafka/pro/scheduled_messages/contracts/config.rb +56 -0
- data/lib/karafka/pro/scheduled_messages/contracts/message.rb +61 -0
- data/lib/karafka/pro/scheduled_messages/daily_buffer.rb +79 -0
- data/lib/karafka/pro/scheduled_messages/day.rb +45 -0
- data/lib/karafka/pro/scheduled_messages/deserializers/headers.rb +46 -0
- data/lib/karafka/pro/scheduled_messages/deserializers/payload.rb +35 -0
- data/lib/karafka/pro/scheduled_messages/dispatcher.rb +122 -0
- data/lib/karafka/pro/scheduled_messages/errors.rb +28 -0
- data/lib/karafka/pro/scheduled_messages/max_epoch.rb +41 -0
- data/lib/karafka/pro/scheduled_messages/proxy.rb +176 -0
- data/lib/karafka/pro/scheduled_messages/schema_validator.rb +37 -0
- data/lib/karafka/pro/scheduled_messages/serializer.rb +55 -0
- data/lib/karafka/pro/scheduled_messages/setup/config.rb +60 -0
- data/lib/karafka/pro/scheduled_messages/state.rb +62 -0
- data/lib/karafka/pro/scheduled_messages/tracker.rb +64 -0
- data/lib/karafka/pro/scheduled_messages.rb +67 -0
- data/lib/karafka/processing/executor.rb +6 -0
- data/lib/karafka/processing/strategies/default.rb +10 -0
- data/lib/karafka/railtie.rb +0 -20
- data/lib/karafka/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +26 -3
- metadata.gz.sig +3 -2
@@ -0,0 +1,185 @@
|
|
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 ScheduledMessages
|
17
|
+
# Consumer that coordinates scheduling of messages when the time comes
|
18
|
+
class Consumer < ::Karafka::BaseConsumer
|
19
|
+
# Prepares the initial state of all stateful components
|
20
|
+
def initialized
|
21
|
+
clear!
|
22
|
+
# Max epoch is always moving forward with the time. Never backwards, hence we do not
|
23
|
+
# reset it at all.
|
24
|
+
@max_epoch = MaxEpoch.new
|
25
|
+
@state = State.new(nil)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Processes messages and runs dispatch (via tick) if needed
|
29
|
+
def consume
|
30
|
+
return if reload!
|
31
|
+
|
32
|
+
messages.each do |message|
|
33
|
+
SchemaValidator.call(message)
|
34
|
+
process_message(message)
|
35
|
+
end
|
36
|
+
|
37
|
+
@states_reporter.call
|
38
|
+
|
39
|
+
eofed if eofed?
|
40
|
+
|
41
|
+
# Unless given day data is fully loaded we should not dispatch any notifications nor
|
42
|
+
# should we mark messages.
|
43
|
+
return unless @state.loaded?
|
44
|
+
|
45
|
+
tick
|
46
|
+
|
47
|
+
# Despite the fact that we need to load the whole stream once a day we do mark.
|
48
|
+
# We mark as consumed for two main reasons:
|
49
|
+
# - by marking we can indicate to Web UI and other monitoring tools that we have a
|
50
|
+
# potential real lag with loading schedules in case there would be a lot of messages
|
51
|
+
# added to the schedules topic
|
52
|
+
# - we prevent a situation where there is no notion of this consumer group in the
|
53
|
+
# reporting, allowing us to establish "presence"
|
54
|
+
mark_as_consumed(messages.last)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Runs end of file operations
|
58
|
+
def eofed
|
59
|
+
return if reload!
|
60
|
+
|
61
|
+
# If end of the partition is reached, it always means all data is loaded
|
62
|
+
@state.loaded!
|
63
|
+
@states_reporter.call
|
64
|
+
end
|
65
|
+
|
66
|
+
# Performs periodic operations when no new data is provided to the topic partition
|
67
|
+
def tick
|
68
|
+
return if reload!
|
69
|
+
|
70
|
+
# We should not dispatch any data until the whole state is loaded. We need to make sure,
|
71
|
+
# that all tombstone events are loaded not to duplicate dispatches
|
72
|
+
return unless @state.loaded?
|
73
|
+
|
74
|
+
keys = []
|
75
|
+
epochs = []
|
76
|
+
|
77
|
+
# We first collect all the data for dispatch and then dispatch and **only** after
|
78
|
+
# dispatch that is sync is successful we remove those messages from the daily buffer
|
79
|
+
# and update the max epoch. Since only the dispatch itself is volatile and can crash
|
80
|
+
# with timeouts, etc, we need to be sure it wen through prior to deleting those messages
|
81
|
+
# from the daily buffer. That way we ensure the at least once delivery and in case of
|
82
|
+
# a transactional producer, exactly once delivery.
|
83
|
+
@daily_buffer.for_dispatch do |epoch, message|
|
84
|
+
epochs << epoch
|
85
|
+
keys << message.key
|
86
|
+
@dispatcher << message
|
87
|
+
end
|
88
|
+
|
89
|
+
@dispatcher.flush
|
90
|
+
|
91
|
+
@max_epoch.update(epochs.max)
|
92
|
+
|
93
|
+
keys.each { |key| @daily_buffer.delete(key) }
|
94
|
+
|
95
|
+
@states_reporter.call
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
# Takes each message and adds it to the daily accumulator if needed or performs other
|
101
|
+
# accumulator and time related per-message operations.
|
102
|
+
# @param message [Karafka::Messages::Message]
|
103
|
+
def process_message(message)
|
104
|
+
# If we started to receive messages younger than the moment we created the consumer for
|
105
|
+
# the given day, it means we have loaded all the history and we are no longer in the
|
106
|
+
# loading phase.
|
107
|
+
if message.timestamp.to_i > @today.created_at
|
108
|
+
@state.loaded!
|
109
|
+
tags.add(:state, @state.to_s)
|
110
|
+
end
|
111
|
+
|
112
|
+
# If this is a schedule message we need to check if this is for today. Tombstone events
|
113
|
+
# are always considered immediate as they indicate, that a message with a given key
|
114
|
+
# was already dispatched or that user decided not to dispatch and cancelled the dispatch
|
115
|
+
# via tombstone publishing.
|
116
|
+
if message.headers['schedule_source_type'] == 'schedule'
|
117
|
+
time = message.headers['schedule_target_epoch']
|
118
|
+
|
119
|
+
# Do not track historical below today as those will be reflected in the daily buffer
|
120
|
+
@tracker.track(message) if time >= @today.starts_at
|
121
|
+
|
122
|
+
if time > @today.ends_at || time < @max_epoch.to_i
|
123
|
+
# Clean the message immediately when not needed (won't be scheduled) to preserve
|
124
|
+
# memory
|
125
|
+
message.clean!
|
126
|
+
|
127
|
+
return
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Add to buffer all tombstones and messages for the same day
|
132
|
+
@daily_buffer << message
|
133
|
+
end
|
134
|
+
|
135
|
+
# Moves the offset back and re-seeks and reloads the current day not dispatched assignments
|
136
|
+
def reload!
|
137
|
+
# If this is a new assignment we always need to seek from beginning to load the data
|
138
|
+
if @state.fresh?
|
139
|
+
clear!
|
140
|
+
seek(0)
|
141
|
+
|
142
|
+
return true
|
143
|
+
end
|
144
|
+
|
145
|
+
# Unless state is loaded we do not do anything more because we're in the loading process
|
146
|
+
return false unless @state.loaded?
|
147
|
+
|
148
|
+
# If day has ended we reload and start new day with new schedules
|
149
|
+
if @today.ended?
|
150
|
+
clear!
|
151
|
+
seek(0)
|
152
|
+
|
153
|
+
return true
|
154
|
+
end
|
155
|
+
|
156
|
+
false
|
157
|
+
end
|
158
|
+
|
159
|
+
# Resets all buffers and states so we can start a new day with a clean slate
|
160
|
+
# We can fully recreate the dispatcher because any undispatched messages will be dispatched
|
161
|
+
# with the new day dispatcher after it is reloaded.
|
162
|
+
def clear!
|
163
|
+
@daily_buffer = DailyBuffer.new
|
164
|
+
@today = Day.new
|
165
|
+
@tracker = Tracker.new
|
166
|
+
@state = State.new(false)
|
167
|
+
@dispatcher = config.dispatcher_class.new(topic.name, partition)
|
168
|
+
@states_reporter = Helpers::IntervalRunner.new do
|
169
|
+
@tracker.today = @daily_buffer.size
|
170
|
+
@tracker.state = @state.to_s
|
171
|
+
|
172
|
+
@dispatcher.state(@tracker)
|
173
|
+
end
|
174
|
+
|
175
|
+
tags.add(:state, @state.to_s)
|
176
|
+
end
|
177
|
+
|
178
|
+
# @return [Karafka::Core::Configurable::Node] Schedules config node
|
179
|
+
def config
|
180
|
+
@config ||= Karafka::App.config.scheduled_messages
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,56 @@
|
|
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 ScheduledMessages
|
17
|
+
# Recurring Tasks related contracts
|
18
|
+
module Contracts
|
19
|
+
# Makes sure, all the expected config is defined as it should be
|
20
|
+
class Config < ::Karafka::Contracts::Base
|
21
|
+
configure do |config|
|
22
|
+
config.error_messages = YAML.safe_load(
|
23
|
+
File.read(
|
24
|
+
File.join(Karafka.gem_root, 'config', 'locales', 'pro_errors.yml')
|
25
|
+
)
|
26
|
+
).fetch('en').fetch('validations').fetch('config')
|
27
|
+
end
|
28
|
+
|
29
|
+
nested(:scheduled_messages) do
|
30
|
+
required(:consumer_class) { |val| val < ::Karafka::BaseConsumer }
|
31
|
+
|
32
|
+
# Do not allow to run more often than every second
|
33
|
+
required(:interval) { |val| val.is_a?(Integer) && val >= 1_000 }
|
34
|
+
|
35
|
+
required(:flush_batch_size) { |val| val.is_a?(Integer) && val.positive? }
|
36
|
+
|
37
|
+
required(:dispatcher_class) { |val| !val.nil? }
|
38
|
+
|
39
|
+
required(:group_id) do |val|
|
40
|
+
val.is_a?(String) && Karafka::Contracts::TOPIC_REGEXP.match?(val)
|
41
|
+
end
|
42
|
+
|
43
|
+
required(:states_postfix) do |val|
|
44
|
+
val.is_a?(String) && Karafka::Contracts::TOPIC_REGEXP.match?(val)
|
45
|
+
end
|
46
|
+
|
47
|
+
nested(:deserializers) do
|
48
|
+
required(:headers) { |val| !val.nil? }
|
49
|
+
required(:payload) { |val| !val.nil? }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,61 @@
|
|
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 ScheduledMessages
|
17
|
+
module Contracts
|
18
|
+
# Future message expected format.
|
19
|
+
#
|
20
|
+
# Our envelope always needs to comply with this format, otherwise we won't have enough
|
21
|
+
# details to be able to dispatch the message
|
22
|
+
class Message < ::Karafka::Contracts::Base
|
23
|
+
configure do |config|
|
24
|
+
config.error_messages = YAML.safe_load(
|
25
|
+
File.read(
|
26
|
+
File.join(Karafka.gem_root, 'config', 'locales', 'pro_errors.yml')
|
27
|
+
)
|
28
|
+
).fetch('en').fetch('validations').fetch('scheduled_messages_message')
|
29
|
+
end
|
30
|
+
|
31
|
+
# Headers we expect in each message of type "message" that goes to our scheduled messages
|
32
|
+
# topic
|
33
|
+
EXPECTED_HEADERS = %w[
|
34
|
+
schedule_schema_version
|
35
|
+
schedule_target_epoch
|
36
|
+
schedule_source_type
|
37
|
+
schedule_target_topic
|
38
|
+
].freeze
|
39
|
+
|
40
|
+
required(:key) { |val| val.is_a?(String) && val.size.positive? }
|
41
|
+
required(:headers) { |val| val.is_a?(Hash) && (val.keys & EXPECTED_HEADERS).size == 4 }
|
42
|
+
|
43
|
+
# Make sure, that schedule_target_epoch is not older than grace period behind us.
|
44
|
+
# While this is not ideal verification of scheduling stuff in past, at leats it will
|
45
|
+
# prevent user errors when they schedule at 0, etc
|
46
|
+
virtual do |data, errors|
|
47
|
+
next unless errors.empty?
|
48
|
+
|
49
|
+
epoch_time = data[:headers].fetch('schedule_target_epoch').to_i
|
50
|
+
|
51
|
+
# We allow for small lag as those will be dispatched but we should prevent dispatching
|
52
|
+
# in the past in general as often it is a source of errors
|
53
|
+
next if epoch_time >= Time.now.to_i - 10
|
54
|
+
|
55
|
+
[[[:headers], :schedule_target_epoch_in_the_past]]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,79 @@
|
|
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 ScheduledMessages
|
17
|
+
# Stores schedules for the current day and gives back those that should be dispatched
|
18
|
+
# We do not use min-heap implementation and just a regular hash because we want to be able
|
19
|
+
# to update the schedules based on the key as well as remove the schedules in case it would
|
20
|
+
# be cancelled. While removals could be implemented, updates with different timestamp would
|
21
|
+
# be more complex. At the moment a lookup of 8 640 000 messages (100 per second) takes
|
22
|
+
# up to 1.5 second, thus it is acceptable. Please ping me if you encounter performance
|
23
|
+
# issues with this naive implementation so it can be improved.
|
24
|
+
class DailyBuffer
|
25
|
+
def initialize
|
26
|
+
@accu = {}
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Integer] number of elements to schedule today
|
30
|
+
def size
|
31
|
+
@accu.size
|
32
|
+
end
|
33
|
+
|
34
|
+
# Adds message to the buffer or removes the message from the buffer if it is a tombstone
|
35
|
+
# message for a given key
|
36
|
+
#
|
37
|
+
# @param message [Karafka::Messages::Message]
|
38
|
+
#
|
39
|
+
# @note Only messages for a given day should be added here.
|
40
|
+
def <<(message)
|
41
|
+
# Non schedule are only tombstones and cancellations
|
42
|
+
schedule = message.headers['schedule_source_type'] == 'schedule'
|
43
|
+
|
44
|
+
key = message.key
|
45
|
+
|
46
|
+
if schedule
|
47
|
+
epoch = message.headers['schedule_target_epoch']
|
48
|
+
@accu[key] = [epoch, message]
|
49
|
+
else
|
50
|
+
@accu.delete(key)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Yields messages that should be dispatched (sent) to Kafka
|
55
|
+
#
|
56
|
+
# @yieldparam [Integer, Karafka::Messages::Message] epoch of the message and the message
|
57
|
+
# itself
|
58
|
+
#
|
59
|
+
# @note We yield epoch alongside of the message so we do not have to extract it several
|
60
|
+
# times later on. This simplifies the API
|
61
|
+
def for_dispatch
|
62
|
+
dispatch = Time.now.to_i
|
63
|
+
|
64
|
+
@accu.each_value do |epoch, message|
|
65
|
+
next unless epoch <= dispatch
|
66
|
+
|
67
|
+
yield(epoch, message)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Removes given key from the accumulator
|
72
|
+
# @param key [String] key to remove
|
73
|
+
def delete(key)
|
74
|
+
@accu.delete(key)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,45 @@
|
|
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 ScheduledMessages
|
17
|
+
# Just a simple UTC day implementation.
|
18
|
+
# Since we operate on a scope of one day, this allows us to encapsulate when given day ends
|
19
|
+
class Day
|
20
|
+
# @return [Integer] utc timestamp when this day object was created. Keep in mind, that
|
21
|
+
# this is **not** when the day started but when this object was created.
|
22
|
+
attr_reader :created_at
|
23
|
+
# @return [Integer] utc timestamp when this day ends (last second of day).
|
24
|
+
# Equal to 23:59:59.
|
25
|
+
attr_reader :ends_at
|
26
|
+
# @return [Integer] utc timestamp when this day starts. Equal to 00:00:00
|
27
|
+
attr_reader :starts_at
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
@created_at = Time.now.to_i
|
31
|
+
|
32
|
+
time = Time.at(@created_at)
|
33
|
+
|
34
|
+
@starts_at = Time.utc(time.year, time.month, time.day).to_i
|
35
|
+
@ends_at = @starts_at + 86_399
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Boolean] did the current day we operate on ended.
|
39
|
+
def ended?
|
40
|
+
@ends_at < Time.now.to_i
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,46 @@
|
|
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 ScheduledMessages
|
17
|
+
# Namespace for schedules data related deserializers.
|
18
|
+
module Deserializers
|
19
|
+
# Converts certain pieces of headers into their integer form for messages
|
20
|
+
class Headers
|
21
|
+
# @param metadata [Karafka::aMessages::Metadata]
|
22
|
+
# @return [Hash] headers
|
23
|
+
def call(metadata)
|
24
|
+
raw_headers = metadata.raw_headers
|
25
|
+
|
26
|
+
type = raw_headers.fetch('schedule_source_type')
|
27
|
+
|
28
|
+
# tombstone and cancellation events are not operable, thus we do not have to cast any
|
29
|
+
# of the headers pieces
|
30
|
+
return raw_headers unless type == 'schedule'
|
31
|
+
|
32
|
+
headers = raw_headers.dup
|
33
|
+
headers['schedule_target_epoch'] = headers['schedule_target_epoch'].to_i
|
34
|
+
|
35
|
+
# This attribute is optional, this is why we have to check for its existence
|
36
|
+
if headers.key?('schedule_target_partition')
|
37
|
+
headers['schedule_target_partition'] = headers['schedule_target_partition'].to_i
|
38
|
+
end
|
39
|
+
|
40
|
+
headers
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,35 @@
|
|
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 ScheduledMessages
|
17
|
+
module Deserializers
|
18
|
+
# States payload deserializer
|
19
|
+
# We only deserialize states data and never anything else. Other payloads are the payloads
|
20
|
+
# we are expected to proxy, thus there is no need to deserialize them in any context.
|
21
|
+
# Their appropriate target topics should have expected deserializers
|
22
|
+
class Payload
|
23
|
+
# @param message [::Karafka::Messages::Message]
|
24
|
+
# @return [Hash] deserialized data
|
25
|
+
def call(message)
|
26
|
+
::JSON.parse(
|
27
|
+
Zlib::Inflate.inflate(message.raw_payload),
|
28
|
+
symbolize_names: true
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,122 @@
|
|
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 ScheduledMessages
|
17
|
+
# Dispatcher responsible for dispatching the messages to appropriate target topics and for
|
18
|
+
# dispatching other messages. All messages (aside from the once users dispatch with the
|
19
|
+
# envelope) are sent via this dispatcher.
|
20
|
+
#
|
21
|
+
# Messages are buffered and dispatched in batches to improve dispatch performance.
|
22
|
+
class Dispatcher
|
23
|
+
# @return [Array<Hash>] buffer with message hashes for dispatch
|
24
|
+
attr_reader :buffer
|
25
|
+
|
26
|
+
# @param topic [String] consumed topic name
|
27
|
+
# @param partition [Integer] consumed partition
|
28
|
+
def initialize(topic, partition)
|
29
|
+
@topic = topic
|
30
|
+
@partition = partition
|
31
|
+
@buffer = []
|
32
|
+
@serializer = Serializer.new
|
33
|
+
end
|
34
|
+
|
35
|
+
# Prepares the scheduled message to the dispatch to the target topic. Extracts all the
|
36
|
+
# "schedule_" details and prepares it, so the dispatched message goes with the expected
|
37
|
+
# attributes to the desired location. Alongside of that it actually builds 2
|
38
|
+
# (1 if logs off) messages: tombstone event matching the schedule so it is no longer valid
|
39
|
+
# and the log message that has the same data as the dispatched message. Helpful when
|
40
|
+
# debugging.
|
41
|
+
#
|
42
|
+
# @param message [Karafka::Messages::Message] message from the schedules topic.
|
43
|
+
#
|
44
|
+
# @note This method adds the message to the buffer, does **not** dispatch it.
|
45
|
+
# @note It also produces needed tombstone event as well as an audit log message
|
46
|
+
def <<(message)
|
47
|
+
target_headers = message.raw_headers.merge(
|
48
|
+
'schedule_source_topic' => @topic,
|
49
|
+
'schedule_source_partition' => @partition.to_s,
|
50
|
+
'schedule_source_offset' => message.offset.to_s,
|
51
|
+
'schedule_source_key' => message.key
|
52
|
+
).compact
|
53
|
+
|
54
|
+
target = {
|
55
|
+
payload: message.raw_payload,
|
56
|
+
headers: target_headers
|
57
|
+
}
|
58
|
+
|
59
|
+
extract(target, message.headers, :topic)
|
60
|
+
extract(target, message.headers, :partition)
|
61
|
+
extract(target, message.headers, :key)
|
62
|
+
extract(target, message.headers, :partition_key)
|
63
|
+
|
64
|
+
@buffer << target
|
65
|
+
|
66
|
+
# Tombstone message so this schedule is no longer in use and gets removed from Kafka by
|
67
|
+
# Kafka itself during compacting. It will not cancel it because already dispatched but
|
68
|
+
# will cause it not to be sent again and will be marked as dispatched.
|
69
|
+
@buffer << Proxy.tombstone(message: message)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Builds and dispatches the state report message with schedules details
|
73
|
+
#
|
74
|
+
# @param tracker [Tracker]
|
75
|
+
#
|
76
|
+
# @note This is dispatched async because it's just a statistical metric.
|
77
|
+
def state(tracker)
|
78
|
+
config.producer.produce_async(
|
79
|
+
topic: "#{@topic}#{config.states_postfix}",
|
80
|
+
payload: @serializer.state(tracker),
|
81
|
+
key: 'state',
|
82
|
+
partition: @partition,
|
83
|
+
headers: { 'zlib' => 'true' }
|
84
|
+
)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Sends all messages to Kafka in a sync way.
|
88
|
+
# We use sync with batches to prevent overloading.
|
89
|
+
# When transactional producer in use, this will be wrapped in a transaction automatically.
|
90
|
+
def flush
|
91
|
+
until @buffer.empty?
|
92
|
+
config.producer.produce_many_sync(
|
93
|
+
# We can remove this prior to the dispatch because we only evict messages from the
|
94
|
+
# daily buffer once dispatch is successful
|
95
|
+
@buffer.shift(config.flush_batch_size)
|
96
|
+
)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
# @return [Karafka::Core::Configurable::Node] scheduled messages config node
|
103
|
+
def config
|
104
|
+
@config ||= Karafka::App.config.scheduled_messages
|
105
|
+
end
|
106
|
+
|
107
|
+
# Extracts and copies the future attribute to a proper place in the target message.
|
108
|
+
#
|
109
|
+
# @param target [Hash]
|
110
|
+
# @param headers [Hash]
|
111
|
+
# @param attribute [Symbol]
|
112
|
+
def extract(target, headers, attribute)
|
113
|
+
schedule_attribute = "schedule_target_#{attribute}"
|
114
|
+
|
115
|
+
return unless headers.key?(schedule_attribute)
|
116
|
+
|
117
|
+
target[attribute] = headers[schedule_attribute]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,28 @@
|
|
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 ScheduledMessages
|
17
|
+
# Scheduled Messages related errors
|
18
|
+
module Errors
|
19
|
+
# Base for all the recurring tasks errors
|
20
|
+
BaseError = Class.new(::Karafka::Errors::BaseError)
|
21
|
+
|
22
|
+
# Raised when the scheduled messages processor is older than the messages we started to
|
23
|
+
# receive. In such cases we stop because there may be schema changes.
|
24
|
+
IncompatibleSchemaError = Class.new(BaseError)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|