rdkafka 0.6.0 → 0.9.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.semaphore/semaphore.yml +23 -0
  3. data/CHANGELOG.md +27 -0
  4. data/README.md +9 -9
  5. data/docker-compose.yml +17 -11
  6. data/ext/README.md +10 -15
  7. data/ext/Rakefile +24 -3
  8. data/lib/rdkafka.rb +8 -0
  9. data/lib/rdkafka/abstract_handle.rb +82 -0
  10. data/lib/rdkafka/admin.rb +155 -0
  11. data/lib/rdkafka/admin/create_topic_handle.rb +27 -0
  12. data/lib/rdkafka/admin/create_topic_report.rb +22 -0
  13. data/lib/rdkafka/admin/delete_topic_handle.rb +27 -0
  14. data/lib/rdkafka/admin/delete_topic_report.rb +22 -0
  15. data/lib/rdkafka/bindings.rb +64 -18
  16. data/lib/rdkafka/callbacks.rb +106 -0
  17. data/lib/rdkafka/config.rb +38 -9
  18. data/lib/rdkafka/consumer.rb +221 -46
  19. data/lib/rdkafka/consumer/headers.rb +7 -5
  20. data/lib/rdkafka/consumer/partition.rb +1 -1
  21. data/lib/rdkafka/consumer/topic_partition_list.rb +6 -16
  22. data/lib/rdkafka/error.rb +35 -4
  23. data/lib/rdkafka/metadata.rb +92 -0
  24. data/lib/rdkafka/producer.rb +50 -24
  25. data/lib/rdkafka/producer/delivery_handle.rb +7 -49
  26. data/lib/rdkafka/producer/delivery_report.rb +7 -2
  27. data/lib/rdkafka/version.rb +3 -3
  28. data/rdkafka.gemspec +3 -3
  29. data/spec/rdkafka/abstract_handle_spec.rb +114 -0
  30. data/spec/rdkafka/admin/create_topic_handle_spec.rb +52 -0
  31. data/spec/rdkafka/admin/create_topic_report_spec.rb +16 -0
  32. data/spec/rdkafka/admin/delete_topic_handle_spec.rb +52 -0
  33. data/spec/rdkafka/admin/delete_topic_report_spec.rb +16 -0
  34. data/spec/rdkafka/admin_spec.rb +203 -0
  35. data/spec/rdkafka/bindings_spec.rb +28 -10
  36. data/spec/rdkafka/callbacks_spec.rb +20 -0
  37. data/spec/rdkafka/config_spec.rb +51 -9
  38. data/spec/rdkafka/consumer/message_spec.rb +6 -1
  39. data/spec/rdkafka/consumer_spec.rb +287 -20
  40. data/spec/rdkafka/error_spec.rb +7 -3
  41. data/spec/rdkafka/metadata_spec.rb +78 -0
  42. data/spec/rdkafka/producer/delivery_handle_spec.rb +3 -43
  43. data/spec/rdkafka/producer/delivery_report_spec.rb +5 -1
  44. data/spec/rdkafka/producer_spec.rb +220 -100
  45. data/spec/spec_helper.rb +34 -6
  46. metadata +37 -13
  47. data/.travis.yml +0 -34
@@ -1,6 +1,6 @@
1
1
  module Rdkafka
2
2
  class Producer
3
- # Delivery report for a succesfully produced message.
3
+ # Delivery report for a successfully produced message.
4
4
  class DeliveryReport
5
5
  # The partition this message was produced to.
6
6
  # @return [Integer]
@@ -10,11 +10,16 @@ module Rdkafka
10
10
  # @return [Integer]
11
11
  attr_reader :offset
12
12
 
13
+ # Error in case happen during produce.
14
+ # @return [string]
15
+ attr_reader :error
16
+
13
17
  private
14
18
 
15
- def initialize(partition, offset)
19
+ def initialize(partition, offset, error = nil)
16
20
  @partition = partition
17
21
  @offset = offset
22
+ @error = error
18
23
  end
19
24
  end
20
25
  end
