karafka 2.3.3 → 2.4.0.beta2

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.
Files changed (127) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/workflows/ci.yml +12 -38
  4. data/CHANGELOG.md +59 -0
  5. data/Gemfile +6 -3
  6. data/Gemfile.lock +29 -27
  7. data/bin/integrations +1 -1
  8. data/config/locales/errors.yml +21 -2
  9. data/config/locales/pro_errors.yml +16 -1
  10. data/karafka.gemspec +4 -2
  11. data/lib/active_job/queue_adapters/karafka_adapter.rb +2 -0
  12. data/lib/karafka/admin/configs/config.rb +81 -0
  13. data/lib/karafka/admin/configs/resource.rb +88 -0
  14. data/lib/karafka/admin/configs.rb +103 -0
  15. data/lib/karafka/admin.rb +211 -90
  16. data/lib/karafka/base_consumer.rb +2 -2
  17. data/lib/karafka/cli/info.rb +9 -7
  18. data/lib/karafka/cli/server.rb +7 -7
  19. data/lib/karafka/cli/topics/align.rb +109 -0
  20. data/lib/karafka/cli/topics/base.rb +66 -0
  21. data/lib/karafka/cli/topics/create.rb +35 -0
  22. data/lib/karafka/cli/topics/delete.rb +30 -0
  23. data/lib/karafka/cli/topics/migrate.rb +31 -0
  24. data/lib/karafka/cli/topics/plan.rb +169 -0
  25. data/lib/karafka/cli/topics/repartition.rb +41 -0
  26. data/lib/karafka/cli/topics/reset.rb +18 -0
  27. data/lib/karafka/cli/topics.rb +13 -123
  28. data/lib/karafka/connection/client.rb +55 -37
  29. data/lib/karafka/connection/listener.rb +22 -17
  30. data/lib/karafka/connection/proxy.rb +93 -4
  31. data/lib/karafka/connection/status.rb +14 -2
  32. data/lib/karafka/constraints.rb +3 -3
  33. data/lib/karafka/contracts/config.rb +14 -1
  34. data/lib/karafka/contracts/topic.rb +1 -1
  35. data/lib/karafka/deserializers/headers.rb +15 -0
  36. data/lib/karafka/deserializers/key.rb +15 -0
  37. data/lib/karafka/deserializers/payload.rb +16 -0
  38. data/lib/karafka/embedded.rb +2 -0
  39. data/lib/karafka/helpers/async.rb +5 -2
  40. data/lib/karafka/helpers/colorize.rb +6 -0
  41. data/lib/karafka/instrumentation/callbacks/oauthbearer_token_refresh.rb +29 -0
  42. data/lib/karafka/instrumentation/logger_listener.rb +23 -3
  43. data/lib/karafka/instrumentation/notifications.rb +10 -0
  44. data/lib/karafka/instrumentation/vendors/appsignal/client.rb +16 -2
  45. data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +20 -0
  46. data/lib/karafka/messages/batch_metadata.rb +1 -1
  47. data/lib/karafka/messages/builders/batch_metadata.rb +1 -1
  48. data/lib/karafka/messages/builders/message.rb +10 -6
  49. data/lib/karafka/messages/message.rb +2 -1
  50. data/lib/karafka/messages/metadata.rb +20 -4
  51. data/lib/karafka/messages/parser.rb +1 -1
  52. data/lib/karafka/pro/base_consumer.rb +12 -23
  53. data/lib/karafka/pro/encryption/cipher.rb +7 -3
  54. data/lib/karafka/pro/encryption/contracts/config.rb +1 -0
  55. data/lib/karafka/pro/encryption/errors.rb +4 -1
  56. data/lib/karafka/pro/encryption/messages/middleware.rb +13 -11
  57. data/lib/karafka/pro/encryption/messages/parser.rb +22 -20
  58. data/lib/karafka/pro/encryption/setup/config.rb +5 -0
  59. data/lib/karafka/pro/iterator/expander.rb +2 -1
  60. data/lib/karafka/pro/iterator/tpl_builder.rb +38 -0
  61. data/lib/karafka/pro/iterator.rb +28 -2
  62. data/lib/karafka/pro/loader.rb +3 -0
  63. data/lib/karafka/pro/processing/coordinator.rb +15 -2
  64. data/lib/karafka/pro/processing/expansions_selector.rb +2 -0
  65. data/lib/karafka/pro/processing/jobs_queue.rb +122 -5
  66. data/lib/karafka/pro/processing/periodic_job/consumer.rb +67 -0
  67. data/lib/karafka/pro/processing/piping/consumer.rb +126 -0
  68. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom.rb +1 -1
  69. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom_vp.rb +1 -1
  70. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom.rb +1 -1
  71. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom_vp.rb +1 -1
  72. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom.rb +1 -1
  73. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom_vp.rb +1 -1
  74. data/lib/karafka/pro/processing/strategies/aj/dlq_mom.rb +1 -1
  75. data/lib/karafka/pro/processing/strategies/aj/dlq_mom_vp.rb +1 -1
  76. data/lib/karafka/pro/processing/strategies/aj/lrj_mom_vp.rb +2 -0
  77. data/lib/karafka/pro/processing/strategies/default.rb +5 -1
  78. data/lib/karafka/pro/processing/strategies/dlq/default.rb +21 -5
  79. data/lib/karafka/pro/processing/strategies/lrj/default.rb +2 -0
  80. data/lib/karafka/pro/processing/strategies/lrj/mom.rb +2 -0
  81. data/lib/karafka/pro/processing/subscription_groups_coordinator.rb +52 -0
  82. data/lib/karafka/pro/routing/features/direct_assignments/config.rb +27 -0
  83. data/lib/karafka/pro/routing/features/direct_assignments/contracts/consumer_group.rb +53 -0
  84. data/lib/karafka/pro/routing/features/direct_assignments/contracts/topic.rb +108 -0
  85. data/lib/karafka/pro/routing/features/direct_assignments/subscription_group.rb +77 -0
  86. data/lib/karafka/pro/routing/features/direct_assignments/topic.rb +69 -0
  87. data/lib/karafka/pro/routing/features/direct_assignments.rb +25 -0
  88. data/lib/karafka/pro/routing/features/patterns/builder.rb +1 -1
  89. data/lib/karafka/pro/routing/features/swarm/contracts/routing.rb +76 -0
  90. data/lib/karafka/pro/routing/features/swarm/contracts/topic.rb +16 -5
  91. data/lib/karafka/pro/routing/features/swarm/topic.rb +25 -2
  92. data/lib/karafka/pro/routing/features/swarm.rb +11 -0
  93. data/lib/karafka/pro/swarm/liveness_listener.rb +20 -0
  94. data/lib/karafka/processing/coordinator.rb +17 -8
  95. data/lib/karafka/processing/coordinators_buffer.rb +5 -2
  96. data/lib/karafka/processing/executor.rb +6 -2
  97. data/lib/karafka/processing/executors_buffer.rb +5 -2
  98. data/lib/karafka/processing/jobs_queue.rb +9 -4
  99. data/lib/karafka/processing/strategies/aj_dlq_mom.rb +1 -1
  100. data/lib/karafka/processing/strategies/default.rb +7 -1
  101. data/lib/karafka/processing/strategies/dlq.rb +17 -2
  102. data/lib/karafka/processing/workers_batch.rb +4 -1
  103. data/lib/karafka/routing/builder.rb +6 -2
  104. data/lib/karafka/routing/consumer_group.rb +2 -1
  105. data/lib/karafka/routing/features/dead_letter_queue/config.rb +5 -0
  106. data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +8 -0
  107. data/lib/karafka/routing/features/dead_letter_queue/topic.rb +10 -2
  108. data/lib/karafka/routing/features/deserializers/config.rb +18 -0
  109. data/lib/karafka/routing/features/deserializers/contracts/topic.rb +31 -0
  110. data/lib/karafka/routing/features/deserializers/topic.rb +51 -0
  111. data/lib/karafka/routing/features/deserializers.rb +11 -0
  112. data/lib/karafka/routing/proxy.rb +9 -14
  113. data/lib/karafka/routing/router.rb +11 -2
  114. data/lib/karafka/routing/subscription_group.rb +9 -1
  115. data/lib/karafka/routing/topic.rb +0 -1
  116. data/lib/karafka/runner.rb +1 -1
  117. data/lib/karafka/setup/config.rb +50 -9
  118. data/lib/karafka/status.rb +7 -8
  119. data/lib/karafka/swarm/supervisor.rb +16 -2
  120. data/lib/karafka/templates/karafka.rb.erb +28 -1
  121. data/lib/karafka/version.rb +1 -1
  122. data.tar.gz.sig +0 -0
  123. metadata +38 -12
  124. metadata.gz.sig +0 -0
  125. data/lib/karafka/routing/consumer_mapper.rb +0 -23
  126. data/lib/karafka/serialization/json/deserializer.rb +0 -19
  127. data/lib/karafka/time_trackers/partition_usage.rb +0 -56
