nulogy_message_bus_consumer 0.3.0 → 0.5.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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +5 -4
  3. data/config/credentials/message-bus-us-east-1.key +1 -0
  4. data/config/credentials/message-bus-us-east-1.yml.enc +1 -0
  5. data/lib/nulogy_message_bus_consumer.rb +18 -6
  6. data/lib/nulogy_message_bus_consumer/clock.rb +13 -0
  7. data/lib/nulogy_message_bus_consumer/config.rb +12 -4
  8. data/lib/nulogy_message_bus_consumer/deployment/ecs.rb +23 -0
  9. data/lib/nulogy_message_bus_consumer/handlers/log_unprocessed_messages.rb +2 -1
  10. data/lib/nulogy_message_bus_consumer/kafka_utils.rb +2 -1
  11. data/lib/nulogy_message_bus_consumer/lag_tracker.rb +53 -0
  12. data/lib/nulogy_message_bus_consumer/message.rb +12 -5
  13. data/lib/nulogy_message_bus_consumer/null_logger.rb +6 -3
  14. data/lib/nulogy_message_bus_consumer/pipeline.rb +6 -3
  15. data/lib/nulogy_message_bus_consumer/steps/commit_on_success.rb +1 -0
  16. data/lib/nulogy_message_bus_consumer/steps/connect_to_message_bus.rb +27 -8
  17. data/lib/nulogy_message_bus_consumer/steps/deduplicate_messages.rb +1 -1
  18. data/lib/nulogy_message_bus_consumer/steps/{monitor_replication_lag.rb → log_consumer_lag.rb} +3 -3
  19. data/lib/nulogy_message_bus_consumer/steps/log_messages.rb +14 -3
  20. data/lib/nulogy_message_bus_consumer/steps/stream_messages.rb +2 -2
  21. data/lib/nulogy_message_bus_consumer/steps/stream_messages_until_none_are_left.rb +2 -2
  22. data/lib/nulogy_message_bus_consumer/steps/supervise_consumer_lag.rb +76 -0
  23. data/lib/nulogy_message_bus_consumer/version.rb +1 -1
  24. data/lib/tasks/engine/message_bus_consumer.rake +9 -10
  25. data/spec/dummy/Rakefile +6 -0
  26. data/spec/dummy/app/assets/config/manifest.js +3 -0
  27. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  28. data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
  29. data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
  30. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  31. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  32. data/spec/dummy/app/javascript/packs/application.js +15 -0
  33. data/spec/dummy/app/jobs/application_job.rb +7 -0
  34. data/spec/dummy/app/mailers/application_mailer.rb +4 -0
  35. data/spec/dummy/app/models/application_record.rb +3 -0
  36. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  37. data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
  38. data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
  39. data/spec/dummy/bin/rails +4 -0
  40. data/spec/dummy/bin/rake +4 -0
  41. data/spec/dummy/bin/setup +33 -0
  42. data/spec/dummy/config.ru +5 -0
  43. data/spec/dummy/config/application.rb +29 -0
  44. data/spec/dummy/config/boot.rb +5 -0
  45. data/spec/dummy/config/cable.yml +10 -0
  46. data/spec/dummy/config/credentials/message-bus-us-east-1.key +1 -0
  47. data/spec/dummy/config/credentials/message-bus-us-east-1.yml.enc +1 -0
  48. data/spec/dummy/config/database.yml +27 -0
  49. data/spec/dummy/config/environment.rb +5 -0
  50. data/spec/dummy/config/environments/development.rb +62 -0
  51. data/spec/dummy/config/environments/production.rb +112 -0
  52. data/spec/dummy/config/environments/test.rb +49 -0
  53. data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
  54. data/spec/dummy/config/initializers/assets.rb +12 -0
  55. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  56. data/spec/dummy/config/initializers/content_security_policy.rb +28 -0
  57. data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
  58. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  59. data/spec/dummy/config/initializers/inflections.rb +16 -0
  60. data/spec/dummy/config/initializers/message_bus_consumer.rb +5 -0
  61. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  62. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  63. data/spec/dummy/config/locales/en.yml +33 -0
  64. data/spec/dummy/config/puma.rb +36 -0
  65. data/spec/dummy/config/routes.rb +3 -0
  66. data/spec/dummy/config/spring.rb +6 -0
  67. data/spec/dummy/config/storage.yml +34 -0
  68. data/spec/dummy/db/schema.rb +21 -0
  69. data/spec/dummy/log/development.log +4 -0
  70. data/spec/dummy/log/production.log +18 -0
  71. data/spec/dummy/log/test.log +6083 -0
  72. data/spec/dummy/public/404.html +67 -0
  73. data/spec/dummy/public/422.html +67 -0
  74. data/spec/dummy/public/500.html +66 -0
  75. data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
  76. data/spec/dummy/public/apple-touch-icon.png +0 -0
  77. data/spec/dummy/public/favicon.ico +0 -0
  78. data/spec/dummy/tmp/development_secret.txt +1 -0
  79. data/spec/integration/nulogy_message_bus_consumer/auditor_spec.rb +59 -0
  80. data/spec/integration/nulogy_message_bus_consumer/kafka_utils_spec.rb +41 -0
  81. data/spec/integration/nulogy_message_bus_consumer/steps/commit_on_success_spec.rb +131 -0
  82. data/spec/integration/nulogy_message_bus_consumer/steps/connect_to_message_bus_spec.rb +54 -0
  83. data/spec/integration/nulogy_message_bus_consumer/steps/supervise_consumer_lag_spec.rb +54 -0
  84. data/spec/integration/test_topic_spec.rb +39 -0
  85. data/spec/spec_helper.rb +49 -0
  86. data/spec/support/kafka.rb +74 -0
  87. data/spec/support/middleware_tap.rb +12 -0
  88. data/spec/support/test_topic.rb +48 -0
  89. data/spec/unit/nulogy_message_bus_consumer/config_spec.rb +20 -0
  90. data/spec/unit/nulogy_message_bus_consumer/lag_tracker.rb +35 -0
  91. data/spec/unit/nulogy_message_bus_consumer/message_spec.rb +84 -0
  92. data/spec/unit/nulogy_message_bus_consumer/pipeline_spec.rb +49 -0
  93. data/spec/unit/nulogy_message_bus_consumer/steps/commit_on_success_spec.rb +58 -0
  94. data/spec/unit/nulogy_message_bus_consumer/steps/deduplicate_messages_spec.rb +56 -0
  95. data/spec/unit/nulogy_message_bus_consumer/steps/log_messages_spec.rb +70 -0
  96. data/spec/unit/nulogy_message_bus_consumer/steps/monitor_replication_lag/calculator_spec.rb +63 -0
  97. data/spec/unit/nulogy_message_bus_consumer/steps/stream_messages_spec.rb +35 -0
  98. data/spec/unit/nulogy_message_bus_consumer_spec.rb +30 -0
  99. metadata +251 -27