@@ -1,5 +1,5 @@
1
1
  module Rdkafka
2
- VERSION = "0.6.0"
3
- LIBRDKAFKA_VERSION = "1.1.0"
4
- LIBRDKAFKA_SOURCE_SHA256 = "123b47404c16bcde194b4bd1221c21fdce832ad12912bd8074f88f64b2b86f2b"
2
+ VERSION = "0.9.0"
3
+ LIBRDKAFKA_VERSION = "1.4.0"
4
+ LIBRDKAFKA_SOURCE_SHA256 = "ae27ea3f3d0d32d29004e7f709efbba2666c5383a107cc45b3a1949486b2eb84"
5
5
  end
data/rdkafka.gemspec CHANGED
@@ -4,7 +4,7 @@ Gem::Specification.new do |gem|
4
4
  gem.authors = ['Thijs Cadier']
5
5
  gem.email = ["thijs@appsignal.com"]
6
6
  gem.description = "Modern Kafka client library for Ruby based on librdkafka"
7
- gem.summary = "Kafka client library wrapping librdkafka using the ffi gem and futures from concurrent-ruby for Kafka 0.10+"
7
+ gem.summary = "The rdkafka gem is a modern Kafka client library for Ruby based on librdkafka. It wraps the production-ready C client using the ffi gem and targets Kafka 1.0+ and Ruby 2.4+."
8
8
  gem.license = 'MIT'
9
9
  gem.homepage = 'https://github.com/thijsc/rdkafka-ruby'
10
10
 
@@ -14,12 +14,12 @@ Gem::Specification.new do |gem|
14
14
  gem.name = 'rdkafka'
15
15
  gem.require_paths = ['lib']
16
16
  gem.version = Rdkafka::VERSION
17
- gem.required_ruby_version = '>= 2.1'
17
+ gem.required_ruby_version = '>= 2.4'
18
18
  gem.extensions = %w(ext/Rakefile)
19
19
 
20
20
  gem.add_dependency 'ffi', '~> 1.9'
21
21
  gem.add_dependency 'mini_portile2', '~> 2.1'
22
- gem.add_dependency 'rake', '~> 12.3'
22
+ gem.add_dependency 'rake', '>= 12.3'
23
23
 
24
24
  gem.add_development_dependency 'pry', '~> 0.10'
25
25
  gem.add_development_dependency 'rspec', '~> 3.5'