@@ -0,0 +1,27 @@
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 Routing
17
+ module Features
18
+ class DirectAssignments < Base
19
+ # Direct assignments feature configuration
20
+ Config = Struct.new(:active, :partitions, keyword_init: true) do
21
+ alias_method :active?, :active
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,53 @@
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 Routing
17
+ module Features
18
+ class DirectAssignments < Base
19
+ module Contracts
20
+ # Contract to validate configuration of the direct assignments feature
21
+ class ConsumerGroup < Karafka::Contracts::Base
22
+ configure do |config|
23
+ config.error_messages = YAML.safe_load(
24
+ File.read(
25
+ File.join(Karafka.gem_root, 'config', 'locales', 'pro_errors.yml')
26
+ )
27
+ ).fetch('en').fetch('validations').fetch('consumer_group')
28
+
29
+ virtual do |data, errors|
30
+ next unless errors.empty?
31
+
32
+ active = []
33
+
34
+ data[:topics].each do |topic|
35
+ active << topic[:direct_assignments][:active]
36
+ end
37
+
38
+ # If none active we use standard subscriptions
39
+ next if active.none?
40
+ # If all topics from a given CG are using direct assignments that is expected
41
+ # and allowed
42
+ next if active.all?
43
+
44
+ [[%i[direct_assignments], :homogenous]]
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,108 @@
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 Routing
17
+ module Features
18
+ class DirectAssignments < Base
19
+ # Namespace for direct assignments feature contracts
20
+ module Contracts
21
+ # Contract to validate configuration of the direct assignments topic feature
22
+ class Topic < Karafka::Contracts::Base
23
+ configure do |config|
24
+ config.error_messages = YAML.safe_load(
25
+ File.read(
26
+ File.join(Karafka.gem_root, 'config', 'locales', 'pro_errors.yml')
27
+ )
28
+ ).fetch('en').fetch('validations').fetch('topic')
29
+ end
30
+
31
+ nested(:direct_assignments) do
32
+ required(:active) { |val| [true, false].include?(val) }
33
+
34
+ required(:partitions) do |val|
35
+ next true if val == true
36
+ next false unless val.is_a?(Hash)
37
+ next false unless val.keys.all? { |part| part.is_a?(Integer) }
38
+ next false unless val.values.all? { |flag| flag == true }
39
+
40
+ true
41
+ end
42
+ end
43
+
44
+ virtual do |data, errors|
45
+ next unless errors.empty?
46
+
47
+ direct_assignments = data[:direct_assignments]
48
+ partitions = direct_assignments[:partitions]
49
+
50
+ next unless direct_assignments[:active]
51
+ next unless partitions.is_a?(Hash)
52
+ next unless partitions.empty?
53
+
54
+ [[%i[direct_assignments], :active_but_empty]]
55
+ end
56
+
57
+ # Make sure that when we use swarm, all requested partitions have allocation
58
+ # We cannot assign partitions and then not allocate them in swarm mode
59
+ # If this is the case, they should not be assigned in the first place
60
+ virtual do |data, errors|
61
+ next unless errors.empty?
62
+
63
+ direct_assignments = data[:direct_assignments]
64
+ swarm = data[:swarm]
65
+
66
+ next unless direct_assignments[:active]
67
+ next unless swarm[:active]
68
+
69
+ nodes = swarm[:nodes]
70
+
71
+ next unless nodes.is_a?(Hash)
72
+ # Can be true for all partitions assignment and in this case we do not check
73
+ next unless direct_assignments[:partitions].is_a?(Hash)
74
+
75
+ direct_partitions = direct_assignments[:partitions].keys
76
+ swarm_partitions = nodes.values.flatten
77
+
78
+ next unless swarm_partitions.all? { |partition| partition.is_a?(Integer) }
79
+ next if direct_partitions.sort == swarm_partitions.sort
80
+
81
+ # If we assigned more partitions than we distributed in swarm
82
+ if (direct_partitions - swarm_partitions).size.positive?
83
+ [[%i[direct_assignments], :swarm_not_complete]]
84
+ else
85
+ [[%i[direct_assignments], :swarm_overbooked]]
86
+ end
87
+ end
88
+
89
+ # Make sure that direct assignments are not used together with patterns as we
90
+ # cannot apply patterns on direct assignments
91
+ virtual do |data, errors|
92
+ next unless errors.empty?
93
+
94
+ direct_assignments = data[:direct_assignments]
95
+ patterns = data[:patterns]
96
+
97
+ next unless direct_assignments[:active]
98
+ next unless patterns[:active]
99
+
100
+ [[%i[direct_assignments], :patterns_active]]
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,77 @@
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 Routing
17
+ module Features
18
+ # Alterations to the direct assignments that allow us to do stable direct assignments
19
+ # without working with consumer groups dynamic assignments
20
+ class DirectAssignments < Base
21
+ # Extension allowing us to select correct subscriptions and assignments based on the
22
+ # expanded routing setup
23
+ module SubscriptionGroup
24
+ # @return [false, Array<String>] false if we do not have any subscriptions or array
25
+ # with all the subscriptions for given subscription group
26
+ def subscriptions
27
+ topics
28
+ .select(&:active?)
29
+ .reject { |topic| topic.direct_assignments.active? }
30
+ .map(&:subscription_name)
31
+ .then { |subscriptions| subscriptions.empty? ? false : subscriptions }
32
+ end
33
+
34
+ # @param consumer [Karafka::Connection::Proxy] consumer for expanding the partition
35
+ # knowledge in case of certain topics assignments
36
+ # @return [Rdkafka::Consumer::TopicPartitionList] final tpl for assignments
37
+ def assignments(consumer)
38
+ topics
39
+ .select(&:active?)
40
+ .select { |topic| topic.direct_assignments.active? }
41
+ .map { |topic| build_assignments(topic) }
42
+ .to_h
43
+ .tap { |topics| return false if topics.empty? }
44
+ .then { |topics| Iterator::Expander.new.call(topics) }
45
+ .then { |topics| Iterator::TplBuilder.new(consumer, topics).call }
46
+ end
47
+
48
+ private
49
+
50
+ # Builds assignments based on all the routing stuff. Takes swarm into consideration.
51
+ #
52
+ # @param topic [Karafka::Routing::Topic]
53
+ # @return [Array<String, Hash>]
54
+ def build_assignments(topic)
55
+ node = Karafka::App.config.swarm.node
56
+
57
+ standard_setup = [
58
+ topic.subscription_name,
59
+ topic.direct_assignments.partitions
60
+ ]
61
+
62
+ return standard_setup unless node
63
+ # Unless user explicitly assigned particular partitions to particular nodes, we just
64
+ # go with full regular assignments
65
+ return standard_setup unless topic.swarm.nodes.is_a?(Hash)
66
+
67
+ [
68
+ topic.subscription_name,
69
+ topic.swarm.nodes.fetch(node.id).map { |partition| [partition, true] }.to_h
70
+ ]
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,69 @@
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 Routing
17
+ module Features
18
+ class DirectAssignments < Base
19
+ # Topic extensions for direct assignments
20
+ module Topic
21
+ # Allows for direct assignment of
22
+ # @param partitions_or_all [true, Array<Integer>] informs Karafka that we want
23
+ # to use direct assignments instead of automatic for this topic. It also allows us
24
+ # to specify which partitions we're interested in or `true` if in all
25
+ #
26
+ # @example Assign all available partitions
27
+ # direct_assignments(true)
28
+ #
29
+ # @example Assign partitions 2, 3 and 5
30
+ # direct_assignments(2, 3, 5)
31
+ #
32
+ # @example Assign partitions from 0 to 3
33
+ # direct_assignments(0..3)
34
+ def direct_assignments(*partitions_or_all)
35
+ @direct_assignments ||= if partitions_or_all == [true]
36
+ Config.new(
37
+ active: true,
38
+ partitions: true
39
+ )
40
+ elsif partitions_or_all.size == 1 && partitions_or_all.first.is_a?(Range)
41
+ partitions_or_all = partitions_or_all.first.to_a
42
+
43
+ Config.new(
44
+ active: true,
45
+ partitions: partitions_or_all.map { |partition| [partition, true] }.to_h
46
+ )
47
+ else
48
+ Config.new(
49
+ active: !partitions_or_all.empty?,
50
+ partitions: partitions_or_all.map { |partition| [partition, true] }.to_h
51
+ )
52
+ end
53
+ end
54
+
55
+ alias assign direct_assignments
56
+
57
+ # @return [Hash] topic with all its native configuration options plus direct
58
+ # assignments
59
+ def to_h
60
+ super.merge(
61
+ direct_assignments: direct_assignments.to_h
62
+ ).freeze
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,25 @@
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 Routing
17
+ module Features
18
+ # Allows for building direct assignments that bypass consumer group auto-assignments
19
+ # Useful for building complex pipelines that have explicit assignment requirements
20
+ class DirectAssignments < Base
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -26,7 +26,7 @@ module Karafka
26
26
  # the regexp if we used named patterns
