karafka 2.4.8 → 2.4.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/workflows/ci.yml +0 -1
  4. data/.ruby-version +1 -1
  5. data/CHANGELOG.md +17 -0
  6. data/Gemfile +8 -5
  7. data/Gemfile.lock +24 -15
  8. data/bin/integrations +5 -0
  9. data/certs/cert.pem +26 -0
  10. data/config/locales/errors.yml +5 -0
  11. data/config/locales/pro_errors.yml +34 -0
  12. data/karafka.gemspec +1 -1
  13. data/lib/karafka/admin.rb +42 -0
  14. data/lib/karafka/base_consumer.rb +23 -0
  15. data/lib/karafka/contracts/config.rb +2 -0
  16. data/lib/karafka/contracts/consumer_group.rb +17 -0
  17. data/lib/karafka/errors.rb +3 -2
  18. data/lib/karafka/instrumentation/logger_listener.rb +3 -0
  19. data/lib/karafka/instrumentation/notifications.rb +3 -0
  20. data/lib/karafka/instrumentation/vendors/appsignal/client.rb +32 -11
  21. data/lib/karafka/instrumentation/vendors/appsignal/errors_listener.rb +1 -1
  22. data/lib/karafka/messages/message.rb +6 -0
  23. data/lib/karafka/pro/loader.rb +3 -1
  24. data/lib/karafka/pro/processing/strategies/dlq/default.rb +16 -1
  25. data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj_mom.rb +5 -1
  26. data/lib/karafka/pro/processing/strategies/dlq/ftr_mom.rb +17 -1
  27. data/lib/karafka/pro/processing/strategies/dlq/lrj_mom.rb +17 -1
  28. data/lib/karafka/pro/processing/strategies/dlq/mom.rb +22 -6
  29. data/lib/karafka/pro/recurring_tasks/consumer.rb +105 -0
  30. data/lib/karafka/pro/recurring_tasks/contracts/config.rb +53 -0
  31. data/lib/karafka/pro/recurring_tasks/contracts/task.rb +41 -0
  32. data/lib/karafka/pro/recurring_tasks/deserializer.rb +35 -0
  33. data/lib/karafka/pro/recurring_tasks/dispatcher.rb +87 -0
  34. data/lib/karafka/pro/recurring_tasks/errors.rb +34 -0
  35. data/lib/karafka/pro/recurring_tasks/executor.rb +152 -0
  36. data/lib/karafka/pro/recurring_tasks/listener.rb +38 -0
  37. data/lib/karafka/pro/recurring_tasks/matcher.rb +38 -0
  38. data/lib/karafka/pro/recurring_tasks/schedule.rb +63 -0
  39. data/lib/karafka/pro/recurring_tasks/serializer.rb +113 -0
  40. data/lib/karafka/pro/recurring_tasks/setup/config.rb +52 -0
  41. data/lib/karafka/pro/recurring_tasks/task.rb +151 -0
  42. data/lib/karafka/pro/recurring_tasks.rb +87 -0
  43. data/lib/karafka/pro/routing/features/recurring_tasks/builder.rb +131 -0
  44. data/lib/karafka/pro/routing/features/recurring_tasks/config.rb +28 -0
  45. data/lib/karafka/pro/routing/features/recurring_tasks/contracts/topic.rb +40 -0
  46. data/lib/karafka/pro/routing/features/recurring_tasks/proxy.rb +27 -0
  47. data/lib/karafka/pro/routing/features/recurring_tasks/topic.rb +44 -0
  48. data/lib/karafka/pro/routing/features/recurring_tasks.rb +25 -0
  49. data/lib/karafka/pro/routing/features/scheduled_messages/builder.rb +131 -0
  50. data/lib/karafka/pro/routing/features/scheduled_messages/config.rb +28 -0
  51. data/lib/karafka/pro/routing/features/scheduled_messages/contracts/topic.rb +40 -0
  52. data/lib/karafka/pro/routing/features/scheduled_messages/proxy.rb +27 -0
  53. data/lib/karafka/pro/routing/features/scheduled_messages/topic.rb +44 -0
  54. data/lib/karafka/pro/routing/features/scheduled_messages.rb +24 -0
  55. data/lib/karafka/pro/scheduled_messages/consumer.rb +185 -0
  56. data/lib/karafka/pro/scheduled_messages/contracts/config.rb +56 -0
  57. data/lib/karafka/pro/scheduled_messages/contracts/message.rb +61 -0
  58. data/lib/karafka/pro/scheduled_messages/daily_buffer.rb +79 -0
  59. data/lib/karafka/pro/scheduled_messages/day.rb +45 -0
  60. data/lib/karafka/pro/scheduled_messages/deserializers/headers.rb +46 -0
  61. data/lib/karafka/pro/scheduled_messages/deserializers/payload.rb +35 -0
  62. data/lib/karafka/pro/scheduled_messages/dispatcher.rb +122 -0
  63. data/lib/karafka/pro/scheduled_messages/errors.rb +28 -0
  64. data/lib/karafka/pro/scheduled_messages/max_epoch.rb +41 -0
  65. data/lib/karafka/pro/scheduled_messages/proxy.rb +176 -0
  66. data/lib/karafka/pro/scheduled_messages/schema_validator.rb +37 -0
  67. data/lib/karafka/pro/scheduled_messages/serializer.rb +55 -0
  68. data/lib/karafka/pro/scheduled_messages/setup/config.rb +60 -0
  69. data/lib/karafka/pro/scheduled_messages/state.rb +62 -0
  70. data/lib/karafka/pro/scheduled_messages/tracker.rb +64 -0
  71. data/lib/karafka/pro/scheduled_messages.rb +67 -0
  72. data/lib/karafka/processing/executor.rb +6 -0
  73. data/lib/karafka/processing/strategies/default.rb +10 -0
  74. data/lib/karafka/processing/strategies/dlq.rb +16 -2
  75. data/lib/karafka/processing/strategies/dlq_mom.rb +25 -6
  76. data/lib/karafka/processing/worker.rb +11 -1
  77. data/lib/karafka/railtie.rb +11 -42
  78. data/lib/karafka/routing/features/dead_letter_queue/config.rb +3 -0
  79. data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +1 -0
  80. data/lib/karafka/routing/features/dead_letter_queue/topic.rb +7 -2
  81. data/lib/karafka/routing/features/eofed/contracts/topic.rb +12 -0
  82. data/lib/karafka/routing/topic.rb +14 -0
  83. data/lib/karafka/setup/config.rb +3 -0
  84. data/lib/karafka/version.rb +1 -1
  85. data.tar.gz.sig +0 -0
  86. metadata +68 -25
  87. metadata.gz.sig +0 -0
  88. 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