@@ -0,0 +1,114 @@
1
+ require "spec_helper"
2
+
3
+ describe Rdkafka::AbstractHandle do
4
+ let(:response) { 0 }
5
+ let(:result) { -1 }
6
+
7
+ context "A subclass that does not implement the required methods" do
8
+
9
+ class BadTestHandle < Rdkafka::AbstractHandle
10
+ layout :pending, :bool,
11
+ :response, :int
12
+ end
13
+
14
+ it "raises an exception if operation_name is called" do
15
+ expect {
16
+ BadTestHandle.new.operation_name
17
+ }.to raise_exception(RuntimeError, /Must be implemented by subclass!/)
18
+ end
19
+
20
+ it "raises an exception if create_result is called" do
21
+ expect {
22
+ BadTestHandle.new.create_result
23
+ }.to raise_exception(RuntimeError, /Must be implemented by subclass!/)
24
+ end
25
+ end
26
+
27
+ class TestHandle < Rdkafka::AbstractHandle
28
+ layout :pending, :bool,
29
+ :response, :int,
30
+ :result, :int
31
+
32
+ def operation_name
33
+ "test_operation"
34
+ end
35
+
36
+ def create_result
37
+ self[:result]
38
+ end
39
+ end
40
+
41
+ subject do
42
+ TestHandle.new.tap do |handle|
43
+ handle[:pending] = pending_handle
44
+ handle[:response] = response
45
+ handle[:result] = result
46
+ end
47
+ end
48
+
49
+ describe ".register and .remove" do
50
+ let(:pending_handle) { true }
51
+
52
+ it "should register and remove a delivery handle" do
53
+ Rdkafka::AbstractHandle.register(subject)
54
+ removed = Rdkafka::AbstractHandle.remove(subject.to_ptr.address)
55
+ expect(removed).to eq subject
56
+ expect(Rdkafka::AbstractHandle::REGISTRY).to be_empty
57
+ end
58
+ end
59
+
60
+ describe "#pending?" do
61
+ context "when true" do
62
+ let(:pending_handle) { true }
63
+
64
+ it "should be true" do
65
+ expect(subject.pending?).to be true
66
+ end
67
+ end
68
+
69
+ context "when not true" do
70
+ let(:pending_handle) { false }
71
+
72
+ it "should be false" do
73
+ expect(subject.pending?).to be false
74
+ end
75
+ end
76
+ end
77
+
78
+ describe "#wait" do
79
+ let(:pending_handle) { true }
80
+
81
+ it "should wait until the timeout and then raise an error" do
82
+ expect {
83
+ subject.wait(max_wait_timeout: 0.1)
84
+ }.to raise_error Rdkafka::AbstractHandle::WaitTimeoutError, /test_operation/
85
+ end
86
+
87
+ context "when not pending anymore and no error" do
88
+ let(:pending_handle) { false }
89
+ let(:result) { 1 }
90
+
91
+ it "should return a result" do
92
+ wait_result = subject.wait
93
+ expect(wait_result).to eq(result)
94
+ end
95
+
96
+ it "should wait without a timeout" do
97
+ wait_result = subject.wait(max_wait_timeout: nil)
98
+ expect(wait_result).to eq(result)
99
+ end
100
+ end
101
+
102
+ context "when not pending anymore and there was an error" do
103
+ let(:pending_handle) { false }
104
+ let(:response) { 20 }
105
+
106
+ it "should raise an rdkafka error" do
107
+ expect {
108
+ subject.wait
109
+ }.to raise_error Rdkafka::RdkafkaError
110
+ end
111
+ end
112
+ end
113
+ end
114
+
@@ -0,0 +1,52 @@
1
+ require "spec_helper"
2
+
3
+ describe Rdkafka::Admin::CreateTopicHandle do
4
+ let(:response) { 0 }
5
+
6
+ subject do
7
+ Rdkafka::Admin::CreateTopicHandle.new.tap do |handle|
8
+ handle[:pending] = pending_handle
9
+ handle[:response] = response
10
+ handle[:error_string] = FFI::Pointer::NULL
11
+ handle[:result_name] = FFI::MemoryPointer.from_string("my-test-topic")
12
+ end
13
+ end
14
+
15
+ describe "#wait" do
16
+ let(:pending_handle) { true }
17
+
18
+ it "should wait until the timeout and then raise an error" do
19
+ expect {
20
+ subject.wait(max_wait_timeout: 0.1)
21
+ }.to raise_error Rdkafka::Admin::CreateTopicHandle::WaitTimeoutError, /create topic/
22
+ end
23
+
24
+ context "when not pending anymore and no error" do
25
+ let(:pending_handle) { false }
26
+
27
+ it "should return a create topic report" do
28
+ report = subject.wait
29
+
30
+ expect(report.error_string).to eq(nil)
31
+ expect(report.result_name).to eq("my-test-topic")
32
+ end
33
+
34
+ it "should wait without a timeout" do
35
+ report = subject.wait(max_wait_timeout: nil)
36
+
37
+ expect(report.error_string).to eq(nil)
38
+ expect(report.result_name).to eq("my-test-topic")
39
+ end
40
+ end
41
+ end
42
+
43
+ describe "#raise_error" do
44
+ let(:pending_handle) { false }
45
+
46
+ it "should raise the appropriate error" do
47
+ expect {
48
+ subject.raise_error
49
+ }.to raise_exception(Rdkafka::RdkafkaError, /Success \(no_error\)/)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,16 @@
1
+ require "spec_helper"
2
+
3
+ describe Rdkafka::Admin::CreateTopicReport do
4
+ subject { Rdkafka::Admin::CreateTopicReport.new(
5
+ FFI::MemoryPointer.from_string("error string"),
6
+ FFI::MemoryPointer.from_string("result name")
7
+ )}
8
+
9
+ it "should get the error string" do
10
+ expect(subject.error_string).to eq("error string")
11
+ end
12
+
13
+ it "should get the result name" do
14
+ expect(subject.result_name).to eq("result name")
15
+ end
16
+ end
@@ -0,0 +1,52 @@
1
+ require "spec_helper"
2
+
3
+ describe Rdkafka::Admin::DeleteTopicHandle do
4
+ let(:response) { 0 }
5
+
6
+ subject do
7
+ Rdkafka::Admin::DeleteTopicHandle.new.tap do |handle|
8
+ handle[:pending] = pending_handle
9
+ handle[:response] = response
10
+ handle[:error_string] = FFI::Pointer::NULL
11
+ handle[:result_name] = FFI::MemoryPointer.from_string("my-test-topic")
12
+ end
13
+ end
14
+
15
+ describe "#wait" do
16
+ let(:pending_handle) { true }
17
+
18
+ it "should wait until the timeout and then raise an error" do
19
+ expect {
20
+ subject.wait(max_wait_timeout: 0.1)
21
+ }.to raise_error Rdkafka::Admin::DeleteTopicHandle::WaitTimeoutError, /delete topic/
22
+ end
23
+
24
+ context "when not pending anymore and no error" do
25
+ let(:pending_handle) { false }
26
+
27
+ it "should return a delete topic report" do
28
+ report = subject.wait
29
+
30
+ expect(report.error_string).to eq(nil)
31
+ expect(report.result_name).to eq("my-test-topic")
32
+ end
33
+
34
+ it "should wait without a timeout" do
35
+ report = subject.wait(max_wait_timeout: nil)
36
+
37
+ expect(report.error_string).to eq(nil)
38
+ expect(report.result_name).to eq("my-test-topic")
39
+ end
40
+ end
41
+ end
42
+
43
+ describe "#raise_error" do
44
+ let(:pending_handle) { false }
45
+
46
+ it "should raise the appropriate error" do
47
+ expect {
48
+ subject.raise_error
49
+ }.to raise_exception(Rdkafka::RdkafkaError, /Success \(no_error\)/)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,16 @@
1
+ require "spec_helper"
2
+
3
+ describe Rdkafka::Admin::DeleteTopicReport do
4
+ subject { Rdkafka::Admin::DeleteTopicReport.new(
5
+ FFI::MemoryPointer.from_string("error string"),
6
+ FFI::MemoryPointer.from_string("result name")
7
+ )}
8
+
9
+ it "should get the error string" do
10
+ expect(subject.error_string).to eq("error string")
11
+ end
12
+
13
+ it "should get the result name" do
14
+ expect(subject.result_name).to eq("result name")
15
+ end
16
+ end
@@ -0,0 +1,203 @@
1
+ require "spec_helper"
2
+ require "ostruct"
3
+
4
+ describe Rdkafka::Admin do
5
+ let(:config) { rdkafka_config }
6
+ let(:admin) { config.admin }
7
+
8
+ after do
9
+ # Registry should always end up being empty
10
+ expect(Rdkafka::Admin::CreateTopicHandle::REGISTRY).to be_empty
11
+ admin.close
12
+ end
13
+
14
+ let(:topic_name) { "test-topic-#{Random.new.rand(0..1_000_000)}" }
15
+ let(:topic_partition_count) { 3 }
16
+ let(:topic_replication_factor) { 1 }
17
+ let(:topic_config) { {"cleanup.policy" => "compact", "min.cleanable.dirty.ratio" => 0.8} }
18
+ let(:invalid_topic_config) { {"cleeeeenup.policee" => "campact"} }
19
+
20
+ describe "#create_topic" do
21
+ describe "called with invalid input" do
22
+ describe "with an invalid topic name" do
23
+ # https://github.com/apache/kafka/blob/trunk/clients/src/main/java/org/apache/kafka/common/internals/Topic.java#L29
24
+ # public static final String LEGAL_CHARS = "[a-zA-Z0-9._-]";
25
+ let(:topic_name) { "[!@#]" }
26
+
27
+ it "raises an exception" do
28
+ create_topic_handle = admin.create_topic(topic_name, topic_partition_count, topic_replication_factor)
29
+ expect {
30
+ create_topic_handle.wait(max_wait_timeout: 15.0)
31
+ }.to raise_exception { |ex|
32
+ expect(ex).to be_a(Rdkafka::RdkafkaError)
33
+ expect(ex.message).to match(/Broker: Invalid topic \(topic_exception\)/)
34
+ expect(ex.broker_message).to match(/Topic name.*is illegal, it contains a character other than ASCII alphanumerics/)
35
+ }
36
+ end
37
+ end
38
+
39
+ describe "with the name of a topic that already exists" do
40
+ let(:topic_name) { "empty_test_topic" } # created in spec_helper.rb
41
+
42
+ it "raises an exception" do
43
+ create_topic_handle = admin.create_topic(topic_name, topic_partition_count, topic_replication_factor)
44
+ expect {
45
+ create_topic_handle.wait(max_wait_timeout: 15.0)
46
+ }.to raise_exception { |ex|
47
+ expect(ex).to be_a(Rdkafka::RdkafkaError)
48
+ expect(ex.message).to match(/Broker: Topic already exists \(topic_already_exists\)/)
49
+ expect(ex.broker_message).to match(/Topic 'empty_test_topic' already exists/)
50
+ }
51
+ end
52
+ end
53
+
54
+ describe "with an invalid partition count" do
55
+ let(:topic_partition_count) { -1 }
56
+
57
+ it "raises an exception" do
58
+ expect {
59
+ admin.create_topic(topic_name, topic_partition_count, topic_replication_factor)
60
+ }.to raise_error Rdkafka::Config::ConfigError, /num_partitions out of expected range/
61
+ end
62
+ end
63
+
64
+ describe "with an invalid replication factor" do
65
+ let(:topic_replication_factor) { -2 }
66
+
67
+ it "raises an exception" do
68
+ expect {
69
+ admin.create_topic(topic_name, topic_partition_count, topic_replication_factor)
70
+ }.to raise_error Rdkafka::Config::ConfigError, /replication_factor out of expected range/
71
+ end
72
+ end
73
+
74
+ describe "with an invalid topic configuration" do
75
+ it "doesn't create the topic" do
76
+ create_topic_handle = admin.create_topic(topic_name, topic_partition_count, topic_replication_factor, invalid_topic_config)
77
+ expect {
78
+ create_topic_handle.wait(max_wait_timeout: 15.0)
79
+ }.to raise_error Rdkafka::RdkafkaError, /Broker: Configuration is invalid \(invalid_config\)/
80
+ end
81
+ end
82
+ end
83
+
84
+ context "edge case" do
85
+ context "where we are unable to get the background queue" do
86
+ before do
87
+ allow(Rdkafka::Bindings).to receive(:rd_kafka_queue_get_background).and_return(FFI::Pointer::NULL)
88
+ end
89
+
90
+ it "raises an exception" do
91
+ expect {
92
+ admin.create_topic(topic_name, topic_partition_count, topic_replication_factor)
93
+ }.to raise_error Rdkafka::Config::ConfigError, /rd_kafka_queue_get_background was NULL/
94
+ end
95
+ end
96
+
97
+ context "where rd_kafka_CreateTopics raises an exception" do
98
+ before do
99
+ allow(Rdkafka::Bindings).to receive(:rd_kafka_CreateTopics).and_raise(RuntimeError.new("oops"))
100
+ end
101
+
102
+ it "raises an exception" do
103
+ expect {
104
+ admin.create_topic(topic_name, topic_partition_count, topic_replication_factor)
105
+ }.to raise_error RuntimeError, /oops/
106
+ end
107
+ end
108
+ end
109
+
110
+ it "creates a topic" do
111
+ create_topic_handle = admin.create_topic(topic_name, topic_partition_count, topic_replication_factor, topic_config)
112
+ create_topic_report = create_topic_handle.wait(max_wait_timeout: 15.0)
113
+ expect(create_topic_report.error_string).to be_nil
114
+ expect(create_topic_report.result_name).to eq(topic_name)
115
+ end
116
+ end
117
+
118
+ describe "#delete_topic" do
119
+ describe "called with invalid input" do
120
+ # https://github.com/apache/kafka/blob/trunk/clients/src/main/java/org/apache/kafka/common/internals/Topic.java#L29
121
+ # public static final String LEGAL_CHARS = "[a-zA-Z0-9._-]";
122
+ describe "with an invalid topic name" do
123
+ let(:topic_name) { "[!@#]" }
124
+
125
+ it "raises an exception" do
126
+ delete_topic_handle = admin.delete_topic(topic_name)
127
+ expect {
128
+ delete_topic_handle.wait(max_wait_timeout: 15.0)
129
+ }.to raise_exception { |ex|
130
+ expect(ex).to be_a(Rdkafka::RdkafkaError)
131
+ expect(ex.message).to match(/Broker: Unknown topic or partition \(unknown_topic_or_part\)/)
132
+ expect(ex.broker_message).to match(/Broker: Unknown topic or partition/)
133
+ }
134
+ end
135
+ end
136
+
137
+ describe "with the name of a topic that does not exist" do
138
+ it "raises an exception" do
139
+ delete_topic_handle = admin.delete_topic(topic_name)
140
+ expect {
141
+ delete_topic_handle.wait(max_wait_timeout: 15.0)
142
+ }.to raise_exception { |ex|
143
+ expect(ex).to be_a(Rdkafka::RdkafkaError)
144
+ expect(ex.message).to match(/Broker: Unknown topic or partition \(unknown_topic_or_part\)/)
145
+ expect(ex.broker_message).to match(/Broker: Unknown topic or partition/)
146
+ }
147
+ end
148
+ end
149
+ end
150
+
151
+ context "edge case" do
152
+ context "where we are unable to get the background queue" do
153
+ before do
154
+ allow(Rdkafka::Bindings).to receive(:rd_kafka_queue_get_background).and_return(FFI::Pointer::NULL)
155
+ end
156
+
157
+ it "raises an exception" do
158
+ expect {
159
+ admin.delete_topic(topic_name)
160
+ }.to raise_error Rdkafka::Config::ConfigError, /rd_kafka_queue_get_background was NULL/
161
+ end
162
+ end
163
+
164
+ context "where rd_kafka_DeleteTopics raises an exception" do
165
+ before do
166
+ allow(Rdkafka::Bindings).to receive(:rd_kafka_DeleteTopics).and_raise(RuntimeError.new("oops"))
167
+ end
168
+
169
+ it "raises an exception" do
170
+ expect {
171
+ admin.delete_topic(topic_name)
172
+ }.to raise_error RuntimeError, /oops/
173
+ end
174
+ end
175
+ end
176
+
177
+
178
+ it "deletes a topic that was newly created" do
179
+ create_topic_handle = admin.create_topic(topic_name, topic_partition_count, topic_replication_factor)
180
+ create_topic_report = create_topic_handle.wait(max_wait_timeout: 15.0)
181
+ expect(create_topic_report.error_string).to be_nil
182
+ expect(create_topic_report.result_name).to eq(topic_name)
183
+
184
+ # Retry topic deletion a few times. On CI Kafka seems to not
185
+ # always be ready for it immediately
186
+ delete_topic_report = nil
187
+ 10.times do |i|
188
+ begin
189
+ delete_topic_handle = admin.delete_topic(topic_name)
190
+ delete_topic_report = delete_topic_handle.wait(max_wait_timeout: 15.0)
191
+ break
192
+ rescue Rdkafka::RdkafkaError => ex
193
+ if i > 3
194
+ raise ex
195
+ end
196
+ end
197
+ end
198
+
199
+ expect(delete_topic_report.error_string).to be_nil
200
+ expect(delete_topic_report.result_name).to eq(topic_name)
201
+ end
202
+ end
203
+ end