27
27
  # @param block [Proc]
28
28
  def pattern(regexp_or_name, regexp = nil, &block)
29
- consumer_group('app') do
29
+ consumer_group(default_group_id) do
30
30
  pattern(regexp_or_name, regexp, &block)
31
31
  end
32
32
  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 Routing
17
+ module Features
18
+ class Swarm < Base
19
+ module Contracts
20
+ # Special contract that validates prior to starting swarm that each node has
21
+ # at least one assignment.
22
+ #
23
+ # It is special because we cannot run it during routing definitions, because we can
24
+ # only run it when all routes are defined and full context is available.
25
+ #
26
+ # This is why we use it before warmup when everything is expected to be configured.
27
+ class Routing < Karafka::Contracts::Base
28
+ configure do |config|
29
+ config.error_messages = YAML.safe_load(
30
+ File.read(
31
+ File.join(Karafka.gem_root, 'config', 'locales', 'pro_errors.yml')
32
+ )
33
+ ).fetch('en').fetch('validations').fetch('routing')
34
+ end
35
+
36
+ # Validates that each node has at least one assignment.
37
+ #
38
+ # @param builder [Karafka::Routing::Builder]
39
+ def validate!(builder)
40
+ nodes_setup = Hash.new do |h, node_id|
41
+ h[node_id] = { active: false, node_id: node_id }
42
+ end
43
+
44
+ # Initialize nodes in the hash so we can iterate over them
45
+ App.config.swarm.nodes.times { |node_id| nodes_setup[node_id] }
46
+ nodes_setup.freeze
47
+
48
+ builder.each do |consumer_group|
49
+ consumer_group.topics.each do |topic|
50
+ nodes_setup.each do |node_id, details|
51
+ next unless topic.active?
52
+ next unless topic.swarm.nodes.include?(node_id)
53
+
54
+ details[:active] = true
55
+ end
56
+ end
57
+ end
58
+
59
+ nodes_setup.each_value do |details|
60
+ super(details)
61
+ end
62
+ end
63
+
64
+ virtual do |data, errors|
65
+ next unless errors.empty?
66
+ next if data[:active]
67
+
68
+ [[%i[swarm_nodes], :not_used]]
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -32,10 +32,18 @@ module Karafka
32
32
  required(:active) { |val| val == true }
