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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# This Karafka component is a Pro component.
|
|
3
|
+
# This Karafka component is a Pro component under a commercial license.
|
|
4
|
+
# This Karafka component is NOT licensed under LGPL.
|
|
5
|
+
#
|
|
4
6
|
# All of the commercial components are present in the lib/karafka/pro directory of this
|
|
5
7
|
# repository and their usage requires commercial license agreement.
|
|
6
8
|
#
|
|
@@ -16,39 +18,46 @@ module Karafka
|
|
|
16
18
|
class Partitioner < ::Karafka::Processing::Partitioner
|
|
17
19
|
# @param topic [String] topic name
|
|
18
20
|
# @param messages [Array<Karafka::Messages::Message>] karafka messages
|
|
21
|
+
# @param coordinator [Karafka::Pro::Processing::Coordinator] processing coordinator that
|
|
22
|
+
# will be used with those messages
|
|
19
23
|
# @yieldparam [Integer] group id
|
|
20
24
|
# @yieldparam [Array<Karafka::Messages::Message>] karafka messages
|
|
21
|
-
def call(topic, messages)
|
|
25
|
+
def call(topic, messages, coordinator)
|
|
22
26
|
ktopic = @subscription_group.topics.find(topic)
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
# We only partition work if we have:
|
|
29
|
+
# - a virtual partitioner
|
|
30
|
+
# - more than one thread to process the data
|
|
31
|
+
# - collective is not collapsed via coordinator
|
|
32
|
+
#
|
|
33
|
+
# With one thread it is not worth partitioning the work as the work itself will be
|
|
34
|
+
# assigned to one thread (pointless work)
|
|
35
|
+
#
|
|
36
|
+
# We collapse the partitioning on errors because we "regain" full ordering on a batch
|
|
37
|
+
# that potentially contains the data that caused the error.
|
|
38
|
+
#
|
|
39
|
+
# This is great because it allows us to run things without the parallelization that adds
|
|
40
|
+
# a bit of uncertainty and allows us to use DLQ and safely skip messages if needed.
|
|
41
|
+
if ktopic.virtual_partitions? &&
|
|
42
|
+
ktopic.virtual_partitions.max_partitions > 1 &&
|
|
43
|
+
!coordinator.collapsed?
|
|
44
|
+
# We need to reduce it to the max concurrency, so the group_id is not a direct effect
|
|
31
45
|
# of the end user action. Otherwise the persistence layer for consumers would cache
|
|
32
46
|
# it forever and it would cause memory leaks
|
|
33
|
-
groupings = messages
|
|
34
|
-
.group_by { |msg| ktopic.virtual_partitioner.call(msg) }
|
|
35
|
-
.values
|
|
36
|
-
|
|
37
|
-
# Reduce the max concurrency to a size that matches the concurrency
|
|
38
|
-
# As mentioned above we cannot use the partitioning keys directly as it could cause
|
|
39
|
-
# memory leaks
|
|
40
47
|
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
# This also needs to be consistent because the aggregation here needs to warrant, that
|
|
49
|
+
# the same partitioned message will always be assigned to the same virtual partition.
|
|
50
|
+
# Otherwise in case of a window aggregation with VP spanning across several polls, the
|
|
51
|
+
# data could not be complete.
|
|
52
|
+
groupings = messages.group_by do |msg|
|
|
53
|
+
key = ktopic.virtual_partitions.partitioner.call(msg).to_s.sum
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
groupings << (groupings.pop + groupings.pop).sort_by!(&:offset)
|
|
55
|
+
key % ktopic.virtual_partitions.max_partitions
|
|
49
56
|
end
|
|
50
57
|
|
|
51
|
-
groupings.
|
|
58
|
+
groupings.each do |key, messages_group|
|
|
59
|
+
yield(key, messages_group)
|
|
60
|
+
end
|
|
52
61
|
else
|
|
53
62
|
# When no virtual partitioner, works as regular one
|
|
54
63
|
yield(0, messages)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# This Karafka component is a Pro component.
|
|
3
|
+
# This Karafka component is a Pro component under a commercial license.
|
|
4
|
+
# This Karafka component is NOT licensed under LGPL.
|
|
5
|
+
#
|
|
4
6
|
# All of the commercial components are present in the lib/karafka/pro directory of this
|
|
5
7
|
# repository and their usage requires commercial license agreement.
|
|
6
8
|
#
|
|
@@ -29,17 +31,15 @@ module Karafka
|
|
|
29
31
|
# @param jobs_array [Array<Karafka::Processing::Jobs::Base>] jobs we want to schedule
|
|
30
32
|
#
|
|
31
33
|
def schedule_consumption(queue, jobs_array)
|
|
32
|
-
|
|
34
|
+
perf_tracker = PerformanceTracker.instance
|
|
33
35
|
|
|
34
36
|
ordered = []
|
|
35
37
|
|
|
36
38
|
jobs_array.each do |job|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
ordered << [job, cost]
|
|
39
|
+
ordered << [
|
|
40
|
+
job,
|
|
41
|
+
processing_cost(perf_tracker, job)
|
|
42
|
+
]
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
ordered.sort_by!(&:last)
|
|
@@ -50,6 +50,25 @@ module Karafka
|
|
|
50
50
|
queue << job
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# @param perf_tracker [PerformanceTracker]
|
|
57
|
+
# @param job [Karafka::Processing::Jobs::Base] job we will be processing
|
|
58
|
+
# @return [Numeric] estimated cost of processing this job
|
|
59
|
+
def processing_cost(perf_tracker, job)
|
|
60
|
+
if job.is_a?(::Karafka::Processing::Jobs::Consume)
|
|
61
|
+
messages = job.messages
|
|
62
|
+
message = messages.first
|
|
63
|
+
|
|
64
|
+
perf_tracker.processing_time_p95(message.topic, message.partition) * messages.size
|
|
65
|
+
else
|
|
66
|
+
# LJF will set first the most expensive, but we want to run the zero cost jobs
|
|
67
|
+
# related to the lifecycle always first. That is why we "emulate" that they
|
|
68
|
+
# the longest possible jobs that anyone can run
|
|
69
|
+
Float::INFINITY
|
|
70
|
+
end
|
|
71
|
+
end
|
|
53
72
|
end
|
|
54
73
|
end
|
|
55
74
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
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 Processing
|
|
17
|
+
module Strategies
|
|
18
|
+
module Aj
|
|
19
|
+
# ActiveJob enabled
|
|
20
|
+
# DLQ enabled
|
|
21
|
+
# Filtering enabled
|
|
22
|
+
# Long-Running Job enabled
|
|
23
|
+
# Manual offset management enabled
|
|
24
|
+
module DlqFtrLrjMom
|
|
25
|
+
include Strategies::Aj::FtrMom
|
|
26
|
+
include Strategies::Aj::DlqMom
|
|
27
|
+
include Strategies::Aj::LrjMom
|
|
28
|
+
|
|
29
|
+
# Features for this strategy
|
|
30
|
+
FEATURES = %i[
|
|
31
|
+
active_job
|
|
32
|
+
dead_letter_queue
|
|
33
|
+
filtering
|
|
34
|
+
long_running_job
|
|
35
|
+
manual_offset_management
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
# This strategy assumes we do not early break on shutdown as it has VP
|
|
39
|
+
def handle_after_consume
|
|
40
|
+
coordinator.on_finished do
|
|
41
|
+
if coordinator.success?
|
|
42
|
+
coordinator.pause_tracker.reset
|
|
43
|
+
|
|
44
|
+
if coordinator.filtered? && !revoked?
|
|
45
|
+
handle_post_filtering
|
|
46
|
+
elsif !revoked?
|
|
47
|
+
seek(coordinator.seek_offset)
|
|
48
|
+
resume
|
|
49
|
+
else
|
|
50
|
+
resume
|
|
51
|
+
end
|
|
52
|
+
elsif coordinator.pause_tracker.attempt <= topic.dead_letter_queue.max_retries
|
|
53
|
+
retry_after_pause
|
|
54
|
+
else
|
|
55
|
+
coordinator.pause_tracker.reset
|
|
56
|
+
skippable_message, = find_skippable_message
|
|
57
|
+
dispatch_to_dlq(skippable_message) if dispatch_to_dlq?
|
|
58
|
+
mark_as_consumed(skippable_message)
|
|
59
|
+
pause(coordinator.seek_offset, nil, false)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
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 Processing
|
|
17
|
+
module Strategies
|
|
18
|
+
module Aj
|
|
19
|
+
# ActiveJob enabled
|
|
20
|
+
# DLQ enabled
|
|
21
|
+
# Filtering enabled
|
|
22
|
+
# Long-Running Job enabled
|
|
23
|
+
# Manual offset management enabled
|
|
24
|
+
# Virtual Partitions enabled
|
|
25
|
+
module DlqFtrLrjMomVp
|
|
26
|
+
include Strategies::Aj::FtrMom
|
|
27
|
+
include Strategies::Aj::DlqMomVp
|
|
28
|
+
include Strategies::Aj::LrjMom
|
|
29
|
+
|
|
30
|
+
# Features for this strategy
|
|
31
|
+
FEATURES = %i[
|
|
32
|
+
active_job
|
|
33
|
+
dead_letter_queue
|
|
34
|
+
filtering
|
|
35
|
+
long_running_job
|
|
36
|
+
manual_offset_management
|
|
37
|
+
virtual_partitions
|
|
38
|
+
].freeze
|
|
39
|
+
|
|
40
|
+
# This strategy assumes we do not early break on shutdown as it has VP
|
|
41
|
+
def handle_after_consume
|
|
42
|
+
coordinator.on_finished do |last_group_message|
|
|
43
|
+
if coordinator.success?
|
|
44
|
+
coordinator.pause_tracker.reset
|
|
45
|
+
|
|
46
|
+
# Since we have VP here we do not commit intermediate offsets and need to commit
|
|
47
|
+
# them here. We do commit in collapsed mode but this is generalized.
|
|
48
|
+
mark_as_consumed(last_group_message) unless revoked?
|
|
49
|
+
|
|
50
|
+
if coordinator.filtered? && !revoked?
|
|
51
|
+
handle_post_filtering
|
|
52
|
+
elsif !revoked?
|
|
53
|
+
seek(coordinator.seek_offset)
|
|
54
|
+
resume
|
|
55
|
+
else
|
|
56
|
+
resume
|
|
57
|
+
end
|
|
58
|
+
elsif coordinator.pause_tracker.attempt <= topic.dead_letter_queue.max_retries
|
|
59
|
+
retry_after_pause
|
|
60
|
+
else
|
|
61
|
+
coordinator.pause_tracker.reset
|
|
62
|
+
skippable_message, = find_skippable_message
|
|
63
|
+
dispatch_to_dlq(skippable_message) if dispatch_to_dlq?
|
|
64
|
+
mark_as_consumed(skippable_message)
|
|
65
|
+
pause(coordinator.seek_offset, nil, false)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
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 Processing
|
|
17
|
+
module Strategies
|
|
18
|
+
module Aj
|
|
19
|
+
# - Aj
|
|
20
|
+
# - Dlq
|
|
21
|
+
# - Ftr
|
|
22
|
+
# - Mom
|
|
23
|
+
module DlqFtrMom
|
|
24
|
+
include Strategies::Dlq::FtrMom
|
|
25
|
+
|
|
26
|
+
# Features for this strategy
|
|
27
|
+
FEATURES = %i[
|
|
28
|
+
active_job
|
|
29
|
+
dead_letter_queue
|
|
30
|
+
filtering
|
|
31
|
+
manual_offset_management
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
# We write our own custom handler for after consume here, because we mark as consumed
|
|
35
|
+
# per each job in the AJ consumer itself (for this strategy). This means, that for DLQ
|
|
36
|
+
# dispatch, we can mark this message as consumed as well.
|
|
37
|
+
def handle_after_consume
|
|
38
|
+
coordinator.on_finished do
|
|
39
|
+
return if revoked?
|
|
40
|
+
|
|
41
|
+
if coordinator.success?
|
|
42
|
+
coordinator.pause_tracker.reset
|
|
43
|
+
|
|
44
|
+
return if coordinator.manual_pause?
|
|
45
|
+
|
|
46
|
+
handle_post_filtering
|
|
47
|
+
elsif coordinator.pause_tracker.attempt <= topic.dead_letter_queue.max_retries
|
|
48
|
+
retry_after_pause
|
|
49
|
+
# If we've reached number of retries that we could, we need to skip the first
|
|
50
|
+
# message that was not marked as consumed, pause and continue, while also moving
|
|
51
|
+
# this message to the dead topic.
|
|
52
|
+
#
|
|
53
|
+
# For a Mom setup, this means, that user has to manage the checkpointing by
|
|
54
|
+
# himself. If no checkpointing is ever done, we end up with an endless loop.
|
|
55
|
+
else
|
|
56
|
+
coordinator.pause_tracker.reset
|
|
57
|
+
skippable_message, = find_skippable_message
|
|
58
|
+
dispatch_to_dlq(skippable_message) if dispatch_to_dlq?
|
|
59
|
+
# We can commit the offset here because we know that we skip it "forever" and
|
|
60
|
+
# since AJ consumer commits the offset after each job, we also know that the
|
|
61
|
+
# previous job was successful
|
|
62
|
+
mark_as_consumed(skippable_message)
|
|
63
|
+
pause(coordinator.seek_offset, nil, false)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
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 Processing
|
|
17
|
+
module Strategies
|
|
18
|
+
module Aj
|
|
19
|
+
# - Aj
|
|
20
|
+
# - Dlq
|
|
21
|
+
# - Ftr
|
|
22
|
+
# - Mom
|
|
23
|
+
# - VP
|
|
24
|
+
module DlqFtrMomVp
|
|
25
|
+
include Strategies::Aj::DlqMomVp
|
|
26
|
+
include Strategies::Aj::DlqFtrMom
|
|
27
|
+
|
|
28
|
+
# Features for this strategy
|
|
29
|
+
FEATURES = %i[
|
|
30
|
+
active_job
|
|
31
|
+
dead_letter_queue
|
|
32
|
+
filtering
|
|
33
|
+
manual_offset_management
|
|
34
|
+
virtual_partitions
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
# AJ VP does not early stop on shutdown, hence here we can mark as consumed at the
|
|
38
|
+
# end of all VPs
|
|
39
|
+
def handle_after_consume
|
|
40
|
+
coordinator.on_finished do |last_group_message|
|
|
41
|
+
return if revoked?
|
|
42
|
+
|
|
43
|
+
if coordinator.success?
|
|
44
|
+
coordinator.pause_tracker.reset
|
|
45
|
+
|
|
46
|
+
return if coordinator.manual_pause?
|
|
47
|
+
|
|
48
|
+
mark_as_consumed(last_group_message)
|
|
49
|
+
|
|
50
|
+
handle_post_filtering
|
|
51
|
+
elsif coordinator.pause_tracker.attempt <= topic.dead_letter_queue.max_retries
|
|
52
|
+
retry_after_pause
|
|
53
|
+
# If we've reached number of retries that we could, we need to skip the first
|
|
54
|
+
# message that was not marked as consumed, pause and continue, while also moving
|
|
55
|
+
# this message to the dead topic.
|
|
56
|
+
#
|
|
57
|
+
# For a Mom setup, this means, that user has to manage the checkpointing by
|
|
58
|
+
# himself. If no checkpointing is ever done, we end up with an endless loop.
|
|
59
|
+
else
|
|
60
|
+
coordinator.pause_tracker.reset
|
|
61
|
+
skippable_message, = find_skippable_message
|
|
62
|
+
dispatch_to_dlq(skippable_message) if dispatch_to_dlq?
|
|
63
|
+
# We can commit the offset here because we know that we skip it "forever" and
|
|
64
|
+
# since AJ consumer commits the offset after each job, we also know that the
|
|
65
|
+
# previous job was successful
|
|
66
|
+
mark_as_consumed(skippable_message)
|
|
67
|
+
pause(coordinator.seek_offset, nil, false)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
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 Processing
|
|
17
|
+
module Strategies
|
|
18
|
+
module Aj
|
|
19
|
+
# ActiveJob enabled
|
|
20
|
+
# DLQ enabled
|
|
21
|
+
# Long-Running Job enabled
|
|
22
|
+
# Manual offset management enabled
|
|
23
|
+
#
|
|
24
|
+
# This case is a bit of special. Please see the `AjDlqMom` for explanation on how the
|
|
25
|
+
# offset management works in this case.
|
|
26
|
+
module DlqLrjMom
|
|
27
|
+
include Strategies::Default
|
|
28
|
+
include Strategies::Dlq::Default
|
|
29
|
+
include Strategies::Aj::LrjMom
|
|
30
|
+
|
|
31
|
+
# Features for this strategy
|
|
32
|
+
FEATURES = %i[
|
|
33
|
+
active_job
|
|
34
|
+
dead_letter_queue
|
|
35
|
+
long_running_job
|
|
36
|
+
manual_offset_management
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
# We cannot use a VP version of this, because non-VP can early stop on shutdown
|
|
40
|
+
def handle_after_consume
|
|
41
|
+
coordinator.on_finished do
|
|
42
|
+
if coordinator.success?
|
|
43
|
+
coordinator.pause_tracker.reset
|
|
44
|
+
|
|
45
|
+
seek(coordinator.seek_offset) unless revoked?
|
|
46
|
+
|
|
47
|
+
resume
|
|
48
|
+
elsif coordinator.pause_tracker.attempt <= topic.dead_letter_queue.max_retries
|
|
49
|
+
retry_after_pause
|
|
50
|
+
else
|
|
51
|
+
coordinator.pause_tracker.reset
|
|
52
|
+
skippable_message, = find_skippable_message
|
|
53
|
+
dispatch_to_dlq(skippable_message) if dispatch_to_dlq?
|
|
54
|
+
mark_as_consumed(skippable_message)
|
|
55
|
+
pause(coordinator.seek_offset, nil, false)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
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 Processing
|
|
17
|
+
module Strategies
|
|
18
|
+
module Aj
|
|
19
|
+
# ActiveJob enabled
|
|
20
|
+
# DLQ enabled
|
|
21
|
+
# Long-Running Job enabled
|
|
22
|
+
# Manual offset management enabled
|
|
23
|
+
# Virtual Partitions enabled
|
|
24
|
+
#
|
|
25
|
+
# This case is a bit of special. Please see the `Aj::DlqMom` for explanation on how the
|
|
26
|
+
# offset management works in this case.
|
|
27
|
+
module DlqLrjMomVp
|
|
28
|
+
include Strategies::Aj::DlqMomVp
|
|
29
|
+
include Strategies::Aj::LrjMom
|
|
30
|
+
|
|
31
|
+
# Features for this strategy
|
|
32
|
+
FEATURES = %i[
|
|
33
|
+
active_job
|
|
34
|
+
dead_letter_queue
|
|
35
|
+
long_running_job
|
|
36
|
+
manual_offset_management
|
|
37
|
+
virtual_partitions
|
|
38
|
+
].freeze
|
|
39
|
+
|
|
40
|
+
# This strategy assumes we do not early break on shutdown as it has VP
|
|
41
|
+
def handle_after_consume
|
|
42
|
+
coordinator.on_finished do |last_group_message|
|
|
43
|
+
if coordinator.success?
|
|
44
|
+
coordinator.pause_tracker.reset
|
|
45
|
+
|
|
46
|
+
# Since we have VP here we do not commit intermediate offsets and need to commit
|
|
47
|
+
# them here. We do commit in collapsed mode but this is generalized.
|
|
48
|
+
mark_as_consumed(last_group_message) unless revoked?
|
|
49
|
+
seek(coordinator.seek_offset) unless revoked?
|
|
50
|
+
|
|
51
|
+
resume
|
|
52
|
+
elsif coordinator.pause_tracker.attempt <= topic.dead_letter_queue.max_retries
|
|
53
|
+
retry_after_pause
|
|
54
|
+
else
|
|
55
|
+
coordinator.pause_tracker.reset
|
|
56
|
+
skippable_message, = find_skippable_message
|
|
57
|
+
dispatch_to_dlq(skippable_message) if dispatch_to_dlq?
|
|
58
|
+
mark_as_consumed(skippable_message)
|
|
59
|
+
pause(coordinator.seek_offset, nil, false)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
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 Processing
|
|
17
|
+
module Strategies
|
|
18
|
+
module Aj
|
|
19
|
+
# ActiveJob enabled
|
|
20
|
+
# DLQ enabled
|
|
21
|
+
# Manual offset management enabled
|
|
22
|
+
#
|
|
23
|
+
# AJ has manual offset management on by default and the offset management is delegated to
|
|
24
|
+
# the AJ consumer. This means, we cannot mark as consumed always. We can only mark as
|
|
25
|
+
# consumed when we skip given job upon errors. In all the other scenarios marking as
|
|
26
|
+
# consumed needs to happen in the AJ consumer on a per job basis.
|
|
27
|
+
module DlqMom
|
|
28
|
+
include Strategies::Dlq::Mom
|
|
29
|
+
|
|
30
|
+
# Features for this strategy
|
|
31
|
+
FEATURES = %i[
|
|
32
|
+
active_job
|
|
33
|
+
dead_letter_queue
|
|
34
|
+
manual_offset_management
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
# How should we post-finalize consumption.
|
|
38
|
+
def handle_after_consume
|
|
39
|
+
coordinator.on_finished do
|
|
40
|
+
return if revoked?
|
|
41
|
+
|
|
42
|
+
if coordinator.success?
|
|
43
|
+
# Do NOT commit offsets, they are comitted after each job in the AJ consumer.
|
|
44
|
+
coordinator.pause_tracker.reset
|
|
45
|
+
elsif coordinator.pause_tracker.attempt <= topic.dead_letter_queue.max_retries
|
|
46
|
+
retry_after_pause
|
|
47
|
+
else
|
|
48
|
+
coordinator.pause_tracker.reset
|
|
49
|
+
skippable_message, = find_skippable_message
|
|
50
|
+
dispatch_to_dlq(skippable_message) if dispatch_to_dlq?
|
|
51
|
+
# We can commit the offset here because we know that we skip it "forever" and
|
|
52
|
+
# since AJ consumer commits the offset after each job, we also know that the
|
|
53
|
+
# previous job was successful
|
|
54
|
+
mark_as_consumed(skippable_message)
|
|
55
|
+
pause(coordinator.seek_offset, nil, false)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|