karafka 2.0.0 → 2.1.0
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/.github/FUNDING.yml +1 -0
- data/.github/workflows/ci.yml +40 -16
- data/.rspec +4 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +459 -796
- data/Gemfile +3 -2
- data/Gemfile.lock +44 -32
- data/LICENSE +1 -1
- data/README.md +22 -17
- data/bin/integrations +86 -26
- data/bin/karafka +1 -13
- data/bin/rspecs +6 -0
- data/bin/verify_license_integrity +37 -0
- data/certs/cert_chain.pem +26 -0
- data/config/{errors.yml → locales/errors.yml} +34 -6
- data/config/locales/pro_errors.yml +34 -0
- data/docker-compose.yml +4 -30
- data/karafka.gemspec +17 -10
- data/lib/active_job/karafka.rb +3 -7
- data/lib/active_job/queue_adapters/karafka_adapter.rb +7 -1
- data/lib/karafka/active_job/consumer.rb +29 -6
- data/lib/karafka/active_job/current_attributes/loading.rb +36 -0
- data/lib/karafka/active_job/current_attributes/persistence.rb +28 -0
- data/lib/karafka/active_job/current_attributes.rb +42 -0
- data/lib/karafka/active_job/dispatcher.rb +34 -3
- data/lib/karafka/active_job/job_options_contract.rb +13 -2
- data/lib/karafka/admin.rb +246 -0
- data/lib/karafka/app.rb +30 -7
- data/lib/karafka/base_consumer.rb +124 -82
- data/lib/karafka/cli/base.rb +20 -0
- data/lib/karafka/cli/console.rb +13 -8
- data/lib/karafka/cli/info.rb +3 -2
- data/lib/karafka/cli/server.rb +55 -8
- data/lib/karafka/cli/topics.rb +146 -0
- data/lib/karafka/connection/client.rb +118 -61
- data/lib/karafka/connection/consumer_group_coordinator.rb +48 -0
- data/lib/karafka/connection/listener.rb +78 -30
- data/lib/karafka/connection/listeners_batch.rb +18 -2
- data/lib/karafka/connection/rebalance_manager.rb +3 -0
- data/lib/karafka/contracts/config.rb +1 -1
- data/lib/karafka/contracts/consumer_group.rb +22 -9
- data/lib/karafka/contracts/server_cli_options.rb +71 -11
- data/lib/karafka/contracts/topic.rb +65 -0
- data/lib/karafka/contracts.rb +1 -1
- data/lib/karafka/embedded.rb +36 -0
- data/lib/karafka/errors.rb +6 -4
- data/lib/karafka/helpers/colorize.rb +6 -0
- data/lib/karafka/instrumentation/callbacks/error.rb +3 -4
- data/lib/karafka/instrumentation/callbacks/statistics.rb +14 -4
- data/lib/karafka/instrumentation/logger_listener.rb +151 -22
- data/lib/karafka/instrumentation/notifications.rb +26 -7
- data/lib/karafka/instrumentation/proctitle_listener.rb +7 -16
- data/lib/karafka/instrumentation/vendors/datadog/dashboard.json +1 -1
- data/lib/karafka/instrumentation/vendors/datadog/logger_listener.rb +153 -0
- data/lib/karafka/instrumentation/vendors/datadog/{listener.rb → metrics_listener.rb} +62 -35
- data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +166 -0
- data/lib/karafka/licenser.rb +53 -50
- data/lib/karafka/messages/builders/batch_metadata.rb +8 -8
- data/lib/karafka/messages/builders/messages.rb +6 -2
- data/lib/karafka/messages/message.rb +14 -2
- data/lib/karafka/messages/messages.rb +5 -0
- data/lib/karafka/messages/parser.rb +14 -0
- data/lib/karafka/patches/rdkafka/bindings.rb +132 -0
- data/lib/karafka/pro/active_job/consumer.rb +10 -9
- data/lib/karafka/pro/active_job/dispatcher.rb +28 -3
- data/lib/karafka/pro/active_job/job_options_contract.rb +16 -3
- data/lib/karafka/pro/encryption/cipher.rb +58 -0
- data/lib/karafka/pro/encryption/contracts/config.rb +79 -0
- data/lib/karafka/pro/encryption/errors.rb +24 -0
- data/lib/karafka/pro/encryption/messages/middleware.rb +46 -0
- data/lib/karafka/pro/encryption/messages/parser.rb +56 -0
- data/lib/karafka/pro/encryption/setup/config.rb +48 -0
- data/lib/karafka/pro/encryption.rb +47 -0
- data/lib/karafka/pro/iterator.rb +253 -0
- data/lib/karafka/pro/loader.rb +52 -26
- data/lib/karafka/pro/performance_tracker.rb +10 -6
- data/lib/karafka/pro/processing/collapser.rb +62 -0
- data/lib/karafka/pro/processing/coordinator.rb +92 -16
- data/lib/karafka/pro/processing/filters/base.rb +61 -0
- data/lib/karafka/pro/processing/filters/delayer.rb +70 -0
- data/lib/karafka/pro/processing/filters/expirer.rb +51 -0
- data/lib/karafka/pro/processing/filters/throttler.rb +84 -0
- data/lib/karafka/pro/processing/filters/virtual_limiter.rb +52 -0
- data/lib/karafka/pro/processing/filters_applier.rb +104 -0
- data/lib/karafka/pro/processing/jobs/consume_non_blocking.rb +7 -5
- data/lib/karafka/pro/processing/jobs/revoked_non_blocking.rb +37 -0
- data/lib/karafka/pro/processing/jobs_builder.rb +22 -4
- data/lib/karafka/pro/processing/partitioner.rb +33 -24
- data/lib/karafka/pro/processing/scheduler.rb +27 -8
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom.rb +68 -0
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom_vp.rb +74 -0
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom.rb +72 -0
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom_vp.rb +76 -0
- data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom.rb +64 -0
- data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom_vp.rb +68 -0
- data/lib/karafka/pro/processing/strategies/aj/dlq_mom.rb +64 -0
- data/lib/karafka/pro/processing/strategies/aj/dlq_mom_vp.rb +69 -0
- data/lib/karafka/pro/processing/strategies/aj/ftr_lrj_mom.rb +38 -0
- data/lib/karafka/pro/processing/strategies/aj/ftr_lrj_mom_vp.rb +64 -0
- data/lib/karafka/pro/processing/strategies/aj/ftr_mom.rb +38 -0
- data/lib/karafka/pro/processing/strategies/aj/ftr_mom_vp.rb +58 -0
- data/lib/karafka/pro/processing/strategies/aj/lrj_mom.rb +37 -0
- data/lib/karafka/pro/processing/strategies/aj/lrj_mom_vp.rb +79 -0
- data/lib/karafka/pro/processing/strategies/aj/mom.rb +36 -0
- data/lib/karafka/pro/processing/strategies/aj/mom_vp.rb +52 -0
- data/lib/karafka/pro/processing/strategies/base.rb +26 -0
- data/lib/karafka/pro/processing/strategies/default.rb +105 -0
- data/lib/karafka/pro/processing/strategies/dlq/default.rb +131 -0
- data/lib/karafka/pro/processing/strategies/dlq/ftr.rb +61 -0
- data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj.rb +75 -0
- data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj_mom.rb +71 -0
- data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj_mom_vp.rb +43 -0
- data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj_vp.rb +41 -0
- data/lib/karafka/pro/processing/strategies/dlq/ftr_mom.rb +69 -0
- data/lib/karafka/pro/processing/strategies/dlq/ftr_mom_vp.rb +41 -0
- data/lib/karafka/pro/processing/strategies/dlq/ftr_vp.rb +40 -0
- data/lib/karafka/pro/processing/strategies/dlq/lrj.rb +63 -0
- data/lib/karafka/pro/processing/strategies/dlq/lrj_mom.rb +63 -0
- data/lib/karafka/pro/processing/strategies/dlq/lrj_mom_vp.rb +36 -0
- data/lib/karafka/pro/processing/strategies/dlq/lrj_vp.rb +39 -0
- data/lib/karafka/pro/processing/strategies/dlq/mom.rb +68 -0
- data/lib/karafka/pro/processing/strategies/dlq/mom_vp.rb +37 -0
- data/lib/karafka/pro/processing/strategies/dlq/vp.rb +40 -0
- data/lib/karafka/pro/processing/strategies/ftr/default.rb +104 -0
- data/lib/karafka/pro/processing/strategies/ftr/vp.rb +40 -0
- data/lib/karafka/pro/processing/strategies/lrj/default.rb +87 -0
- data/lib/karafka/pro/processing/strategies/lrj/ftr.rb +69 -0
- data/lib/karafka/pro/processing/strategies/lrj/ftr_mom.rb +67 -0
- data/lib/karafka/pro/processing/strategies/lrj/ftr_mom_vp.rb +40 -0
- data/lib/karafka/pro/processing/strategies/lrj/ftr_vp.rb +39 -0
- data/lib/karafka/pro/processing/strategies/lrj/mom.rb +80 -0
- data/lib/karafka/pro/processing/strategies/lrj/mom_vp.rb +38 -0
- data/lib/karafka/pro/processing/strategies/lrj/vp.rb +36 -0
- data/lib/karafka/pro/processing/strategies/mom/default.rb +46 -0
- data/lib/karafka/pro/processing/strategies/mom/ftr.rb +53 -0
- data/lib/karafka/pro/processing/strategies/mom/ftr_vp.rb +37 -0
- data/lib/karafka/pro/processing/strategies/mom/vp.rb +35 -0
- data/lib/karafka/pro/processing/strategies/vp/default.rb +104 -0
- data/lib/karafka/pro/{contracts/base.rb → processing/strategies.rb} +6 -5
- data/lib/karafka/pro/processing/strategy_selector.rb +84 -0
- data/lib/karafka/pro/processing/virtual_offset_manager.rb +147 -0
- data/lib/karafka/pro/routing/features/base.rb +24 -0
- data/lib/karafka/pro/routing/features/dead_letter_queue/contract.rb +50 -0
- data/lib/karafka/pro/routing/features/dead_letter_queue.rb +27 -0
- data/lib/karafka/pro/routing/{builder_extensions.rb → features/delaying/config.rb} +8 -11
- data/lib/karafka/pro/routing/features/delaying/contract.rb +38 -0
- data/lib/karafka/pro/routing/features/delaying/topic.rb +59 -0
- data/lib/karafka/pro/routing/features/delaying.rb +29 -0
- data/lib/karafka/pro/routing/features/expiring/config.rb +27 -0
- data/lib/karafka/pro/routing/features/expiring/contract.rb +38 -0
- data/lib/karafka/pro/routing/features/expiring/topic.rb +59 -0
- data/lib/karafka/pro/routing/features/expiring.rb +27 -0
- data/lib/karafka/pro/routing/features/filtering/config.rb +40 -0
- data/lib/karafka/pro/routing/features/filtering/contract.rb +41 -0
- data/lib/karafka/pro/routing/features/filtering/topic.rb +51 -0
- data/lib/karafka/pro/routing/features/filtering.rb +27 -0
- data/lib/karafka/pro/routing/features/long_running_job/config.rb +28 -0
- data/lib/karafka/pro/routing/features/long_running_job/contract.rb +37 -0
- data/lib/karafka/pro/routing/features/long_running_job/topic.rb +42 -0
- data/lib/karafka/pro/routing/features/long_running_job.rb +28 -0
- data/lib/karafka/pro/routing/features/throttling/config.rb +32 -0
- data/lib/karafka/pro/routing/features/throttling/contract.rb +41 -0
- data/lib/karafka/pro/routing/features/throttling/topic.rb +69 -0
- data/lib/karafka/pro/routing/features/throttling.rb +30 -0
- data/lib/karafka/pro/routing/features/virtual_partitions/config.rb +30 -0
- data/lib/karafka/pro/routing/features/virtual_partitions/contract.rb +52 -0
- data/lib/karafka/pro/routing/features/virtual_partitions/topic.rb +56 -0
- data/lib/karafka/pro/routing/features/virtual_partitions.rb +27 -0
- data/lib/karafka/process.rb +23 -8
- data/lib/karafka/processing/coordinator.rb +102 -23
- data/lib/karafka/processing/coordinators_buffer.rb +5 -1
- data/lib/karafka/processing/executor.rb +51 -21
- data/lib/karafka/processing/executors_buffer.rb +10 -26
- data/lib/karafka/processing/jobs/base.rb +6 -2
- data/lib/karafka/processing/jobs/consume.rb +8 -5
- data/lib/karafka/processing/jobs/idle.rb +24 -0
- data/lib/karafka/processing/jobs_builder.rb +2 -3
- data/lib/karafka/processing/jobs_queue.rb +14 -8
- data/lib/karafka/processing/partitioner.rb +3 -1
- data/lib/karafka/processing/result.rb +14 -1
- data/lib/karafka/processing/strategies/aj_dlq_mom.rb +44 -0
- data/lib/karafka/processing/strategies/aj_mom.rb +21 -0
- data/lib/karafka/processing/strategies/base.rb +52 -0
- data/lib/karafka/processing/strategies/default.rb +142 -0
- data/lib/karafka/processing/strategies/dlq.rb +88 -0
- data/lib/karafka/processing/strategies/dlq_mom.rb +49 -0
- data/lib/karafka/processing/strategies/mom.rb +29 -0
- data/lib/karafka/processing/strategy_selector.rb +47 -0
- data/lib/karafka/processing/worker.rb +8 -3
- data/lib/karafka/railtie.rb +24 -12
- data/lib/karafka/routing/activity_manager.rb +84 -0
- data/lib/karafka/routing/builder.rb +20 -0
- data/lib/karafka/routing/consumer_group.rb +38 -4
- data/lib/karafka/routing/consumer_mapper.rb +0 -10
- data/lib/karafka/routing/features/active_job/builder.rb +33 -0
- data/lib/karafka/routing/features/active_job/config.rb +15 -0
- data/lib/karafka/routing/features/active_job/contract.rb +41 -0
- data/lib/karafka/routing/features/active_job/topic.rb +33 -0
- data/lib/karafka/routing/features/active_job.rb +13 -0
- data/lib/karafka/routing/features/base/expander.rb +53 -0
- data/lib/karafka/routing/features/base.rb +34 -0
- data/lib/karafka/routing/features/dead_letter_queue/config.rb +19 -0
- data/lib/karafka/routing/features/dead_letter_queue/contract.rb +42 -0
- data/lib/karafka/routing/features/dead_letter_queue/topic.rb +41 -0
- data/lib/karafka/routing/features/dead_letter_queue.rb +16 -0
- data/lib/karafka/routing/features/declaratives/config.rb +18 -0
- data/lib/karafka/routing/features/declaratives/contract.rb +30 -0
- data/lib/karafka/routing/features/declaratives/topic.rb +44 -0
- data/lib/karafka/routing/features/declaratives.rb +14 -0
- data/lib/karafka/routing/features/manual_offset_management/config.rb +15 -0
- data/lib/karafka/routing/features/manual_offset_management/contract.rb +24 -0
- data/lib/karafka/routing/features/manual_offset_management/topic.rb +35 -0
- data/lib/karafka/routing/features/manual_offset_management.rb +18 -0
- data/lib/karafka/routing/proxy.rb +18 -20
- data/lib/karafka/routing/router.rb +27 -2
- data/lib/karafka/routing/subscription_group.rb +43 -5
- data/lib/karafka/routing/subscription_groups_builder.rb +6 -1
- data/lib/karafka/routing/topic.rb +17 -5
- data/lib/karafka/routing/topics.rb +8 -0
- data/lib/karafka/runner.rb +1 -0
- data/lib/karafka/serialization/json/deserializer.rb +1 -0
- data/lib/karafka/server.rb +58 -28
- data/lib/karafka/setup/attributes_map.rb +344 -0
- data/lib/karafka/setup/config.rb +48 -19
- data/lib/karafka/status.rb +35 -7
- data/lib/karafka/templates/karafka.rb.erb +14 -6
- data/lib/karafka/time_trackers/base.rb +1 -6
- data/lib/karafka/time_trackers/pause.rb +49 -19
- data/lib/karafka/time_trackers/poll.rb +7 -3
- data/lib/karafka/version.rb +1 -1
- data/lib/karafka.rb +56 -3
- data/renovate.json +6 -0
- data.tar.gz.sig +0 -0
- metadata +189 -61
- metadata.gz.sig +0 -0
- data/bin/wait_for_kafka +0 -20
- data/certs/mensfeld.pem +0 -25
- data/lib/karafka/active_job/routing/extensions.rb +0 -31
- data/lib/karafka/contracts/consumer_group_topic.rb +0 -42
- data/lib/karafka/instrumentation.rb +0 -21
- data/lib/karafka/pro/base_consumer.rb +0 -82
- data/lib/karafka/pro/contracts/consumer_group.rb +0 -34
- data/lib/karafka/pro/contracts/consumer_group_topic.rb +0 -33
- data/lib/karafka/pro/routing/topic_extensions.rb +0 -38
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
module Processing
|
|
5
|
+
# Our processing patterns differ depending on various features configurations
|
|
6
|
+
# In this namespace we collect strategies for particular feature combinations to simplify the
|
|
7
|
+
# design. Based on features combinations we can then select handling strategy for a given case.
|
|
8
|
+
#
|
|
9
|
+
# @note The lack of common code here is intentional. It would get complex if there would be
|
|
10
|
+
# any type of composition, so each strategy is expected to be self-sufficient
|
|
11
|
+
module Strategies
|
|
12
|
+
# Base strategy that should be included in each strategy, just to ensure the API
|
|
13
|
+
module Base
|
|
14
|
+
# What should happen before jobs are enqueued
|
|
15
|
+
# @note This runs from the listener thread, not recommended to put anything slow here
|
|
16
|
+
def handle_before_enqueue
|
|
17
|
+
raise NotImplementedError, 'Implement in a subclass'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# What should happen before we kick in the processing
|
|
21
|
+
def handle_before_consume
|
|
22
|
+
raise NotImplementedError, 'Implement in a subclass'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# What should happen in the processing
|
|
26
|
+
def handle_consume
|
|
27
|
+
raise NotImplementedError, 'Implement in a subclass'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Post-consumption handling
|
|
31
|
+
def handle_after_consume
|
|
32
|
+
raise NotImplementedError, 'Implement in a subclass'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Idle run handling
|
|
36
|
+
def handle_idle
|
|
37
|
+
raise NotImplementedError, 'Implement in a subclass'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Revocation handling
|
|
41
|
+
def handle_revoked
|
|
42
|
+
raise NotImplementedError, 'Implement in a subclass'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Shutdown handling
|
|
46
|
+
def handle_shutdown
|
|
47
|
+
raise NotImplementedError, 'Implement in a subclass'
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
module Processing
|
|
5
|
+
module Strategies
|
|
6
|
+
# No features enabled.
|
|
7
|
+
# No manual offset management
|
|
8
|
+
# No long running jobs
|
|
9
|
+
# Nothing. Just standard, automatic flow
|
|
10
|
+
module Default
|
|
11
|
+
include Base
|
|
12
|
+
|
|
13
|
+
# Apply strategy for a non-feature based flow
|
|
14
|
+
FEATURES = %i[].freeze
|
|
15
|
+
|
|
16
|
+
# Marks message as consumed in an async way.
|
|
17
|
+
#
|
|
18
|
+
# @param message [Messages::Message] last successfully processed message.
|
|
19
|
+
# @return [Boolean] true if we were able to mark the offset, false otherwise.
|
|
20
|
+
# False indicates that we were not able and that we have lost the partition.
|
|
21
|
+
#
|
|
22
|
+
# @note We keep track of this offset in case we would mark as consumed and got error when
|
|
23
|
+
# processing another message. In case like this we do not pause on the message we've
|
|
24
|
+
# already processed but rather at the next one. This applies to both sync and async
|
|
25
|
+
# versions of this method.
|
|
26
|
+
def mark_as_consumed(message)
|
|
27
|
+
# Ignore earlier offsets than the one we already committed
|
|
28
|
+
return true if coordinator.seek_offset > message.offset
|
|
29
|
+
return false if revoked?
|
|
30
|
+
|
|
31
|
+
unless client.mark_as_consumed(message)
|
|
32
|
+
coordinator.revoke
|
|
33
|
+
|
|
34
|
+
return false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
coordinator.seek_offset = message.offset + 1
|
|
38
|
+
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Marks message as consumed in a sync way.
|
|
43
|
+
#
|
|
44
|
+
# @param message [Messages::Message] last successfully processed message.
|
|
45
|
+
# @return [Boolean] true if we were able to mark the offset, false otherwise.
|
|
46
|
+
# False indicates that we were not able and that we have lost the partition.
|
|
47
|
+
def mark_as_consumed!(message)
|
|
48
|
+
# Ignore earlier offsets than the one we already committed
|
|
49
|
+
return true if coordinator.seek_offset > message.offset
|
|
50
|
+
return false if revoked?
|
|
51
|
+
|
|
52
|
+
unless client.mark_as_consumed!(message)
|
|
53
|
+
coordinator.revoke
|
|
54
|
+
|
|
55
|
+
return false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
coordinator.seek_offset = message.offset + 1
|
|
59
|
+
|
|
60
|
+
true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# No actions needed for the standard flow here
|
|
64
|
+
def handle_before_enqueue
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Increment number of attempts
|
|
69
|
+
def handle_before_consume
|
|
70
|
+
coordinator.pause_tracker.increment
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Run the user consumption code
|
|
74
|
+
def handle_consume
|
|
75
|
+
Karafka.monitor.instrument('consumer.consume', caller: self)
|
|
76
|
+
Karafka.monitor.instrument('consumer.consumed', caller: self) do
|
|
77
|
+
consume
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Mark job as successful
|
|
81
|
+
coordinator.success!(self)
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
coordinator.failure!(self, e)
|
|
84
|
+
|
|
85
|
+
# Re-raise so reported in the consumer
|
|
86
|
+
raise e
|
|
87
|
+
ensure
|
|
88
|
+
# We need to decrease number of jobs that this coordinator coordinates as it has finished
|
|
89
|
+
coordinator.decrement
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Standard flow marks work as consumed and moves on if everything went ok.
|
|
93
|
+
# If there was a processing error, we will pause and continue from the next message
|
|
94
|
+
# (next that is +1 from the last one that was successfully marked as consumed)
|
|
95
|
+
def handle_after_consume
|
|
96
|
+
return if revoked?
|
|
97
|
+
|
|
98
|
+
if coordinator.success?
|
|
99
|
+
coordinator.pause_tracker.reset
|
|
100
|
+
|
|
101
|
+
# We should not move the offset automatically when the partition was paused
|
|
102
|
+
# If we would not do this upon a revocation during the pause time, a different process
|
|
103
|
+
# would pick not from the place where we paused but from the offset that would be
|
|
104
|
+
# automatically committed here
|
|
105
|
+
return if coordinator.manual_pause?
|
|
106
|
+
|
|
107
|
+
mark_as_consumed(messages.last)
|
|
108
|
+
else
|
|
109
|
+
retry_after_pause
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Code that should run on idle runs without messages available
|
|
114
|
+
def handle_idle
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# We need to always un-pause the processing in case we have lost a given partition.
|
|
119
|
+
# Otherwise the underlying librdkafka would not know we may want to continue processing and
|
|
120
|
+
# the pause could in theory last forever
|
|
121
|
+
def handle_revoked
|
|
122
|
+
resume
|
|
123
|
+
|
|
124
|
+
coordinator.revoke
|
|
125
|
+
|
|
126
|
+
Karafka.monitor.instrument('consumer.revoke', caller: self)
|
|
127
|
+
Karafka.monitor.instrument('consumer.revoked', caller: self) do
|
|
128
|
+
revoked
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Runs the shutdown code
|
|
133
|
+
def handle_shutdown
|
|
134
|
+
Karafka.monitor.instrument('consumer.shutting_down', caller: self)
|
|
135
|
+
Karafka.monitor.instrument('consumer.shutdown', caller: self) do
|
|
136
|
+
shutdown
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
module Processing
|
|
5
|
+
module Strategies
|
|
6
|
+
# When using dead letter queue, processing won't stop after defined number of retries
|
|
7
|
+
# upon encountering non-critical errors but the messages that error will be moved to a
|
|
8
|
+
# separate topic with their payload and metadata, so they can be handled differently.
|
|
9
|
+
module Dlq
|
|
10
|
+
include Default
|
|
11
|
+
|
|
12
|
+
# Apply strategy when only dead letter queue is turned on
|
|
13
|
+
FEATURES = %i[
|
|
14
|
+
dead_letter_queue
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
# When manual offset management is on, we do not mark anything as consumed automatically
|
|
18
|
+
# and we rely on the user to figure things out
|
|
19
|
+
def handle_after_consume
|
|
20
|
+
return if revoked?
|
|
21
|
+
|
|
22
|
+
if coordinator.success?
|
|
23
|
+
coordinator.pause_tracker.reset
|
|
24
|
+
|
|
25
|
+
return if coordinator.manual_pause?
|
|
26
|
+
|
|
27
|
+
mark_as_consumed(messages.last)
|
|
28
|
+
elsif coordinator.pause_tracker.attempt <= topic.dead_letter_queue.max_retries
|
|
29
|
+
retry_after_pause
|
|
30
|
+
# If we've reached number of retries that we could, we need to skip the first message
|
|
31
|
+
# that was not marked as consumed, pause and continue, while also moving this message
|
|
32
|
+
# to the dead topic
|
|
33
|
+
else
|
|
34
|
+
# We reset the pause to indicate we will now consider it as "ok".
|
|
35
|
+
coordinator.pause_tracker.reset
|
|
36
|
+
|
|
37
|
+
skippable_message, = find_skippable_message
|
|
38
|
+
|
|
39
|
+
# Send skippable message to the dql topic
|
|
40
|
+
dispatch_to_dlq(skippable_message)
|
|
41
|
+
|
|
42
|
+
# We mark the broken message as consumed and move on
|
|
43
|
+
mark_as_consumed(skippable_message)
|
|
44
|
+
|
|
45
|
+
return if revoked?
|
|
46
|
+
|
|
47
|
+
# We pause to backoff once just in case.
|
|
48
|
+
pause(coordinator.seek_offset, nil, false)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Finds the message may want to skip (all, starting from first)
|
|
53
|
+
# @private
|
|
54
|
+
# @return [Array<Karafka::Messages::Message, Boolean>] message we may want to skip and
|
|
55
|
+
# information if this message was from marked offset or figured out via mom flow
|
|
56
|
+
def find_skippable_message
|
|
57
|
+
skippable_message = messages.find do |msg|
|
|
58
|
+
coordinator.marked? && msg.offset == coordinator.seek_offset
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# If we don't have the message matching the last comitted offset, it means that
|
|
62
|
+
# user operates with manual offsets and we're beyond the batch in which things
|
|
63
|
+
# broke for the first time. Then we skip the first (as no markings) and we
|
|
64
|
+
# move on one by one.
|
|
65
|
+
skippable_message ? [skippable_message, true] : [messages.first, false]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Moves the broken message into a separate queue defined via the settings
|
|
69
|
+
# @private
|
|
70
|
+
# @param skippable_message [Karafka::Messages::Message] message we are skipping that also
|
|
71
|
+
# should go to the dlq topic
|
|
72
|
+
def dispatch_to_dlq(skippable_message)
|
|
73
|
+
producer.produce_async(
|
|
74
|
+
topic: topic.dead_letter_queue.topic,
|
|
75
|
+
payload: skippable_message.raw_payload
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Notify about dispatch on the events bus
|
|
79
|
+
Karafka.monitor.instrument(
|
|
80
|
+
'dead_letter_queue.dispatched',
|
|
81
|
+
caller: self,
|
|
82
|
+
message: skippable_message
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
module Processing
|
|
5
|
+
module Strategies
|
|
6
|
+
# Same as pure dead letter queue but we do not marked failed message as consumed
|
|
7
|
+
module DlqMom
|
|
8
|
+
include Dlq
|
|
9
|
+
|
|
10
|
+
# Apply strategy when dlq is on with manual offset management
|
|
11
|
+
FEATURES = %i[
|
|
12
|
+
dead_letter_queue
|
|
13
|
+
manual_offset_management
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
# When manual offset management is on, we do not mark anything as consumed automatically
|
|
17
|
+
# and we rely on the user to figure things out
|
|
18
|
+
def handle_after_consume
|
|
19
|
+
return if revoked?
|
|
20
|
+
|
|
21
|
+
if coordinator.success?
|
|
22
|
+
coordinator.pause_tracker.reset
|
|
23
|
+
elsif coordinator.pause_tracker.attempt <= topic.dead_letter_queue.max_retries
|
|
24
|
+
retry_after_pause
|
|
25
|
+
# If we've reached number of retries that we could, we need to skip the first message
|
|
26
|
+
# that was not marked as consumed, pause and continue, while also moving this message
|
|
27
|
+
# to the dead topic
|
|
28
|
+
else
|
|
29
|
+
# We reset the pause to indicate we will now consider it as "ok".
|
|
30
|
+
coordinator.pause_tracker.reset
|
|
31
|
+
|
|
32
|
+
skippable_message, = find_skippable_message
|
|
33
|
+
|
|
34
|
+
dispatch_to_dlq(skippable_message)
|
|
35
|
+
|
|
36
|
+
# Save the next offset we want to go with after moving given message to DLQ
|
|
37
|
+
# Without this, we would not be able to move forward and we would end up
|
|
38
|
+
# in an infinite loop trying to un-pause from the message we've already processed
|
|
39
|
+
# Of course, since it's a MoM a rebalance or kill, will move it back as no
|
|
40
|
+
# offsets are being committed
|
|
41
|
+
coordinator.seek_offset = skippable_message.offset + 1
|
|
42
|
+
|
|
43
|
+
pause(coordinator.seek_offset, nil, false)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
module Processing
|
|
5
|
+
module Strategies
|
|
6
|
+
# When using manual offset management, we do not mark as consumed after successful processing
|
|
7
|
+
module Mom
|
|
8
|
+
include Default
|
|
9
|
+
|
|
10
|
+
# Apply strategy when only manual offset management is turned on
|
|
11
|
+
FEATURES = %i[
|
|
12
|
+
manual_offset_management
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
# When manual offset management is on, we do not mark anything as consumed automatically
|
|
16
|
+
# and we rely on the user to figure things out
|
|
17
|
+
def handle_after_consume
|
|
18
|
+
return if revoked?
|
|
19
|
+
|
|
20
|
+
if coordinator.success?
|
|
21
|
+
coordinator.pause_tracker.reset
|
|
22
|
+
else
|
|
23
|
+
retry_after_pause
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
module Processing
|
|
5
|
+
# Selector of appropriate processing strategy matching topic combinations
|
|
6
|
+
class StrategySelector
|
|
7
|
+
attr_reader :strategies
|
|
8
|
+
|
|
9
|
+
# Features we support in the OSS offering.
|
|
10
|
+
SUPPORTED_FEATURES = %i[
|
|
11
|
+
active_job
|
|
12
|
+
manual_offset_management
|
|
13
|
+
dead_letter_queue
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
# We load them once for performance reasons not to do too many lookups
|
|
18
|
+
@strategies = find_all
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @param topic [Karafka::Routing::Topic] topic with settings based on which we find strategy
|
|
22
|
+
# @return [Module] module with proper strategy
|
|
23
|
+
def find(topic)
|
|
24
|
+
feature_set = SUPPORTED_FEATURES.map do |feature_name|
|
|
25
|
+
topic.public_send("#{feature_name}?") ? feature_name : nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
feature_set.compact!
|
|
29
|
+
|
|
30
|
+
@strategies.find do |strategy|
|
|
31
|
+
strategy::FEATURES.sort == feature_set.sort
|
|
32
|
+
end || raise(Errors::StrategyNotFoundError, topic.name)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# @return [Array<Module>] available strategies
|
|
38
|
+
def find_all
|
|
39
|
+
Strategies
|
|
40
|
+
.constants
|
|
41
|
+
.delete_if { |k| k == :Base }
|
|
42
|
+
.map { |k| Strategies.const_get(k) }
|
|
43
|
+
.uniq
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -25,7 +25,7 @@ module Karafka
|
|
|
25
25
|
# @param jobs_queue [JobsQueue]
|
|
26
26
|
# @return [Worker]
|
|
27
27
|
def initialize(jobs_queue)
|
|
28
|
-
@id = SecureRandom.
|
|
28
|
+
@id = SecureRandom.hex(6)
|
|
29
29
|
@jobs_queue = jobs_queue
|
|
30
30
|
end
|
|
31
31
|
|
|
@@ -46,9 +46,9 @@ module Karafka
|
|
|
46
46
|
def process
|
|
47
47
|
job = @jobs_queue.pop
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
instrument_details = { caller: self, job: job, jobs_queue: @jobs_queue }
|
|
49
|
+
instrument_details = { caller: self, job: job, jobs_queue: @jobs_queue }
|
|
51
50
|
|
|
51
|
+
if job
|
|
52
52
|
Karafka.monitor.instrument('worker.process', instrument_details)
|
|
53
53
|
|
|
54
54
|
Karafka.monitor.instrument('worker.processed', instrument_details) do
|
|
@@ -76,12 +76,17 @@ module Karafka
|
|
|
76
76
|
Karafka.monitor.instrument(
|
|
77
77
|
'error.occurred',
|
|
78
78
|
caller: self,
|
|
79
|
+
job: job,
|
|
80
|
+
jobs_queue: @jobs_queue,
|
|
79
81
|
error: e,
|
|
80
82
|
type: 'worker.process.error'
|
|
81
83
|
)
|
|
82
84
|
ensure
|
|
83
85
|
# job can be nil when the queue is being closed
|
|
84
86
|
@jobs_queue.complete(job) if job
|
|
87
|
+
|
|
88
|
+
# Always publish info, that we completed all the work despite its result
|
|
89
|
+
Karafka.monitor.instrument('worker.completed', instrument_details)
|
|
85
90
|
end
|
|
86
91
|
end
|
|
87
92
|
end
|
data/lib/karafka/railtie.rb
CHANGED
|
@@ -2,13 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# This file contains Railtie for auto-configuration
|
|
4
4
|
|
|
5
|
-
rails
|
|
6
|
-
|
|
7
|
-
begin
|
|
8
|
-
require 'rails'
|
|
9
|
-
|
|
10
|
-
rails = true
|
|
11
|
-
rescue LoadError
|
|
5
|
+
unless Karafka.rails?
|
|
12
6
|
# Without defining this in any way, Zeitwerk ain't happy so we do it that way
|
|
13
7
|
module Karafka
|
|
14
8
|
class Railtie
|
|
@@ -16,10 +10,7 @@ rescue LoadError
|
|
|
16
10
|
end
|
|
17
11
|
end
|
|
18
12
|
|
|
19
|
-
if rails
|
|
20
|
-
# Load Karafka
|
|
21
|
-
require 'karafka'
|
|
22
|
-
|
|
13
|
+
if Karafka.rails?
|
|
23
14
|
# Load ActiveJob adapter
|
|
24
15
|
require 'active_job/karafka'
|
|
25
16
|
|
|
@@ -47,10 +38,17 @@ if rails
|
|
|
47
38
|
|
|
48
39
|
next unless Rails.env.development?
|
|
49
40
|
next unless ENV.key?('KARAFKA_CLI')
|
|
41
|
+
# If we are already publishing to STDOUT, no need to add it again.
|
|
42
|
+
# If added again, would print stuff twice
|
|
43
|
+
next if ActiveSupport::Logger.logger_outputs_to?(Rails.logger, $stdout)
|
|
44
|
+
|
|
45
|
+
logger = ActiveSupport::Logger.new($stdout)
|
|
46
|
+
# Inherit the logger level from Rails, otherwise would always run with the debug level
|
|
47
|
+
logger.level = Rails.logger.level
|
|
50
48
|
|
|
51
49
|
Rails.logger.extend(
|
|
52
50
|
ActiveSupport::Logger.broadcast(
|
|
53
|
-
|
|
51
|
+
logger
|
|
54
52
|
)
|
|
55
53
|
)
|
|
56
54
|
end
|
|
@@ -74,11 +72,25 @@ if rails
|
|
|
74
72
|
::Karafka::App.monitor.subscribe('connection.listener.fetch_loop') do
|
|
75
73
|
# Reload code each time there is a change in the code
|
|
76
74
|
next unless Rails.application.reloaders.any?(&:updated?)
|
|
75
|
+
# If consumer persistence is enabled, no reason to reload because we will still keep
|
|
76
|
+
# old consumer instances in memory.
|
|
77
|
+
next if Karafka::App.config.consumer_persistence
|
|
77
78
|
|
|
78
79
|
Rails.application.reloader.reload!
|
|
79
80
|
end
|
|
80
81
|
end
|
|
81
82
|
|
|
83
|
+
initializer 'karafka.release_active_record_connections' do
|
|
84
|
+
ActiveSupport.on_load(:active_record) do
|
|
85
|
+
::Karafka::App.monitor.subscribe('worker.completed') do
|
|
86
|
+
# Always release the connection after processing is done. Otherwise thread may hang
|
|
87
|
+
# blocking the reload and further processing
|
|
88
|
+
# @see https://github.com/rails/rails/issues/44183
|
|
89
|
+
ActiveRecord::Base.clear_active_connections!
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
82
94
|
initializer 'karafka.require_karafka_boot_file' do |app|
|
|
83
95
|
rails6plus = Rails.gem_version >= Gem::Version.new('6.0.0')
|
|
84
96
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
module Routing
|
|
5
|
+
# Allows us to get track of which consumer groups, subscription groups and topics are enabled
|
|
6
|
+
# or disabled via CLI
|
|
7
|
+
class ActivityManager
|
|
8
|
+
# Supported types of inclusions and exclusions
|
|
9
|
+
SUPPORTED_TYPES = %i[
|
|
10
|
+
consumer_groups
|
|
11
|
+
subscription_groups
|
|
12
|
+
topics
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@included = Hash.new { |h, k| h[k] = [] }
|
|
17
|
+
@excluded = Hash.new { |h, k| h[k] = [] }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Adds resource to included/active
|
|
21
|
+
# @param type [Symbol] type for inclusion
|
|
22
|
+
# @param name [String] name of the element
|
|
23
|
+
def include(type, name)
|
|
24
|
+
validate!(type)
|
|
25
|
+
|
|
26
|
+
@included[type] << name
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Adds resource to excluded
|
|
30
|
+
# @param type [Symbol] type for inclusion
|
|
31
|
+
# @param name [String] name of the element
|
|
32
|
+
def exclude(type, name)
|
|
33
|
+
validate!(type)
|
|
34
|
+
|
|
35
|
+
@excluded[type] << name
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param type [Symbol] type for inclusion
|
|
39
|
+
# @param name [String] name of the element
|
|
40
|
+
# @return [Boolean] is the given resource active or not
|
|
41
|
+
def active?(type, name)
|
|
42
|
+
validate!(type)
|
|
43
|
+
|
|
44
|
+
included = @included[type]
|
|
45
|
+
excluded = @excluded[type]
|
|
46
|
+
|
|
47
|
+
# If nothing defined, all active by default
|
|
48
|
+
return true if included.empty? && excluded.empty?
|
|
49
|
+
# Inclusion supersedes exclusion in case someone wrote both
|
|
50
|
+
return true if !included.empty? && included.include?(name)
|
|
51
|
+
|
|
52
|
+
# If there are exclusions but our is not excluded and no inclusions or included, it's ok
|
|
53
|
+
!excluded.empty? &&
|
|
54
|
+
!excluded.include?(name) &&
|
|
55
|
+
(included.empty? || included.include?(name))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [Hash] accumulated data in a hash for validations
|
|
59
|
+
def to_h
|
|
60
|
+
(
|
|
61
|
+
SUPPORTED_TYPES.map { |type| ["include_#{type}".to_sym, @included[type]] } +
|
|
62
|
+
SUPPORTED_TYPES.map { |type| ["exclude_#{type}".to_sym, @excluded[type]] }
|
|
63
|
+
).to_h
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Clears the manager
|
|
67
|
+
def clear
|
|
68
|
+
@included.clear
|
|
69
|
+
@excluded.clear
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Checks if the type we want to register is supported
|
|
75
|
+
#
|
|
76
|
+
# @param type [Symbol] type for inclusion
|
|
77
|
+
def validate!(type)
|
|
78
|
+
return if SUPPORTED_TYPES.include?(type)
|
|
79
|
+
|
|
80
|
+
raise(::Karafka::Errors::UnsupportedCaseError, type)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -33,7 +33,16 @@ module Karafka
|
|
|
33
33
|
instance_eval(&block)
|
|
34
34
|
|
|
35
35
|
each do |consumer_group|
|
|
36
|
+
# Validate consumer group settings
|
|
36
37
|
Contracts::ConsumerGroup.new.validate!(consumer_group.to_h)
|
|
38
|
+
|
|
39
|
+
# and then its topics settings
|
|
40
|
+
consumer_group.topics.each do |topic|
|
|
41
|
+
Contracts::Topic.new.validate!(topic.to_h)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Initialize subscription groups after all the routing is done
|
|
45
|
+
consumer_group.subscription_groups
|
|
37
46
|
end
|
|
38
47
|
end
|
|
39
48
|
|
|
@@ -66,6 +75,17 @@ module Karafka
|
|
|
66
75
|
end
|
|
67
76
|
end
|
|
68
77
|
|
|
78
|
+
# Handles the simple routing case where we create one consumer group and allow for further
|
|
79
|
+
# subscription group customization
|
|
80
|
+
# @param subscription_group_name [String, Symbol] subscription group id. When not provided,
|
|
81
|
+
# a random uuid will be used
|
|
82
|
+
# @param block [Proc] further topics definitions
|
|
83
|
+
def subscription_group(subscription_group_name = SubscriptionGroup.id, &block)
|
|
84
|
+
consumer_group('app') do
|
|
85
|
+
target.public_send(:subscription_group=, subscription_group_name.to_s, &block)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
69
89
|
# In case we use simple style of routing, all topics will be assigned to the same consumer
|
|
70
90
|
# group that will be based on the client_id
|
|
71
91
|
#
|