33
33
 
34
34
  required(:nodes) do |val|
35
- val.is_a?(Range) || (
36
- val.is_a?(Array) &&
37
- val.all? { |id| id.is_a?(Integer) }
38
- )
35
+ next true if val.is_a?(Range)
36
+ next true if val.is_a?(Array) && val.all? { |id| id.is_a?(Integer) }
37
+ next false unless val.is_a?(Hash)
38
+ next false unless val.keys.all? { |id| id.is_a?(Integer) }
39
+
40
+ values = val.values
41
+
42
+ next false unless values.all? { |ps| ps.is_a?(Array) || ps.is_a?(Range) }
43
+ next true if values.flatten.all? { |part| part.is_a?(Integer) }
44
+ next true if values.flatten.all? { |part| part.is_a?(Range) }
45
+
46
+ false
39
47
  end
40
48
  end
41
49
 
@@ -46,9 +54,12 @@ module Karafka
46
54
  next unless errors.empty?
47
55
 
48
56
  nodes = data[:swarm][:nodes]
57
+ # Hash can be used when we use direct assignments. In such cases nodes ids are
58
+ # keys in the hash and values are partitions
59
+ nodes = nodes.keys if nodes.is_a?(Hash)
49
60
 
50
61
  # Defaults
51
- next if nodes.first.zero? && nodes.last == Float::INFINITY
62
+ next if nodes.first.zero? && nodes.include?(Float::INFINITY)
52
63
 
