karafka 2.4.8 → 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/.github/workflows/ci.yml +0 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +17 -0
- data/Gemfile +8 -5
- data/Gemfile.lock +24 -15
- data/bin/integrations +5 -0
- data/certs/cert.pem +26 -0
- data/config/locales/errors.yml +5 -0
- data/config/locales/pro_errors.yml +34 -0
- data/karafka.gemspec +1 -1
- data/lib/karafka/admin.rb +42 -0
- data/lib/karafka/base_consumer.rb +23 -0
- data/lib/karafka/contracts/config.rb +2 -0
- data/lib/karafka/contracts/consumer_group.rb +17 -0
- data/lib/karafka/errors.rb +3 -2
- 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 +3 -1
- data/lib/karafka/pro/processing/strategies/dlq/default.rb +16 -1
- data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj_mom.rb +5 -1
- data/lib/karafka/pro/processing/strategies/dlq/ftr_mom.rb +17 -1
- data/lib/karafka/pro/processing/strategies/dlq/lrj_mom.rb +17 -1
- data/lib/karafka/pro/processing/strategies/dlq/mom.rb +22 -6
- data/lib/karafka/pro/recurring_tasks/consumer.rb +105 -0
- data/lib/karafka/pro/recurring_tasks/contracts/config.rb +53 -0
- data/lib/karafka/pro/recurring_tasks/contracts/task.rb +41 -0
- data/lib/karafka/pro/recurring_tasks/deserializer.rb +35 -0
- data/lib/karafka/pro/recurring_tasks/dispatcher.rb +87 -0
- data/lib/karafka/pro/recurring_tasks/errors.rb +34 -0
- data/lib/karafka/pro/recurring_tasks/executor.rb +152 -0
- data/lib/karafka/pro/recurring_tasks/listener.rb +38 -0
- data/lib/karafka/pro/recurring_tasks/matcher.rb +38 -0
- data/lib/karafka/pro/recurring_tasks/schedule.rb +63 -0
- data/lib/karafka/pro/recurring_tasks/serializer.rb +113 -0
- data/lib/karafka/pro/recurring_tasks/setup/config.rb +52 -0
- data/lib/karafka/pro/recurring_tasks/task.rb +151 -0
- data/lib/karafka/pro/recurring_tasks.rb +87 -0
- data/lib/karafka/pro/routing/features/recurring_tasks/builder.rb +131 -0
- data/lib/karafka/pro/routing/features/recurring_tasks/config.rb +28 -0
- data/lib/karafka/pro/routing/features/recurring_tasks/contracts/topic.rb +40 -0
- data/lib/karafka/pro/routing/features/recurring_tasks/proxy.rb +27 -0
- data/lib/karafka/pro/routing/features/recurring_tasks/topic.rb +44 -0
- data/lib/karafka/pro/routing/features/recurring_tasks.rb +25 -0
- 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/processing/strategies/dlq.rb +16 -2
- data/lib/karafka/processing/strategies/dlq_mom.rb +25 -6
- data/lib/karafka/processing/worker.rb +11 -1
- data/lib/karafka/railtie.rb +11 -42
- data/lib/karafka/routing/features/dead_letter_queue/config.rb +3 -0
- data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +1 -0
- data/lib/karafka/routing/features/dead_letter_queue/topic.rb +7 -2
- data/lib/karafka/routing/features/eofed/contracts/topic.rb +12 -0
- data/lib/karafka/routing/topic.rb +14 -0
- data/lib/karafka/setup/config.rb +3 -0
- data/lib/karafka/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +68 -25
- metadata.gz.sig +0 -0
- data/certs/cert_chain.pem +0 -26
@@ -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
|
@@ -0,0 +1,41 @@
|
|
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
|
+
# Simple max value accumulator. When we dispatch messages we can store the max timestamp
|
18
|
+
# until which messages were dispatched by us. This allows us to quickly skip those messages
|
19
|
+
# during recovery, because we do know, they were dispatched.
|
20
|
+
class MaxEpoch
|
21
|
+
def initialize
|
22
|
+
@max = -1
|
23
|
+
end
|
24
|
+
|
25
|
+
# Updates epoch if bigger than current max
|
26
|
+
# @param new_max [Integer] potential new max epoch
|
27
|
+
def update(new_max)
|
28
|
+
return unless new_max
|
29
|
+
return unless new_max > @max
|
30
|
+
|
31
|
+
@max = new_max
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Integer] max epoch recorded
|
35
|
+
def to_i
|
36
|
+
@max
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,176 @@
|
|
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
|
+
# Proxy used to wrap the scheduled messages with the correct dispatch envelope.
|
18
|
+
# Each message that goes to the scheduler topic needs to have specific headers and other
|
19
|
+
# details that are required by the system so we know how and when to dispatch it.
|
20
|
+
#
|
21
|
+
# Each message that goes to the proxy topic needs to have a unique key. We inject those
|
22
|
+
# automatically unless user provides one in an envelope. Since we want to make sure, that
|
23
|
+
# the messages dispatched by the user all go to the same partition (if with same key), we
|
24
|
+
# inject a partition_key based on the user key or other details if present. That allows us
|
25
|
+
# to make sure, that they will always go to the same partition on our side.
|
26
|
+
#
|
27
|
+
# This wrapper validates the initial message that user wants to send in the future, as well
|
28
|
+
# as the envelope and specific requirements for a message to be send in the future
|
29
|
+
module Proxy
|
30
|
+
# General WaterDrop message contract. Before we envelop a message, we need to be certain
|
31
|
+
# it is correct, hence we use this contract.
|
32
|
+
MSG_CONTRACT = ::WaterDrop::Contracts::Message.new(
|
33
|
+
# Payload size is a subject to the target producer dispatch validation, so we set it
|
34
|
+
# to 100MB basically to ignore it here.
|
35
|
+
max_payload_size: 104_857_600
|
36
|
+
)
|
37
|
+
|
38
|
+
# Post-rebind contract to ensure, that user provided all needed details that would allow
|
39
|
+
# the system to operate correctly
|
40
|
+
POST_CONTRACT = Contracts::Message.new
|
41
|
+
|
42
|
+
# Attributes used to build a partition key for the schedules topic dispatch of a given
|
43
|
+
# message. We use this order as this order describes the priority of usage.
|
44
|
+
PARTITION_KEY_BASE_ATTRIBUTES = %i[
|
45
|
+
partition
|
46
|
+
partition_key
|
47
|
+
].freeze
|
48
|
+
|
49
|
+
private_constant :MSG_CONTRACT, :POST_CONTRACT, :PARTITION_KEY_BASE_ATTRIBUTES
|
50
|
+
|
51
|
+
class << self
|
52
|
+
# Generates a schedule message envelope wrapping the original dispatch
|
53
|
+
#
|
54
|
+
# @param message [Hash] message hash of a message that would originally go to WaterDrop
|
55
|
+
# producer directly.
|
56
|
+
# @param epoch [Integer] time in the future (or now) when dispatch this message in the
|
57
|
+
# Unix epoch timestamp
|
58
|
+
# @param envelope [Hash] Special details that the envelop needs to have, like a unique
|
59
|
+
# key. If unique key is not provided we build a random unique one and use a
|
60
|
+
# partition_key based on the original message key (if present) to ensure that all
|
61
|
+
# relevant messages are dispatched to the same topic partition.
|
62
|
+
# @return [Hash] dispatched message wrapped with an envelope
|
63
|
+
#
|
64
|
+
# @note This proxy does **not** inject the dispatched messages topic unless provided in
|
65
|
+
# the envelope. That's because user can have multiple scheduled messages topics to
|
66
|
+
# group outgoing messages, etc.
|
67
|
+
def schedule(message:, epoch:, envelope: {})
|
68
|
+
# We need to ensure that the message we want to proxy is fully legit. Otherwise, since
|
69
|
+
# we envelope details like target topic, we could end up having incorrect data to
|
70
|
+
# schedule
|
71
|
+
MSG_CONTRACT.validate!(message, WaterDrop::Errors::MessageInvalidError)
|
72
|
+
|
73
|
+
headers = (message[:headers] || {}).merge(
|
74
|
+
'schedule_schema_version' => ScheduledMessages::SCHEMA_VERSION,
|
75
|
+
'schedule_target_epoch' => epoch.to_i.to_s,
|
76
|
+
'schedule_source_type' => 'schedule'
|
77
|
+
)
|
78
|
+
|
79
|
+
export(headers, message, :topic)
|
80
|
+
export(headers, message, :partition)
|
81
|
+
export(headers, message, :key)
|
82
|
+
export(headers, message, :partition_key)
|
83
|
+
|
84
|
+
proxy_message = {
|
85
|
+
payload: message[:payload],
|
86
|
+
headers: headers
|
87
|
+
}.merge(envelope)
|
88
|
+
|
89
|
+
enrich(proxy_message, message)
|
90
|
+
|
91
|
+
# Final validation to make sure all user provided extra data and what we have built
|
92
|
+
# complies with our requirements
|
93
|
+
POST_CONTRACT.validate!(proxy_message)
|
94
|
+
# After proxy specific validations we also ensure, that the final form is correct
|
95
|
+
MSG_CONTRACT.validate!(proxy_message, WaterDrop::Errors::MessageInvalidError)
|
96
|
+
|
97
|
+
proxy_message
|
98
|
+
end
|
99
|
+
|
100
|
+
# Generates a tombstone message to cancel already scheduled message dispatch
|
101
|
+
# @param key [String] key used by the original message as a unique identifier
|
102
|
+
# @param envelope [Hash] Special details that can identify the message location like
|
103
|
+
# topic and partition (if used) so the cancellation goes to the correct location.
|
104
|
+
# @return [Hash] cancellation message
|
105
|
+
#
|
106
|
+
# @note Technically it is a tombstone but we differentiate just for the sake of ability
|
107
|
+
# to debug stuff if needed
|
108
|
+
def cancel(key:, envelope: {})
|
109
|
+
{
|
110
|
+
key: key,
|
111
|
+
payload: nil,
|
112
|
+
headers: {
|
113
|
+
'schedule_schema_version' => ScheduledMessages::SCHEMA_VERSION,
|
114
|
+
'schedule_source_type' => 'cancel'
|
115
|
+
}
|
116
|
+
}.merge(envelope)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Builds tombstone with the dispatched message details. Those details can be used
|
120
|
+
# in Web UI, etc when analyzing dispatches.
|
121
|
+
# @param message [Karafka::Messages::Message] message we want to tombstone
|
122
|
+
# topic and partition (if used) so the cancellation goes to the correct location.
|
123
|
+
def tombstone(message:)
|
124
|
+
{
|
125
|
+
key: message.key,
|
126
|
+
payload: nil,
|
127
|
+
topic: message.topic,
|
128
|
+
partition: message.partition,
|
129
|
+
headers: message.raw_headers.merge(
|
130
|
+
'schedule_schema_version' => ScheduledMessages::SCHEMA_VERSION,
|
131
|
+
'schedule_source_type' => 'tombstone',
|
132
|
+
'schedule_source_offset' => message.offset.to_s
|
133
|
+
)
|
134
|
+
}
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
# Transfers the message key attributes into headers. Since we need to have our own
|
140
|
+
# envelope key and other details, we transfer the original message details into headers
|
141
|
+
# so we can re-use them when we dispatch the scheduled messages at an appropriate time
|
142
|
+
#
|
143
|
+
# @param headers [Hash] envelope headers to which we will add appropriate attribute
|
144
|
+
# @param message [Hash] original user message
|
145
|
+
# @param attribute [Symbol] attribute we're interested in exporting to headers
|
146
|
+
# @note Modifies headers in place
|
147
|
+
def export(headers, message, attribute)
|
148
|
+
return unless message.key?(attribute)
|
149
|
+
|
150
|
+
headers["schedule_target_#{attribute}"] = message.fetch(attribute).to_s
|
151
|
+
end
|
152
|
+
|
153
|
+
# Adds the key and (if applicable) partition key to ensure, that related messages that
|
154
|
+
# user wants to dispatch in the future, are all in the same topic partition.
|
155
|
+
# @param proxy_message [Hash] our message envelope
|
156
|
+
# @param message [Hash] user original message
|
157
|
+
# @note Modifies `proxy_message` in place
|
158
|
+
def enrich(proxy_message, message)
|
159
|
+
# If there is an envelope message key already, nothing needed
|
160
|
+
return if proxy_message.key?(:key)
|
161
|
+
|
162
|
+
proxy_message[:key] = "#{message[:topic]}-#{SecureRandom.uuid}"
|
163
|
+
|
164
|
+
PARTITION_KEY_BASE_ATTRIBUTES.each do |attribute|
|
165
|
+
next unless message.key?(attribute)
|
166
|
+
# Do not overwrite if explicitely set by the user
|
167
|
+
next if proxy_message.key?(attribute)
|
168
|
+
|
169
|
+
proxy_message[:partition_key] = message.fetch(attribute).to_s
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,37 @@
|
|
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
|
+
# Validator that checks if we can process this scheduled message
|
18
|
+
# If we encounter message that has a schema version higher than our process is aware of
|
19
|
+
# we raise and error and do not process. This is to make sure we do not deal with messages
|
20
|
+
# that are not compatible in case of schema changes.
|
21
|
+
module SchemaValidator
|
22
|
+
class << self
|
23
|
+
# Check if we can work with this schema message and raise error if not.
|
24
|
+
#
|
25
|
+
# @param message [Karafka::Messages::Message]
|
26
|
+
def call(message)
|
27
|
+
message_version = message.headers['schedule_schema_version']
|
28
|
+
|
29
|
+
return if message_version <= ScheduledMessages::SCHEMA_VERSION
|
30
|
+
|
31
|
+
raise Errors::IncompatibleSchemaError, message_version
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,55 @@
|
|
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
|
+
# Serializers used to build payloads (if applicable) for dispatch
|
18
|
+
# @note We only deal with states payload. Other payloads are not ours but end users.
|
19
|
+
class Serializer
|
20
|
+
include ::Karafka::Core::Helpers::Time
|
21
|
+
|
22
|
+
# @param tracker [Tracker] tracker based on which we build the state
|
23
|
+
# @return [String] compressed payload with the state details
|
24
|
+
def state(tracker)
|
25
|
+
data = {
|
26
|
+
schema_version: ScheduledMessages::STATES_SCHEMA_VERSION,
|
27
|
+
dispatched_at: float_now,
|
28
|
+
state: tracker.state,
|
29
|
+
daily: tracker.daily
|
30
|
+
}
|
31
|
+
|
32
|
+
compress(
|
33
|
+
serialize(data)
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# @param hash [Hash] hash to cast to json
|
40
|
+
# @return [String] json hash
|
41
|
+
def serialize(hash)
|
42
|
+
hash.to_json
|
43
|
+
end
|
44
|
+
|
45
|
+
# Compresses the provided data
|
46
|
+
#
|
47
|
+
# @param data [String] data to compress
|
48
|
+
# @return [String] compressed data
|
49
|
+
def compress(data)
|
50
|
+
Zlib::Deflate.deflate(data)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|