@@ -0,0 +1,67 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The page you were looking for doesn't exist (404)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ .rails-default-error-page {
8
+ background-color: #EFEFEF;
9
+ color: #2E2F30;
10
+ text-align: center;
11
+ font-family: arial, sans-serif;
12
+ margin: 0;
13
+ }
14
+
15
+ .rails-default-error-page div.dialog {
16
+ width: 95%;
17
+ max-width: 33em;
18
+ margin: 4em auto 0;
19
+ }
20
+
21
+ .rails-default-error-page div.dialog > div {
22
+ border: 1px solid #CCC;
23
+ border-right-color: #999;
24
+ border-left-color: #999;
25
+ border-bottom-color: #BBB;
26
+ border-top: #B00100 solid 4px;
27
+ border-top-left-radius: 9px;
28
+ border-top-right-radius: 9px;
29
+ background-color: white;
30
+ padding: 7px 12% 0;
31
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
32
+ }
33
+
34
+ .rails-default-error-page h1 {
35
+ font-size: 100%;
36
+ color: #730E15;
37
+ line-height: 1.5em;
38
+ }
39
+
40
+ .rails-default-error-page div.dialog > p {
41
+ margin: 0 0 1em;
42
+ padding: 1em;
43
+ background-color: #F7F7F7;
44
+ border: 1px solid #CCC;
45
+ border-right-color: #999;
46
+ border-left-color: #999;
47
+ border-bottom-color: #999;
48
+ border-bottom-left-radius: 4px;
49
+ border-bottom-right-radius: 4px;
50
+ border-top-color: #DADADA;
51
+ color: #666;
52
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
53
+ }
54
+ </style>
55
+ </head>
56
+
57
+ <body class="rails-default-error-page">
58
+ <!-- This file lives in public/404.html -->
59
+ <div class="dialog">
60
+ <div>
61
+ <h1>The page you were looking for doesn't exist.</h1>
62
+ <p>You may have mistyped the address or the page may have moved.</p>
63
+ </div>
64
+ <p>If you are the application owner check the logs for more information.</p>
65
+ </div>
66
+ </body>
67
+ </html>
@@ -0,0 +1,67 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The change you wanted was rejected (422)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ .rails-default-error-page {
8
+ background-color: #EFEFEF;
9
+ color: #2E2F30;
10
+ text-align: center;
11
+ font-family: arial, sans-serif;
12
+ margin: 0;
13
+ }
14
+
15
+ .rails-default-error-page div.dialog {
16
+ width: 95%;
17
+ max-width: 33em;
18
+ margin: 4em auto 0;
19
+ }
20
+
21
+ .rails-default-error-page div.dialog > div {
22
+ border: 1px solid #CCC;
23
+ border-right-color: #999;
24
+ border-left-color: #999;
25
+ border-bottom-color: #BBB;
26
+ border-top: #B00100 solid 4px;
27
+ border-top-left-radius: 9px;
28
+ border-top-right-radius: 9px;
29
+ background-color: white;
30
+ padding: 7px 12% 0;
31
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
32
+ }
33
+
34
+ .rails-default-error-page h1 {
35
+ font-size: 100%;
36
+ color: #730E15;
37
+ line-height: 1.5em;
38
+ }
39
+
40
+ .rails-default-error-page div.dialog > p {
41
+ margin: 0 0 1em;
42
+ padding: 1em;
43
+ background-color: #F7F7F7;
44
+ border: 1px solid #CCC;
45
+ border-right-color: #999;
46
+ border-left-color: #999;
47
+ border-bottom-color: #999;
48
+ border-bottom-left-radius: 4px;
49
+ border-bottom-right-radius: 4px;
50
+ border-top-color: #DADADA;
51
+ color: #666;
52
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
53
+ }
54
+ </style>
55
+ </head>
56
+
57
+ <body class="rails-default-error-page">
58
+ <!-- This file lives in public/422.html -->
59
+ <div class="dialog">
60
+ <div>
61
+ <h1>The change you wanted was rejected.</h1>
62
+ <p>Maybe you tried to change something you didn't have access to.</p>
63
+ </div>
64
+ <p>If you are the application owner check the logs for more information.</p>
65
+ </div>
66
+ </body>
67
+ </html>
@@ -0,0 +1,66 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>We're sorry, but something went wrong (500)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ .rails-default-error-page {
8
+ background-color: #EFEFEF;
9
+ color: #2E2F30;
10
+ text-align: center;
11
+ font-family: arial, sans-serif;
12
+ margin: 0;
13
+ }
14
+
15
+ .rails-default-error-page div.dialog {
16
+ width: 95%;
17
+ max-width: 33em;
18
+ margin: 4em auto 0;
19
+ }
20
+
21
+ .rails-default-error-page div.dialog > div {
22
+ border: 1px solid #CCC;
23
+ border-right-color: #999;
24
+ border-left-color: #999;
25
+ border-bottom-color: #BBB;
26
+ border-top: #B00100 solid 4px;
27
+ border-top-left-radius: 9px;
28
+ border-top-right-radius: 9px;
29
+ background-color: white;
30
+ padding: 7px 12% 0;
31
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
32
+ }
33
+
34
+ .rails-default-error-page h1 {
35
+ font-size: 100%;
36
+ color: #730E15;
37
+ line-height: 1.5em;
38
+ }
39
+
40
+ .rails-default-error-page div.dialog > p {
41
+ margin: 0 0 1em;
42
+ padding: 1em;
43
+ background-color: #F7F7F7;
44
+ border: 1px solid #CCC;
45
+ border-right-color: #999;
46
+ border-left-color: #999;
47
+ border-bottom-color: #999;
48
+ border-bottom-left-radius: 4px;
49
+ border-bottom-right-radius: 4px;
50
+ border-top-color: #DADADA;
51
+ color: #666;
52
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
53
+ }
54
+ </style>
55
+ </head>
56
+
57
+ <body class="rails-default-error-page">
58
+ <!-- This file lives in public/500.html -->
59
+ <div class="dialog">
60
+ <div>
61
+ <h1>We're sorry, but something went wrong.</h1>
62
+ </div>
63
+ <p>If you are the application owner check the logs for more information.</p>
64
+ </div>
65
+ </body>
66
+ </html>
File without changes
File without changes
@@ -0,0 +1 @@
1
+ f68518d3020fd6430af89b8a7e2261ff0bcc4e6325685baca3256f5f99bf51e8003353fa85ce95984de17cf3a19223bcd8d2cfa7df0fc07887ef937919493208
@@ -0,0 +1,59 @@
1
+ RSpec.describe "Auditing pipeline" do # rubocop:disable RSpec/DescribeClass
2
+ let(:topic) { TestTopic.new }
3
+ let(:config) do
4
+ NulogyMessageBusConsumer::Config.new(
5
+ consumer_group_id: random_consumer_group,
6
+ bootstrap_servers: test_bootstrap_servers,
7
+ topic_name: topic.topic_name
8
+ )
9
+ end
10
+ let(:logger) { spy }
11
+
12
+ after { topic.close }
13
+
14
+ context "when some messages have not been processed" do
15
+ it "logs the list of unprocessed messages" do
16
+ produce_message(id: uuid(1))
17
+ process_message(id: uuid(1))
18
+ produce_message(id: uuid(2))
19
+
20
+ expect(logger).to receive(:warn).with(include_json(
21
+ event: "unprocessed_message",
22
+ kafka_message: {id: uuid(2)}
23
+ ))
24
+
25
+ run_audit_pipeline
26
+ end
27
+ end
28
+
29
+ context "when all messages have been processed" do
30
+ it "does not log anything" do
31
+ produce_message(id: uuid(1))
32
+ process_message(id: uuid(1))
33
+
34
+ run_audit_pipeline
35
+
36
+ expect(logger).to have_not_received(:warn)
37
+ end
38
+ end
39
+
40
+ def run_audit_pipeline
41
+ NulogyMessageBusConsumer
42
+ .consumer_audit_pipeline(config: config, logger: logger)
43
+ .invoke
44
+ end
45
+
46
+ def produce_message(id:)
47
+ topic.produce_one_message(
48
+ payload: JSON.dump(id: id)
49
+ )
50
+ end
51
+
52
+ def process_message(id:)
53
+ NulogyMessageBusConsumer::ProcessedMessage.create!(id: id)
54
+ end
55
+
56
+ def uuid(id)
57
+ format("00000000-0000-0000-0000-%012d", id)
58
+ end
59
+ end
@@ -0,0 +1,41 @@
1
+ RSpec.describe NulogyMessageBusConsumer::KafkaUtils do
2
+ subject(:utils) { NulogyMessageBusConsumer::KafkaUtils }
3
+
4
+ let(:topic) { TestTopic.new }
5
+
6
+ after { topic.close }
7
+
8
+ describe "#seek_beginning" do
9
+ it "updates the consumer offset to the beginning of the topic" do
10
+ topic.produce_one_message(payload: "First Message")
11
+ expect(topic.consume_one_message).to have_attributes(payload: "First Message")
12
+ expect(topic.consume_one_message).to eq(nil)
13
+
14
+ utils.seek_beginning(topic.consumer)
15
+
16
+ expect(topic.consume_one_message).to have_attributes(payload: "First Message")
17
+ end
18
+ end
19
+
20
+ describe "#seek_end" do
21
+ it "updates the consumer offset to the end of the topic" do
22
+ topic.produce_one_message(payload: "First Message")
23
+
24
+ utils.seek_ending(topic.consumer)
25
+
26
+ expect(topic.consume_one_message).to eq(nil)
27
+ end
28
+ end
29
+
30
+ describe "#every_message_until_none_are_left" do
31
+ it "does not keep the connection open when there are no messages" do
32
+ topic.produce_one_message(payload: "The Only Message")
33
+
34
+ enum = utils.every_message_until_none_are_left(topic.consumer)
35
+
36
+ expect(enum).to match([
37
+ have_attributes(payload: "The Only Message")
38
+ ])
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,131 @@
1
+ RSpec.describe NulogyMessageBusConsumer::Steps::CommitOnSuccess do
2
+ let(:test_topic) { TestTopic.new }
3
+ let(:consumer) { test_topic.consumer }
4
+ let(:logger) { NulogyMessageBusConsumer::NullLogger.new }
5
+ let(:handler) { spy }
6
+ let(:pipeline) do
7
+ NulogyMessageBusConsumer::Pipeline.new([
8
+ NulogyMessageBusConsumer::Steps::ConnectToMessageBus.new(test_topic.config, logger, kafka_consumer: consumer),
9
+ NulogyMessageBusConsumer::Steps::SeekBeginningOfTopic.new,
10
+ NulogyMessageBusConsumer::Steps::StreamMessagesUntilNoneAreLeft.new(logger),
11
+ NulogyMessageBusConsumer::Steps::CommitOnSuccess.new,
12
+ handler
13
+ ])
14
+ end
15
+ let(:deduped_pipeline) do
16
+ NulogyMessageBusConsumer::Pipeline.new([
17
+ NulogyMessageBusConsumer::Steps::ConnectToMessageBus.new(test_topic.config, logger, kafka_consumer: consumer),
18
+ NulogyMessageBusConsumer::Steps::SeekBeginningOfTopic.new,
19
+ NulogyMessageBusConsumer::Steps::StreamMessagesUntilNoneAreLeft.new(logger),
20
+ NulogyMessageBusConsumer::Steps::DeduplicateMessages.new(logger),
21
+ NulogyMessageBusConsumer::Steps::CommitOnSuccess.new,
22
+ handler
23
+ ])
24
+ end
25
+
26
+ after { test_topic.close }
27
+
28
+ context "when successful" do
29
+ it "commits and processes the next message" do
30
+ expect(handler).to receive(:call).with(a_message_with(key: "test 1")).and_return(:success)
31
+ expect(handler).to receive(:call).with(a_message_with(key: "test 2")).and_return(:success)
32
+ expect(consumer).to receive(:commit).twice
33
+
34
+ test_topic.produce_one_message(key: "test 1")
35
+ test_topic.produce_one_message(key: "test 2")
36
+
37
+ pipeline.invoke
38
+ end
39
+ end
40
+
41
+ context "when failing by :failure" do
42
+ it "reprocesses the message" do
43
+ expect(handler).to receive(:call).with(a_message_with(key: "test 1")).and_return(:failure)
44
+ expect(handler).to receive(:call).with(a_message_with(key: "test 1")).and_return(:success)
45
+ expect(handler).to receive(:call).with(a_message_with(key: "test 2")).and_return(:success)
46
+ expect(consumer).to receive(:commit).twice
47
+
48
+ test_topic.produce_one_message(key: "test 1")
49
+ test_topic.produce_one_message(key: "test 2")
50
+
51
+ pipeline.invoke
52
+ pipeline.invoke
53
+ end
54
+
55
+ # This test is more illustrative of how we expect it to work.
56
+ # Specifically, testing the "auto.offset.store" setting for the consumer.
57
+ context "when a partition has a failing message" do
58
+ let(:handler) { ->(message:, **_) { message.event_data[:type] == "good" ? :success : :failure } }
59
+
60
+ it "processes messages from other partitions without committing offsets for partitions with failing messages" do
61
+ Kafka.create_topic(test_topic.topic_name)
62
+
63
+ # Produce message to a single partition. This partition will be blocked by the second message.
64
+ test_topic.produce_one_message(partition: 1, event_json: {type: "good"}) # success
65
+ test_topic.produce_one_message(partition: 1, event_json: {type: "bad"}) # failure
66
+ blocked_id = test_topic.produce_one_message(partition: 1, event_json: {type: "good"}) # blocked
67
+
68
+ consume_from_partition(1) do
69
+ deduped_pipeline.invoke
70
+ end
71
+
72
+ # produce to another partition
73
+ success_id = test_topic.produce_one_message(partition: 2, event_json: {type: "good"}) # success
74
+
75
+ consume_from_partition(2) do
76
+ deduped_pipeline.invoke
77
+ end
78
+
79
+ # try consuming from all partitions again -- it will fail on the blocked one again
80
+ deduped_pipeline.invoke
81
+
82
+ # Wait for assignment after a reconnect
83
+ NulogyMessageBusConsumer::KafkaUtils.wait_for_assignment(consumer)
84
+
85
+ lag = consumer.lag(consumer.committed)
86
+ expect(lag.dig(test_topic.topic_name, 1)).to be >= 1
87
+ expect(lag.dig(test_topic.topic_name, 2)).to be(0)
88
+ expect(NulogyMessageBusConsumer::ProcessedMessage.exists?(success_id)).to be(true)
89
+ expect(NulogyMessageBusConsumer::ProcessedMessage.exists?(blocked_id)).to be(false)
90
+ end
91
+ end
92
+ end
93
+
94
+ context "when failing by exception" do
95
+ it "reprocesses the message" do
96
+ expect(handler).to receive(:call).with(a_message_with(key: "test 1")).and_raise("intentional error")
97
+ expect(handler).to receive(:call).with(a_message_with(key: "test 1")).and_return(:success)
98
+ expect(handler).to receive(:call).with(a_message_with(key: "test 2")).and_return(:success)
99
+ expect(consumer).to receive(:commit).twice
100
+
101
+ test_topic.produce_one_message(key: "test 1")
102
+ test_topic.produce_one_message(key: "test 2")
103
+
104
+ expect {
105
+ pipeline.invoke
106
+ }.to raise_error("intentional error")
107
+
108
+ pipeline.invoke
109
+ end
110
+ end
111
+
112
+ def a_message_with(matcher)
113
+ hash_including(
114
+ message: have_attributes(matcher)
115
+ )
116
+ end
117
+
118
+ def consume_from_partition(partition_number)
119
+ original_assignment = consumer.assignment
120
+ topic_partitions = original_assignment
121
+ .to_h
122
+ .transform_values { |values| values.select { |t| t.partition == partition_number } }
123
+ new_assignment = Rdkafka::Consumer::TopicPartitionList.new(topic_partitions)
124
+
125
+ consumer.assign(new_assignment)
126
+
127
+ yield
128
+ ensure
129
+ consumer.assign(original_assignment)
130
+ end
131
+ end