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