53
64
  # If our expectation towards which node should run things matches at least one
54
65
  # node, then it's ok
@@ -19,8 +19,31 @@ module Karafka
19
19
  # Topic swarm API extensions
20
20
  module Topic
21
21
  # Allows defining swarm routing topic settings
22
- # @param nodes [Range, Array] range of nodes ids or array with nodes ids for which we
23
- # should run given topic
22
+ # @param nodes [Range, Array, Hash] range of nodes ids or array with nodes ids for
23
+ # which we should run given topic or hash with nodes expected partition assignments
24
+ # for the direct assignments API.
25
+ #
26
+ # @example Assign given topic only to node 1
27
+ # swarm(nodes: [1])
28
+ #
29
+ # @example Assign given topic to nodes from 1 to 3
30
+ # swarm(nodes: 1..3)
31
+ #
32
+ # @example Assign partitions 2 and 3 to node 0 and partitions 0, 1 to node 1
33
+ # swarm(
34
+ # nodes: {
35
+ # 0 => [2, 3],
36
+ # 1 => [0, 1]
37
+ # }
38
+ # )
39
+ #
40
+ # @example Assign partitions in ranges to nodes
41
+ # swarm(
42
+ # nodes: {
43
+ # 0 => (0..2),
44
+ # 1 => (3..5)
45
+ # }
46
+ # )
24
47
  def swarm(nodes: (0...Karafka::App.config.swarm.nodes))
