karafka 2.4.9 → 2.4.10
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.
- 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,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
|
@@ -0,0 +1,60 @@
|
|
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
|
+
# Setup and config related recurring tasks components
|
18
|
+
module Setup
|
19
|
+
# Config for recurring tasks
|
20
|
+
class Config
|
21
|
+
extend ::Karafka::Core::Configurable
|
22
|
+
|
23
|
+
setting(:consumer_class, default: Consumer)
|
24
|
+
setting(:group_id, default: 'karafka_scheduled_messages')
|
25
|
+
|
26
|
+
# By default we will run the scheduling every 15 seconds since we provide a minute-based
|
27
|
+
# precision. Can be increased when having dedicated processes to run this. Lower values
|
28
|
+
# mean more frequent execution on low-throughput topics meaning higher precision.
|
29
|
+
setting(:interval, default: 15_000)
|
30
|
+
|
31
|
+
# How many messages should be flush in one go from the dispatcher at most. If we have
|
32
|
+
# more messages to dispatch, they will be chunked.
|
33
|
+
setting(:flush_batch_size, default: 1_000)
|
34
|
+
|
35
|
+
# Producer to use. By default uses default Karafka producer.
|
36
|
+
setting(
|
37
|
+
:producer,
|
38
|
+
constructor: -> { ::Karafka.producer },
|
39
|
+
lazy: true
|
40
|
+
)
|
41
|
+
|
42
|
+
# Class we use to dispatch messages
|
43
|
+
setting(:dispatcher_class, default: Dispatcher)
|
44
|
+
|
45
|
+
# Postfix for the states topic to build the states based on the group name + postfix
|
46
|
+
setting(:states_postfix, default: '_states')
|
47
|
+
|
48
|
+
setting(:deserializers) do
|
49
|
+
# Deserializer for schedules messages to convert epochs
|
50
|
+
setting(:headers, default: Deserializers::Headers.new)
|
51
|
+
# Only applicable to states
|
52
|
+
setting(:payload, default: Deserializers::Payload.new)
|
53
|
+
end
|
54
|
+
|
55
|
+
configure
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,62 @@
|
|
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
|
+
# Represents the loading/bootstrapping state of the given topic partition
|
18
|
+
#
|
19
|
+
# Bootstrapping can be in the following states:
|
20
|
+
# - fresh - when we got an assignment but we did not load the schedule yet
|
21
|
+
# - loading - when we are in the process of bootstrapping the daily state and we consume
|
22
|
+
# historical messages to build the needed schedules.
|
23
|
+
# - loaded - state in which we finished loading all the schedules and we can dispatch
|
24
|
+
# messages when the time comes and we can process real-time incoming schedules and
|
25
|
+
# changes to schedules as they appear in the stream.
|
26
|
+
class State
|
27
|
+
# @param loaded [nil, false, true] is the state loaded or not yet. `nil` indicates, it is
|
28
|
+
# a fresh, pre-seek state.
|
29
|
+
def initialize(loaded = nil)
|
30
|
+
@loaded = loaded
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [Boolean] are we in a fresh, pre-bootstrap state
|
34
|
+
def fresh?
|
35
|
+
@loaded.nil?
|
36
|
+
end
|
37
|
+
|
38
|
+
# Marks the current state as fully loaded
|
39
|
+
def loaded!
|
40
|
+
@loaded = true
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Boolean] are we in a loaded state
|
44
|
+
def loaded?
|
45
|
+
@loaded == true
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [String] current state string representation
|
49
|
+
def to_s
|
50
|
+
case @loaded
|
51
|
+
when nil
|
52
|
+
'fresh'
|
53
|
+
when false
|
54
|
+
'loading'
|
55
|
+
when true
|
56
|
+
'loaded'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,64 @@
|
|
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
|
+
# Tracks basic state and metrics about schedules to be dispatched
|
18
|
+
#
|
19
|
+
# It provides accurate today dispatch taken from daily buffer and estimates for future days
|
20
|
+
class Tracker
|
21
|
+
# @return [Hash<String, Integer>]
|
22
|
+
attr_reader :daily
|
23
|
+
|
24
|
+
# @return [String] current state
|
25
|
+
attr_accessor :state
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
@daily = Hash.new { |h, k| h[k] = 0 }
|
29
|
+
@created_at = Time.now.to_i
|
30
|
+
end
|
31
|
+
|
32
|
+
# Accurate (because coming from daily buffer) number of things to schedule
|
33
|
+
#
|
34
|
+
# @param sum [Integer]
|
35
|
+
def today=(sum)
|
36
|
+
@daily[epoch_to_date(@created_at)] = sum
|
37
|
+
end
|
38
|
+
|
39
|
+
# Tracks message dispatch
|
40
|
+
#
|
41
|
+
# It is only relevant for future days as for today we use accurate metrics from the daily
|
42
|
+
# buffer
|
43
|
+
#
|
44
|
+
# @param message [Karafka::Messages::Message] schedule message. Should **not** be a
|
45
|
+
# tombstone message. Tombstone messages cancellations are not tracked because it would
|
46
|
+
# drastically increase complexity. For given day we use the accurate counter and for
|
47
|
+
# future days we use estimates.
|
48
|
+
def track(message)
|
49
|
+
epoch = message.headers['schedule_target_epoch']
|
50
|
+
|
51
|
+
@daily[epoch_to_date(epoch)] += 1
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
# @param epoch [Integer] epoch time
|
57
|
+
# @return [String] epoch matching date
|
58
|
+
def epoch_to_date(epoch)
|
59
|
+
Time.at(epoch).utc.to_date.to_s
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,67 @@
|
|
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
|
+
# This feature allows for proxying messages via a special topic that can dispatch them
|
17
|
+
# at a later time, hence scheduled messages. Such messages need to have a special format
|
18
|
+
# but aside from that they are regular Kafka messages.
|
19
|
+
#
|
20
|
+
# This work was conceptually inspired by the Go scheduler:
|
21
|
+
# https://github.com/etf1/kafka-message-scheduler though I did not look at the implementation
|
22
|
+
# itself. Just the concept of daily in-memory scheduling.
|
23
|
+
module ScheduledMessages
|
24
|
+
# Version of the schema we use for envelops in scheduled messages.
|
25
|
+
# We use it to detect any potential upgrades similar to other components of Karafka and to
|
26
|
+
# stop processing of incompatible versions
|
27
|
+
SCHEMA_VERSION = '1.0.0'
|
28
|
+
|
29
|
+
# Version of the states schema. Used to publish per partition simple aggregated metrics
|
30
|
+
# that can be used for schedules reporting
|
31
|
+
STATES_SCHEMA_VERSION = '1.0.0'
|
32
|
+
|
33
|
+
class << self
|
34
|
+
# Runs the `Proxy.call`
|
35
|
+
# @param kwargs [Hash] things requested by the proxy
|
36
|
+
# @return [Hash] message wrapped with the scheduled message envelope
|
37
|
+
def schedule(**kwargs)
|
38
|
+
Proxy.schedule(**kwargs)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Generates a tombstone message to cancel given dispatch (if not yet happened)
|
42
|
+
# @param kwargs [Hash] things requested by the proxy
|
43
|
+
# @return [Hash] tombstone cancelling message
|
44
|
+
def cancel(**kwargs)
|
45
|
+
Proxy.cancel(**kwargs)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Below are private APIs
|
49
|
+
|
50
|
+
# Sets up additional config scope, validations and other things
|
51
|
+
#
|
52
|
+
# @param config [Karafka::Core::Configurable::Node] root node config
|
53
|
+
def pre_setup(config)
|
54
|
+
# Expand the config with this feature specific stuff
|
55
|
+
config.instance_eval do
|
56
|
+
setting(:scheduled_messages, default: Setup::Config.config)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# @param config [Karafka::Core::Configurable::Node] root node config
|
61
|
+
def post_setup(config)
|
62
|
+
RecurringTasks::Contracts::Config.new.validate!(config.to_h)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -181,6 +181,12 @@ module Karafka
|
|
181
181
|
# overhead as this will happen only once per consumer lifetime
|
182
182
|
consumer.messages = empty_messages
|
183
183
|
|
184
|
+
# Run the post-initialization hook for users that need to run some actions when consumer
|
185
|
+
# is built and ready (all basic state and info).
|
186
|
+
# Users should **not** overwrite the `#initialize` because it won't have all the needed
|
187
|
+
# data assigned yet.
|
188
|
+
consumer.on_initialized
|
189
|
+
|
184
190
|
consumer
|
185
191
|
end
|
186
192
|
end
|
@@ -31,6 +31,16 @@ module Karafka
|
|
31
31
|
RUBY
|
32
32
|
end
|
33
33
|
|
34
|
+
# Runs the post-creation, post-assignment code
|
35
|
+
# @note It runs in the listener loop. Should **not** be used for anything heavy or
|
36
|
+
# with any potential errors. Mostly for initialization of states, etc.
|
37
|
+
def handle_initialized
|
38
|
+
Karafka.monitor.instrument('consumer.initialize', caller: self)
|
39
|
+
Karafka.monitor.instrument('consumer.initialized', caller: self) do
|
40
|
+
initialized
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
34
44
|
# Marks message as consumed in an async way.
|
35
45
|
#
|
36
46
|
# @param message [Messages::Message] last successfully processed message.
|
data/lib/karafka/railtie.rb
CHANGED
@@ -65,26 +65,6 @@ if Karafka.rails?
|
|
65
65
|
app.config.autoload_paths += %w[app/consumers]
|
66
66
|
end
|
67
67
|
|
68
|
-
initializer 'karafka.release_active_record_connections' do
|
69
|
-
rails7plus = Rails.gem_version >= Gem::Version.new('7.0.0')
|
70
|
-
|
71
|
-
ActiveSupport.on_load(:active_record) do
|
72
|
-
::Karafka::App.monitor.subscribe('worker.completed') do
|
73
|
-
# Always release the connection after processing is done. Otherwise thread may hang
|
74
|
-
# blocking the reload and further processing
|
75
|
-
# @see https://github.com/rails/rails/issues/44183
|
76
|
-
#
|
77
|
-
# The change technically happens in 7.1 but 7.0 already supports this so we can make
|
78
|
-
# a proper change for 7.0+
|
79
|
-
if rails7plus
|
80
|
-
ActiveRecord::Base.connection_handler.clear_active_connections!
|
81
|
-
else
|
82
|
-
ActiveRecord::Base.clear_active_connections!
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
68
|
initializer 'karafka.require_karafka_boot_file' do |app|
|
89
69
|
rails6plus = Rails.gem_version >= Gem::Version.new('6.0.0')
|
90
70
|
|
data/lib/karafka/version.rb
CHANGED
data.tar.gz.sig
CHANGED
Binary file
|