25
48
  @swarm ||= Config.new(active: true, nodes: nodes)
26
49
  end
@@ -18,6 +18,17 @@ module Karafka
18
18
  # Karafka Pro Swarm extensions to the routing
19
19
  # They allow for more granular work assignment in the swarm
20
20
  class Swarm < Base
21
+ class << self
22
+ # Binds our routing validation contract prior to warmup in the supervisor, so we can
23
+ # run it when all the context should be there (config + full routing)
24
+ #
25
+ # @param config [Karafka::Core::Configurable::Node] app config
26
+ def post_setup(config)
27
+ config.monitor.subscribe('app.before_warmup') do
28
+ Contracts::Routing.new.validate!(config.internal.routing.builder)
29
+ end
30
+ end
31
+ end
21
32
  end
22
33
  end
23
34
  end
@@ -103,6 +103,26 @@ module Karafka
103
103
  end
104
104
  end
105
105
 
106
+ # Deregister the polling tracker for given listener
107
+ # @param _event [Karafka::Core::Monitoring::Event]
108
+ def on_connection_listener_stopping(_event)
109
+ # We are interested in disabling tracking for given listener only if it was requested
110
+ # when karafka was running. If we would always clear, it would not catch the shutdown
111
+ # polling requirements. The "running" listener shutdown operations happen only when
112
+ # the manager requests it for downscaling.
113
+ return if Karafka::App.done?
114
+
115
+ clear_polling_tick
116
+ end
117
+
118
+ # Deregister the polling tracker for given listener
119
+ # @param _event [Karafka::Core::Monitoring::Event]
120
+ def on_connection_listener_stopped(_event)
121
+ return if Karafka::App.done?
122
+
123
+ clear_polling_tick
124
+ end
125
+
106
126
  private
107
127
 
108
128
  # @return [Integer] object id of the current thread
@@ -11,6 +11,7 @@ module Karafka
11
11
  # future mistakes.
12
12
  class Coordinator
13
13
  extend Forwardable
14
+ include Core::Helpers::Time
14
15
 
15
16
  attr_reader :pause_tracker, :seek_offset, :topic, :partition
16
17
 
@@ -25,12 +26,13 @@ module Karafka
25
26
  @pause_tracker = pause_tracker
26
27
  @revoked = false
27
28
  @consumptions = {}
28
- @running_jobs = 0
29
+ @running_jobs = Hash.new { |h, k| h[k] = 0 }
29
30
  @manual_pause = false
30
31
  @manual_seek = false
31
32
  @mutex = Mutex.new
32
33
  @marked = false
33
34
  @failure = false
35
+ @changed_at = monotonic_now
34
36
  end
35
37
 
36
38
  # Starts the coordinator for given consumption jobs
@@ -38,7 +40,7 @@ module Karafka
38
40
  # going to coordinate work. Not used with regular coordinator.
39
41
  def start(messages)
40
42
  @failure = false
41
- @running_jobs = 0
43
+ @running_jobs[:consume] = 0
42
44
  # We need to clear the consumption results hash here, otherwise we could end up storing
43
45
  # consumption results of consumer instances we no longer control
44
46
  @consumptions.clear
@@ -70,16 +72,22 @@ module Karafka
70
72
  end
71
73
 
72
74
  # Increases number of jobs that we handle with this coordinator
73
- def increment
74
- synchronize { @running_jobs += 1 }
75
+ # @param job_type [Symbol] type of job that we want to increment
76
+ def increment(job_type)
77
+ synchronize do
78
+ @running_jobs[job_type] += 1
79
+ @changed_at = monotonic_now
80
+ end
75
81
  end
76
82
 
77
83
  # Decrements number of jobs we handle at the moment
78
- def decrement
84
+ # @param job_type [Symbol] type of job that we want to decrement
85
+ def decrement(job_type)
79
86
  synchronize do
80
- @running_jobs -= 1
87
+ @running_jobs[job_type] -= 1
88
+ @changed_at = monotonic_now
81
89
 
82
- return @running_jobs unless @running_jobs.negative?
90
+ return @running_jobs[job_type] unless @running_jobs[job_type].negative?
83
91
 
84
92
  # This should never happen. If it does, something is heavily out of sync. Please reach
85
93
  # out to us if you encounter this
@@ -90,9 +98,10 @@ module Karafka
90
98
  # Is all the consumption done and finished successfully for this coordinator
91
99
  # We do not say we're successful until all work is done, because running work may still
92
100
  # crash.
101
+ # @note This is only used for consume synchronization
93
102
  def success?
94
103
  synchronize do
95
- @running_jobs.zero? && @consumptions.values.all?(&:success?)
104
+ @running_jobs[:consume].zero? && @consumptions.values.all?(&:success?)
96
105
  end
97
106
  end
98
107
 
@@ -9,10 +9,13 @@ module Karafka
9
9
  # @note This buffer operates only from the listener loop, thus we do not have to make it
10
10
  # thread-safe.
11
11
  class CoordinatorsBuffer
12
+ include Helpers::ConfigImporter.new(
13
+ coordinator_class: %i[internal processing coordinator_class]
14
+ )
15
+
12
16
  # @param topics [Karafka::Routing::Topics]
13
17
  def initialize(topics)
14
18
  @pauses_manager = Connection::PausesManager.new
15
- @coordinator_class = ::Karafka::App.config.internal.processing.coordinator_class
16
19
  @coordinators = Hash.new { |h, k| h[k] = {} }
17
20
  @topics = topics
18
21
  end
@@ -23,7 +26,7 @@ module Karafka
23
26
  @coordinators[topic_name][partition] ||= begin
24
27
  routing_topic = @topics.find(topic_name)
25
28
 
26
- @coordinator_class.new(
29
+ coordinator_class.new(
27
30
  routing_topic,
28
31
  partition,
29
32
  @pauses_manager.fetch(routing_topic, partition)